From 6ff7404a48fa541c02dec42f2a2a6f369f6d8c16 Mon Sep 17 00:00:00 2001 From: Bryan Date: Thu, 10 Dec 2015 19:01:40 -0800 Subject: [PATCH 1/6] Multi Project Validator A tool which searches recursively for project.json files, runs a set of analyses and reports on the result. For CLI we have only one rule currently, that Dependencies between projects must be equivalent to avoid stomping. --- Microsoft.DotNet.Cli.sln | 23 +++- src/Microsoft.DotNet.Cli.Utils/Constants.cs | 1 + tools/MultiProjectValidator/AnalysisResult.cs | 36 ++++++ .../AnalysisRules/DependencyMismatchRule.cs | 94 +++++++++++++++ tools/MultiProjectValidator/IAnalysisRule.cs | 13 ++ .../MultiProjectValidator.xproj | 18 +++ tools/MultiProjectValidator/Program.cs | 114 ++++++++++++++++++ .../MultiProjectValidator/ProjectAnalyzer.cs | 46 +++++++ tools/MultiProjectValidator/ProjectLoader.cs | 41 +++++++ tools/MultiProjectValidator/project.json | 19 +++ 10 files changed, 404 insertions(+), 1 deletion(-) create mode 100644 tools/MultiProjectValidator/AnalysisResult.cs create mode 100644 tools/MultiProjectValidator/AnalysisRules/DependencyMismatchRule.cs create mode 100644 tools/MultiProjectValidator/IAnalysisRule.cs create mode 100644 tools/MultiProjectValidator/MultiProjectValidator.xproj create mode 100644 tools/MultiProjectValidator/Program.cs create mode 100644 tools/MultiProjectValidator/ProjectAnalyzer.cs create mode 100644 tools/MultiProjectValidator/ProjectLoader.cs create mode 100644 tools/MultiProjectValidator/project.json diff --git a/Microsoft.DotNet.Cli.sln b/Microsoft.DotNet.Cli.sln index 4b66ff8c2..ca2cf369b 100644 --- a/Microsoft.DotNet.Cli.sln +++ b/Microsoft.DotNet.Cli.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.24720.0 +VisualStudioVersion = 14.0.23107.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.DotNet.Cli", "src\Microsoft.DotNet.Cli\Microsoft.DotNet.Cli.xproj", "{60CF7E6C-D6C8-439D-B7B7-D8A27E29BE2C}" EndProject @@ -57,6 +57,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "LoadContextTest", "test\Loa EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.DotNet.Tools.New", "src\Microsoft.DotNet.Tools.New\Microsoft.DotNet.Tools.New.xproj", "{BC765FBF-AD7A-4A99-9902-5540C5A74181}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{0722D325-24C8-4E83-B5AF-0A083E7F0749}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "MultiProjectValidator", "tools\MultiProjectValidator\MultiProjectValidator.xproj", "{08A68C6A-86F6-4ED2-89A7-B166D33E9F85}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -421,6 +425,22 @@ Global {BC765FBF-AD7A-4A99-9902-5540C5A74181}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU {BC765FBF-AD7A-4A99-9902-5540C5A74181}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU {BC765FBF-AD7A-4A99-9902-5540C5A74181}.RelWithDebInfo|x64.Build.0 = Release|Any CPU + {08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.Debug|x64.ActiveCfg = Debug|Any CPU + {08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.Debug|x64.Build.0 = Debug|Any CPU + {08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.MinSizeRel|Any CPU.ActiveCfg = Debug|Any CPU + {08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.MinSizeRel|Any CPU.Build.0 = Debug|Any CPU + {08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.MinSizeRel|x64.ActiveCfg = Debug|Any CPU + {08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.MinSizeRel|x64.Build.0 = Debug|Any CPU + {08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.Release|Any CPU.Build.0 = Release|Any CPU + {08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.Release|x64.ActiveCfg = Release|Any CPU + {08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.Release|x64.Build.0 = Release|Any CPU + {08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.RelWithDebInfo|Any CPU.ActiveCfg = Release|Any CPU + {08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU + {08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU + {08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.RelWithDebInfo|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -448,5 +468,6 @@ Global {947DD232-8D9B-4B78-9C6A-94F807D2DD58} = {17735A9D-BFD9-4585-A7CB-3208CA6EA8A7} {7A75ACC4-3C2F-44E1-B492-0EC08704E9FF} = {17735A9D-BFD9-4585-A7CB-3208CA6EA8A7} {BC765FBF-AD7A-4A99-9902-5540C5A74181} = {ED2FE3E2-F7E7-4389-8231-B65123F2076F} + {08A68C6A-86F6-4ED2-89A7-B166D33E9F85} = {0722D325-24C8-4E83-B5AF-0A083E7F0749} EndGlobalSection EndGlobal diff --git a/src/Microsoft.DotNet.Cli.Utils/Constants.cs b/src/Microsoft.DotNet.Cli.Utils/Constants.cs index e8b5e6333..378a0c3db 100644 --- a/src/Microsoft.DotNet.Cli.Utils/Constants.cs +++ b/src/Microsoft.DotNet.Cli.Utils/Constants.cs @@ -11,6 +11,7 @@ namespace Microsoft.DotNet.Cli.Utils { internal static class Constants { + public static readonly string ProjectFileName = "project.json"; public static readonly string ExeSuffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty; // Priority order of runnable suffixes to look for and run diff --git a/tools/MultiProjectValidator/AnalysisResult.cs b/tools/MultiProjectValidator/AnalysisResult.cs new file mode 100644 index 000000000..eee0ecc14 --- /dev/null +++ b/tools/MultiProjectValidator/AnalysisResult.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MultiProjectValidator +{ + public class AnalysisResult + { + + private List _messages; + private bool _passed; + + public AnalysisResult(List messages, bool passed) + { + this._messages = messages; + this._passed = passed; + } + + public List Messages + { + get + { + return _messages; + } + } + + public bool Passed + { + get + { + return _passed; + } + } + } +} diff --git a/tools/MultiProjectValidator/AnalysisRules/DependencyMismatchRule.cs b/tools/MultiProjectValidator/AnalysisRules/DependencyMismatchRule.cs new file mode 100644 index 000000000..865190550 --- /dev/null +++ b/tools/MultiProjectValidator/AnalysisRules/DependencyMismatchRule.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.DotNet.ProjectModel; + +namespace MultiProjectValidator.AnalysisRules +{ + public class DependencyMismatchRule : IAnalysisRule + { + public AnalysisResult Evaluate(List projectContexts) + { + var targetGroupedContexts = GroupContextsByTarget(projectContexts); + + var failureMessages = EvaluateTargetContextGroups(targetGroupedContexts); + var pass = failureMessages.Count == 0; + + var result = new AnalysisResult(failureMessages, pass); + return result; + } + + private List EvaluateTargetContextGroups(Dictionary> targetGroupedContexts) + { + var failureMessages = new List(); + + foreach (var target in targetGroupedContexts.Keys) + { + var targetContexts = targetGroupedContexts[target]; + + failureMessages.AddRange(EvaluateTargetContextGroup(targetContexts)); + } + + return failureMessages; + } + + private List EvaluateTargetContextGroup(List targetContexts) + { + var failedMessages = new List(); + var assemblyVersionMap = new Dictionary(); + + foreach(var context in targetContexts) + { + var libraries = context.LibraryManager.GetLibraries(); + + foreach(var library in libraries) + { + var name = library.Identity.Name; + var version = library.Identity.Version.ToString(); + + if (assemblyVersionMap.ContainsKey(name)) + { + var existingVersion = assemblyVersionMap[name]; + if (!string.Equals(existingVersion, version, StringComparison.OrdinalIgnoreCase)) + { + string message = + $"Dependency mismatch in {context.ProjectFile.ProjectFilePath} for dependency {name}. Versions {version}, {existingVersion}"; + + failedMessages.Add(message); + } + } + else + { + assemblyVersionMap[name] = version; + } + } + } + + return failedMessages; + } + + private Dictionary> GroupContextsByTarget(List projectContexts) + { + var targetContextMap = new Dictionary>(); + foreach (var context in projectContexts) + { + var target = context.TargetFramework + context.RuntimeIdentifier; + + if (targetContextMap.ContainsKey(target)) + { + targetContextMap[target].Add(context); + } + else + { + targetContextMap[target] = new List() + { + context + }; + } + } + + return targetContextMap; + } + } +} diff --git a/tools/MultiProjectValidator/IAnalysisRule.cs b/tools/MultiProjectValidator/IAnalysisRule.cs new file mode 100644 index 000000000..566d01ca1 --- /dev/null +++ b/tools/MultiProjectValidator/IAnalysisRule.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.DotNet.ProjectModel; + +namespace MultiProjectValidator +{ + public interface IAnalysisRule + { + AnalysisResult Evaluate(List projectContexts); + } +} diff --git a/tools/MultiProjectValidator/MultiProjectValidator.xproj b/tools/MultiProjectValidator/MultiProjectValidator.xproj new file mode 100644 index 000000000..c5974d197 --- /dev/null +++ b/tools/MultiProjectValidator/MultiProjectValidator.xproj @@ -0,0 +1,18 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 08a68c6a-86f6-4ed2-89a7-b166d33e9f85 + ProjectSanity + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\tools\$(MSBuildProjectName)\ + + + 2.0 + + + \ No newline at end of file diff --git a/tools/MultiProjectValidator/Program.cs b/tools/MultiProjectValidator/Program.cs new file mode 100644 index 000000000..db01ffce5 --- /dev/null +++ b/tools/MultiProjectValidator/Program.cs @@ -0,0 +1,114 @@ +using System; +using System.IO; +using System.Collections.Generic; +using Microsoft.DotNet.ProjectModel; +using System.Linq; + +namespace MultiProjectValidator +{ + public class Program + { + public static int Main(string[] args) + { + var rootPath = ParseAndValidateArgs(args); + + List projects = null; + try + { + projects = ProjectLoader.Load(rootPath); + } + catch(Exception e) + { + Console.WriteLine("Failed to load projects from path: " + rootPath); + Exit(1); + } + + var analyzer = ProjectAnalyzer.Create(projects); + var analysisResults = analyzer.DoAnalysis(); + var failed = analysisResults.Where((a) => !a.Passed).Any(); + + PrintAnalysisResults(analysisResults); + + return failed ? 1 : 0; + } + + private static void PrintAnalysisResults(List analysisResults) + { + Console.WriteLine("Project Validation Results"); + + var failedCount = analysisResults.Where((a) => !a.Passed).Count(); + var passedCount = analysisResults.Where((a) => a.Passed).Count(); + + Console.WriteLine($"{passedCount} Successful Analysis Rules"); + Console.WriteLine($"{failedCount} Failed Analysis Rules"); + + if(failedCount != 0) + { + Console.WriteLine("Failure Messages"); + + foreach(var result in analysisResults) + { + if (!result.Passed) + { + foreach(var message in result.Messages) + { + Console.WriteLine(message); + } + } + } + } + } + + private static bool AnyAnalysisFailed(List analysisResults) + { + foreach (var result in analysisResults) + { + if (!result.Passed) + { + return true; + } + } + return false; + } + + private static string ParseAndValidateArgs(string[] args) + { + if (args.Length != 1) + { + PrintHelp(); + Exit(1); + } + + string rootPath = args[0]; + + if (!Directory.Exists(rootPath)) + { + Console.WriteLine("Root Directory does not exist: " + rootPath); + Exit(1); + } + + return rootPath; + } + + private static void Exit(int code) + { + Environment.Exit(code); + } + + private static void PrintHelp() + { + var help = @" +Multi-Project Validator + +Description: +This tool recursively searches for project.json's from the given root path. +It then applies a set of analysis rules, determines whether they pass/fail +and then sets exit code to reflect. + +Usage: +pjvalidate [root path of recursive search]"; + + Console.WriteLine(help); + } + } +} diff --git a/tools/MultiProjectValidator/ProjectAnalyzer.cs b/tools/MultiProjectValidator/ProjectAnalyzer.cs new file mode 100644 index 000000000..c0517bbfb --- /dev/null +++ b/tools/MultiProjectValidator/ProjectAnalyzer.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.DotNet.ProjectModel; +using MultiProjectValidator.AnalysisRules; + +namespace MultiProjectValidator +{ + public class ProjectAnalyzer + { + + public static ProjectAnalyzer Create(List projectContexts) + { + // Any Additional rules would be added here + var rules = new List + { + new DependencyMismatchRule() + }; + + return new ProjectAnalyzer(rules, projectContexts); + } + + private List projectContexts; + private List rules; + + private ProjectAnalyzer(List rules, List projectContexts) + { + this.rules = rules; + this.projectContexts = projectContexts; + } + + public List DoAnalysis() + { + var results = new List(); + + foreach(var rule in rules) + { + results.Add(rule.Evaluate(projectContexts)); + } + + return results; + } + + } +} diff --git a/tools/MultiProjectValidator/ProjectLoader.cs b/tools/MultiProjectValidator/ProjectLoader.cs new file mode 100644 index 000000000..b4f214ed0 --- /dev/null +++ b/tools/MultiProjectValidator/ProjectLoader.cs @@ -0,0 +1,41 @@ +using System; +using System.IO; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.DotNet.ProjectModel; + +namespace MultiProjectValidator +{ + public class ProjectLoader + { + private static readonly string PROJECT_FILENAME = "project.json"; + + public static List Load(string rootPath, bool recursive=true) + { + var projectFiles = DiscoverProjectFiles(rootPath); + var projectContextList = LoadProjectContexts(projectFiles); + + return projectContextList; + } + + private static string[] DiscoverProjectFiles(string rootPath) + { + return Directory.GetFiles(rootPath, PROJECT_FILENAME, SearchOption.AllDirectories); + } + + private static List LoadProjectContexts(string[] projectFiles) + { + var projectContexts = new List(); + + foreach (var file in projectFiles) + { + var fileTargetContexts = ProjectContext.CreateContextForEachTarget(file); + + projectContexts.AddRange(fileTargetContexts); + } + + return projectContexts; + } + } +} diff --git a/tools/MultiProjectValidator/project.json b/tools/MultiProjectValidator/project.json new file mode 100644 index 000000000..0f1e860c7 --- /dev/null +++ b/tools/MultiProjectValidator/project.json @@ -0,0 +1,19 @@ +{ + "version": "1.0.0-*", + "name": "pjvalidate", + "compilationOptions": { + "emitEntryPoint": true + }, + + "dependencies": { + "Microsoft.NETCore.Runtime": "1.0.1-beta-*", + "NETStandard.Library": "1.0.0-rc2-23608", + "Microsoft.DotNet.ProjectModel": "1.0.0-*", + "Microsoft.DotNet.Cli.Utils": "1.0.0-*", + "System.Linq": "4.0.1-rc2-23608" + }, + + "frameworks": { + "dnxcore50": { } + } +} From d502f3d57e9f01c00f38f9e3712ee017b66d1928 Mon Sep 17 00:00:00 2001 From: Bryan Date: Fri, 11 Dec 2015 14:05:32 -0800 Subject: [PATCH 2/6] Add Project Validation to build scripts. Fail on a failed analysis. --- scripts/compile.ps1 | 8 ++++++++ scripts/compile.sh | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/scripts/compile.ps1 b/scripts/compile.ps1 index af843eeff..e3e73336c 100644 --- a/scripts/compile.ps1 +++ b/scripts/compile.ps1 @@ -146,6 +146,14 @@ Download it from https://www.cmake.org Exit 1 } + # Run Validation for Project.json dependencies + dotnet publish $RepoRoot\tools\MultiProjectValidator -o $Stage2Dir\..\tools + & "$Stage2Dir\..\tools\pjvalidate" "$RepoRoot\src" + if (!$?) { + Write-Host "Project Validation Failed" + Exit 1 + } + } finally { $env:PATH = $StartPath $env:DOTNET_HOME = $StartDotNetHome diff --git a/scripts/compile.sh b/scripts/compile.sh index f0e13742e..82ee86d08 100755 --- a/scripts/compile.sh +++ b/scripts/compile.sh @@ -118,3 +118,7 @@ DOTNET_HOME=$STAGE2_DIR DOTNET_TOOLS=$STAGE2_DIR $DIR/test/smoke-test.sh # E2E test on the output header "Testing stage2 End to End ..." DOTNET_HOME=$STAGE2_DIR DOTNET_TOOLS=$STAGE2_DIR $DIR/test/e2e-test.sh + +# Run Validation for Project.json dependencies +dotnet publish "$REPOROOT/tools/MultiProjectValidator" -o "$OUTPUT_DIR/../tools" +"$OUTPUT_DIR/tools/pjvalidate" "$REPOROOT/src" From f54b08ea5373b815316cf828c0cf328602937b43 Mon Sep 17 00:00:00 2001 From: Bryan Date: Mon, 14 Dec 2015 15:08:16 -0800 Subject: [PATCH 3/6] Add Console color and upgrade references to 23614 --- tools/MultiProjectValidator/Program.cs | 8 ++++++-- tools/MultiProjectValidator/project.json | 4 +--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/tools/MultiProjectValidator/Program.cs b/tools/MultiProjectValidator/Program.cs index db01ffce5..f5e90b645 100644 --- a/tools/MultiProjectValidator/Program.cs +++ b/tools/MultiProjectValidator/Program.cs @@ -39,10 +39,13 @@ namespace MultiProjectValidator var failedCount = analysisResults.Where((a) => !a.Passed).Count(); var passedCount = analysisResults.Where((a) => a.Passed).Count(); + Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine($"{passedCount} Successful Analysis Rules"); - Console.WriteLine($"{failedCount} Failed Analysis Rules"); - if(failedCount != 0) + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"{failedCount} Failed Analysis Rules"); + + if (failedCount != 0) { Console.WriteLine("Failure Messages"); @@ -57,6 +60,7 @@ namespace MultiProjectValidator } } } + Console.ForegroundColor = ConsoleColor.White; } private static bool AnyAnalysisFailed(List analysisResults) diff --git a/tools/MultiProjectValidator/project.json b/tools/MultiProjectValidator/project.json index 0f1e860c7..84914ef3e 100644 --- a/tools/MultiProjectValidator/project.json +++ b/tools/MultiProjectValidator/project.json @@ -6,11 +6,9 @@ }, "dependencies": { - "Microsoft.NETCore.Runtime": "1.0.1-beta-*", - "NETStandard.Library": "1.0.0-rc2-23608", + "NETStandard.Library": "1.0.0-rc2-23614", "Microsoft.DotNet.ProjectModel": "1.0.0-*", "Microsoft.DotNet.Cli.Utils": "1.0.0-*", - "System.Linq": "4.0.1-rc2-23608" }, "frameworks": { From dd5c0bb4234543fe7ec3501c739de5e58d72499c Mon Sep 17 00:00:00 2001 From: Bryan Date: Mon, 14 Dec 2015 15:15:02 -0800 Subject: [PATCH 4/6] do not fail build on pjvalidate failure --- scripts/compile.ps1 | 9 +++++---- scripts/compile.sh | 3 +++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/compile.ps1 b/scripts/compile.ps1 index e3e73336c..9ec813ac6 100644 --- a/scripts/compile.ps1 +++ b/scripts/compile.ps1 @@ -149,10 +149,11 @@ Download it from https://www.cmake.org # Run Validation for Project.json dependencies dotnet publish $RepoRoot\tools\MultiProjectValidator -o $Stage2Dir\..\tools & "$Stage2Dir\..\tools\pjvalidate" "$RepoRoot\src" - if (!$?) { - Write-Host "Project Validation Failed" - Exit 1 - } + # TODO For release builds, this should be uncommented and fail. + # if (!$?) { + # Write-Host "Project Validation Failed" + # Exit 1 + # } } finally { $env:PATH = $StartPath diff --git a/scripts/compile.sh b/scripts/compile.sh index 82ee86d08..70ad33031 100755 --- a/scripts/compile.sh +++ b/scripts/compile.sh @@ -121,4 +121,7 @@ DOTNET_HOME=$STAGE2_DIR DOTNET_TOOLS=$STAGE2_DIR $DIR/test/e2e-test.sh # Run Validation for Project.json dependencies dotnet publish "$REPOROOT/tools/MultiProjectValidator" -o "$OUTPUT_DIR/../tools" +#TODO for release builds this should fail +set +e "$OUTPUT_DIR/tools/pjvalidate" "$REPOROOT/src" +set -e From 3b848c0487bf439f602f3ed2a20d663e0bfbc91b Mon Sep 17 00:00:00 2001 From: Bryan Date: Tue, 15 Dec 2015 18:09:08 -0800 Subject: [PATCH 5/6] Change Output format to match PR feedback Don't use Environment.Exit() PR Feedback, Unecessary Usings and Immutable types --- scripts/compile.sh | 4 +- tools/MultiProjectValidator/AnalysisResult.cs | 3 - .../DependencyMismatch/DependencyGroup.cs | 53 +++++++++ .../DependencyMismatch/DependencyInfo.cs | 21 ++++ .../AnalysisRules/DependencyMismatchRule.cs | 106 +++++++++++++----- tools/MultiProjectValidator/IAnalysisRule.cs | 5 +- tools/MultiProjectValidator/Program.cs | 23 ++-- .../MultiProjectValidator/ProjectAnalyzer.cs | 5 +- tools/MultiProjectValidator/ProjectLoader.cs | 5 +- 9 files changed, 169 insertions(+), 56 deletions(-) create mode 100644 tools/MultiProjectValidator/AnalysisRules/DependencyMismatch/DependencyGroup.cs create mode 100644 tools/MultiProjectValidator/AnalysisRules/DependencyMismatch/DependencyInfo.cs diff --git a/scripts/compile.sh b/scripts/compile.sh index 70ad33031..5105123c1 100755 --- a/scripts/compile.sh +++ b/scripts/compile.sh @@ -120,8 +120,8 @@ header "Testing stage2 End to End ..." DOTNET_HOME=$STAGE2_DIR DOTNET_TOOLS=$STAGE2_DIR $DIR/test/e2e-test.sh # Run Validation for Project.json dependencies -dotnet publish "$REPOROOT/tools/MultiProjectValidator" -o "$OUTPUT_DIR/../tools" +dotnet publish "$REPOROOT/tools/MultiProjectValidator" -o "$STAGE2_DIR/../tools" #TODO for release builds this should fail set +e -"$OUTPUT_DIR/tools/pjvalidate" "$REPOROOT/src" +"$STAGE2_DIR/../tools/pjvalidate" "$REPOROOT/src" set -e diff --git a/tools/MultiProjectValidator/AnalysisResult.cs b/tools/MultiProjectValidator/AnalysisResult.cs index eee0ecc14..52c8b3336 100644 --- a/tools/MultiProjectValidator/AnalysisResult.cs +++ b/tools/MultiProjectValidator/AnalysisResult.cs @@ -1,7 +1,4 @@ -using System; using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace MultiProjectValidator { diff --git a/tools/MultiProjectValidator/AnalysisRules/DependencyMismatch/DependencyGroup.cs b/tools/MultiProjectValidator/AnalysisRules/DependencyMismatch/DependencyGroup.cs new file mode 100644 index 000000000..59a51096e --- /dev/null +++ b/tools/MultiProjectValidator/AnalysisRules/DependencyMismatch/DependencyGroup.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; + +namespace ProjectSanity.AnalysisRules.DependencyMismatch +{ + internal class DependencyGroup + { + public static DependencyGroup CreateWithEntry(DependencyInfo dependencyInfo) + { + var dependencyGroup = new DependencyGroup + { + DependencyName = dependencyInfo.Name, + VersionDependencyInfoMap = new Dictionary>() + }; + + dependencyGroup.AddEntry(dependencyInfo); + + return dependencyGroup; + } + + public string DependencyName { get; private set; } + public Dictionary> VersionDependencyInfoMap { get; private set; } + + public bool HasConflict + { + get + { + return VersionDependencyInfoMap.Count > 1; + } + } + + public void AddEntry(DependencyInfo dependencyInfo) + { + if (!dependencyInfo.Name.Equals(DependencyName, StringComparison.OrdinalIgnoreCase)) + { + throw new Exception("Added dependency does not match group"); + } + + if (VersionDependencyInfoMap.ContainsKey(dependencyInfo.Version)) + { + VersionDependencyInfoMap[dependencyInfo.Version].Add(dependencyInfo); + } + else + { + VersionDependencyInfoMap[dependencyInfo.Version] = new List() + { + dependencyInfo + }; + } + } + + } +} diff --git a/tools/MultiProjectValidator/AnalysisRules/DependencyMismatch/DependencyInfo.cs b/tools/MultiProjectValidator/AnalysisRules/DependencyMismatch/DependencyInfo.cs new file mode 100644 index 000000000..b91e3298c --- /dev/null +++ b/tools/MultiProjectValidator/AnalysisRules/DependencyMismatch/DependencyInfo.cs @@ -0,0 +1,21 @@ +using Microsoft.DotNet.ProjectModel; + +namespace ProjectSanity.AnalysisRules.DependencyMismatch +{ + internal class DependencyInfo + { + public static DependencyInfo Create(ProjectContext context, LibraryDescription library) + { + return new DependencyInfo + { + ProjectPath = context.ProjectFile.ProjectFilePath, + Version = library.Identity.Version.ToString(), + Name = library.Identity.Name + }; + } + + public string ProjectPath { get; private set; } + public string Version { get; private set; } + public string Name { get; private set; } + } +} diff --git a/tools/MultiProjectValidator/AnalysisRules/DependencyMismatchRule.cs b/tools/MultiProjectValidator/AnalysisRules/DependencyMismatchRule.cs index 865190550..67fab1014 100644 --- a/tools/MultiProjectValidator/AnalysisRules/DependencyMismatchRule.cs +++ b/tools/MultiProjectValidator/AnalysisRules/DependencyMismatchRule.cs @@ -1,8 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Microsoft.DotNet.ProjectModel; +using System.Text; +using ProjectSanity.AnalysisRules.DependencyMismatch; namespace MultiProjectValidator.AnalysisRules { @@ -12,62 +13,73 @@ namespace MultiProjectValidator.AnalysisRules { var targetGroupedContexts = GroupContextsByTarget(projectContexts); - var failureMessages = EvaluateTargetContextGroups(targetGroupedContexts); + var failureMessages = EvaluateProjectContextTargetGroups(targetGroupedContexts); var pass = failureMessages.Count == 0; var result = new AnalysisResult(failureMessages, pass); return result; } - private List EvaluateTargetContextGroups(Dictionary> targetGroupedContexts) + private List EvaluateProjectContextTargetGroups(Dictionary> targetGroupedProjectContexts) { var failureMessages = new List(); - foreach (var target in targetGroupedContexts.Keys) + foreach (var target in targetGroupedProjectContexts.Keys) { - var targetContexts = targetGroupedContexts[target]; + var targetProjectContextGroup = targetGroupedProjectContexts[target]; - failureMessages.AddRange(EvaluateTargetContextGroup(targetContexts)); + var groupFailureMessages = EvaluateProjectContextTargetGroup(targetProjectContextGroup); + + if (groupFailureMessages.Count > 0) + { + string aggregateFailureMessage = $"Failures for Target {target} {Environment.NewLine}{Environment.NewLine}" + + string.Join("", groupFailureMessages); + + failureMessages.Add(aggregateFailureMessage); + } } return failureMessages; } - private List EvaluateTargetContextGroup(List targetContexts) + private List EvaluateProjectContextTargetGroup(List targetProjectContextGroup) { var failedMessages = new List(); - var assemblyVersionMap = new Dictionary(); - foreach(var context in targetContexts) + var dependencyGroups = CreateDependencyGroups(targetProjectContextGroup); + + + foreach (var dependencyGroup in dependencyGroups) { - var libraries = context.LibraryManager.GetLibraries(); - - foreach(var library in libraries) + if(dependencyGroup.HasConflict) { - var name = library.Identity.Name; - var version = library.Identity.Version.ToString(); - - if (assemblyVersionMap.ContainsKey(name)) - { - var existingVersion = assemblyVersionMap[name]; - if (!string.Equals(existingVersion, version, StringComparison.OrdinalIgnoreCase)) - { - string message = - $"Dependency mismatch in {context.ProjectFile.ProjectFilePath} for dependency {name}. Versions {version}, {existingVersion}"; - - failedMessages.Add(message); - } - } - else - { - assemblyVersionMap[name] = version; - } + failedMessages.Add(GetDependencyGroupConflictMessage(dependencyGroup)); } } return failedMessages; } + private string GetDependencyGroupConflictMessage(DependencyGroup dependencyGroup) + { + StringBuilder sb = new StringBuilder(); + + sb.Append($"Conflict for {dependencyGroup.DependencyName} in projects:{Environment.NewLine}"); + + foreach (var version in dependencyGroup.VersionDependencyInfoMap.Keys) + { + var dependencyInfoList = dependencyGroup.VersionDependencyInfoMap[version]; + + foreach (var dependencyInfo in dependencyInfoList) + { + sb.Append($"Version: {dependencyInfo.Version} Path: {dependencyInfo.ProjectPath} {Environment.NewLine}"); + } + } + sb.Append(Environment.NewLine); + + return sb.ToString(); + } + private Dictionary> GroupContextsByTarget(List projectContexts) { var targetContextMap = new Dictionary>(); @@ -90,5 +102,39 @@ namespace MultiProjectValidator.AnalysisRules return targetContextMap; } + + private List CreateDependencyGroups(List projectContexts) + { + var libraryNameDependencyGroupMap = new Dictionary(); + + foreach (var projectContext in projectContexts) + { + var libraries = projectContext.LibraryManager.GetLibraries(); + + foreach (var library in libraries) + { + var dependencyInfo = DependencyInfo.Create(projectContext, library); + + if (libraryNameDependencyGroupMap.ContainsKey(dependencyInfo.Name)) + { + var dependencyGroup = libraryNameDependencyGroupMap[dependencyInfo.Name]; + + dependencyGroup.AddEntry(dependencyInfo); + } + else + { + var dependencyGroup = DependencyGroup.CreateWithEntry(dependencyInfo); + + libraryNameDependencyGroupMap[dependencyInfo.Name] = dependencyGroup; + } + } + } + + return libraryNameDependencyGroupMap.Values.ToList(); + } + + + + } } diff --git a/tools/MultiProjectValidator/IAnalysisRule.cs b/tools/MultiProjectValidator/IAnalysisRule.cs index 566d01ca1..7130bca79 100644 --- a/tools/MultiProjectValidator/IAnalysisRule.cs +++ b/tools/MultiProjectValidator/IAnalysisRule.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.Collections.Generic; using Microsoft.DotNet.ProjectModel; namespace MultiProjectValidator diff --git a/tools/MultiProjectValidator/Program.cs b/tools/MultiProjectValidator/Program.cs index f5e90b645..776a07657 100644 --- a/tools/MultiProjectValidator/Program.cs +++ b/tools/MultiProjectValidator/Program.cs @@ -10,7 +10,17 @@ namespace MultiProjectValidator { public static int Main(string[] args) { - var rootPath = ParseAndValidateArgs(args); + + string rootPath = null; + + try + { + rootPath = ParseAndValidateArgs(args); + } + catch + { + return 1; + } List projects = null; try @@ -20,7 +30,7 @@ namespace MultiProjectValidator catch(Exception e) { Console.WriteLine("Failed to load projects from path: " + rootPath); - Exit(1); + return 1; } var analyzer = ProjectAnalyzer.Create(projects); @@ -80,7 +90,7 @@ namespace MultiProjectValidator if (args.Length != 1) { PrintHelp(); - Exit(1); + throw new Exception(); } string rootPath = args[0]; @@ -88,16 +98,11 @@ namespace MultiProjectValidator if (!Directory.Exists(rootPath)) { Console.WriteLine("Root Directory does not exist: " + rootPath); - Exit(1); + throw new Exception(); } return rootPath; } - - private static void Exit(int code) - { - Environment.Exit(code); - } private static void PrintHelp() { diff --git a/tools/MultiProjectValidator/ProjectAnalyzer.cs b/tools/MultiProjectValidator/ProjectAnalyzer.cs index c0517bbfb..8ea50e5a6 100644 --- a/tools/MultiProjectValidator/ProjectAnalyzer.cs +++ b/tools/MultiProjectValidator/ProjectAnalyzer.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.Collections.Generic; using Microsoft.DotNet.ProjectModel; using MultiProjectValidator.AnalysisRules; diff --git a/tools/MultiProjectValidator/ProjectLoader.cs b/tools/MultiProjectValidator/ProjectLoader.cs index b4f214ed0..e0088568b 100644 --- a/tools/MultiProjectValidator/ProjectLoader.cs +++ b/tools/MultiProjectValidator/ProjectLoader.cs @@ -1,8 +1,5 @@ -using System; -using System.IO; +using System.IO; using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.DotNet.ProjectModel; namespace MultiProjectValidator From c7b14fcdc06e396d997ae69702f4cbb4b86d6561 Mon Sep 17 00:00:00 2001 From: Bryan Date: Wed, 16 Dec 2015 14:10:39 -0800 Subject: [PATCH 6/6] PR Feedback: match coding guidelines --- tools/MultiProjectValidator/AnalysisResult.cs | 4 ++-- tools/MultiProjectValidator/Program.cs | 3 +++ tools/MultiProjectValidator/ProjectAnalyzer.cs | 12 ++++++------ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/tools/MultiProjectValidator/AnalysisResult.cs b/tools/MultiProjectValidator/AnalysisResult.cs index 52c8b3336..0e9ff65d5 100644 --- a/tools/MultiProjectValidator/AnalysisResult.cs +++ b/tools/MultiProjectValidator/AnalysisResult.cs @@ -10,8 +10,8 @@ namespace MultiProjectValidator public AnalysisResult(List messages, bool passed) { - this._messages = messages; - this._passed = passed; + _messages = messages; + _passed = passed; } public List Messages diff --git a/tools/MultiProjectValidator/Program.cs b/tools/MultiProjectValidator/Program.cs index 776a07657..1acd9fbf2 100644 --- a/tools/MultiProjectValidator/Program.cs +++ b/tools/MultiProjectValidator/Program.cs @@ -114,6 +114,9 @@ This tool recursively searches for project.json's from the given root path. It then applies a set of analysis rules, determines whether they pass/fail and then sets exit code to reflect. +Note: +Ensure all analyzed project.json have been restored prior to running this tool. + Usage: pjvalidate [root path of recursive search]"; diff --git a/tools/MultiProjectValidator/ProjectAnalyzer.cs b/tools/MultiProjectValidator/ProjectAnalyzer.cs index 8ea50e5a6..8a4de240b 100644 --- a/tools/MultiProjectValidator/ProjectAnalyzer.cs +++ b/tools/MultiProjectValidator/ProjectAnalyzer.cs @@ -18,22 +18,22 @@ namespace MultiProjectValidator return new ProjectAnalyzer(rules, projectContexts); } - private List projectContexts; - private List rules; + private List _projectContexts; + private List _rules; private ProjectAnalyzer(List rules, List projectContexts) { - this.rules = rules; - this.projectContexts = projectContexts; + _rules = rules; + _projectContexts = projectContexts; } public List DoAnalysis() { var results = new List(); - foreach(var rule in rules) + foreach(var rule in _rules) { - results.Add(rule.Evaluate(projectContexts)); + results.Add(rule.Evaluate(_projectContexts)); } return results;