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.
This commit is contained in:
Bryan 2015-12-10 19:01:40 -08:00
parent b25c2743ca
commit 6ff7404a48
10 changed files with 404 additions and 1 deletions

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace MultiProjectValidator
{
public class AnalysisResult
{
private List<string> _messages;
private bool _passed;
public AnalysisResult(List<string> messages, bool passed)
{
this._messages = messages;
this._passed = passed;
}
public List<string> Messages
{
get
{
return _messages;
}
}
public bool Passed
{
get
{
return _passed;
}
}
}
}

View file

@ -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<ProjectContext> projectContexts)
{
var targetGroupedContexts = GroupContextsByTarget(projectContexts);
var failureMessages = EvaluateTargetContextGroups(targetGroupedContexts);
var pass = failureMessages.Count == 0;
var result = new AnalysisResult(failureMessages, pass);
return result;
}
private List<string> EvaluateTargetContextGroups(Dictionary<string, List<ProjectContext>> targetGroupedContexts)
{
var failureMessages = new List<string>();
foreach (var target in targetGroupedContexts.Keys)
{
var targetContexts = targetGroupedContexts[target];
failureMessages.AddRange(EvaluateTargetContextGroup(targetContexts));
}
return failureMessages;
}
private List<string> EvaluateTargetContextGroup(List<ProjectContext> targetContexts)
{
var failedMessages = new List<string>();
var assemblyVersionMap = new Dictionary<string, string>();
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<string, List<ProjectContext>> GroupContextsByTarget(List<ProjectContext> projectContexts)
{
var targetContextMap = new Dictionary<string, List<ProjectContext>>();
foreach (var context in projectContexts)
{
var target = context.TargetFramework + context.RuntimeIdentifier;
if (targetContextMap.ContainsKey(target))
{
targetContextMap[target].Add(context);
}
else
{
targetContextMap[target] = new List<ProjectContext>()
{
context
};
}
}
return targetContextMap;
}
}
}

View file

@ -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<ProjectContext> projectContexts);
}
}

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>08a68c6a-86f6-4ed2-89a7-b166d33e9f85</ProjectGuid>
<RootNamespace>ProjectSanity</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\tools\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View file

@ -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<ProjectContext> 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<AnalysisResult> 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<AnalysisResult> 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);
}
}
}

View file

@ -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<ProjectContext> projectContexts)
{
// Any Additional rules would be added here
var rules = new List<IAnalysisRule>
{
new DependencyMismatchRule()
};
return new ProjectAnalyzer(rules, projectContexts);
}
private List<ProjectContext> projectContexts;
private List<IAnalysisRule> rules;
private ProjectAnalyzer(List<IAnalysisRule> rules, List<ProjectContext> projectContexts)
{
this.rules = rules;
this.projectContexts = projectContexts;
}
public List<AnalysisResult> DoAnalysis()
{
var results = new List<AnalysisResult>();
foreach(var rule in rules)
{
results.Add(rule.Evaluate(projectContexts));
}
return results;
}
}
}

View file

@ -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<ProjectContext> 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<ProjectContext> LoadProjectContexts(string[] projectFiles)
{
var projectContexts = new List<ProjectContext>();
foreach (var file in projectFiles)
{
var fileTargetContexts = ProjectContext.CreateContextForEachTarget(file);
projectContexts.AddRange(fileTargetContexts);
}
return projectContexts;
}
}
}

View file

@ -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": { }
}
}