Project Migration Report

This commit is contained in:
Bryan Thornbury 2016-10-04 14:59:04 -07:00
parent 28bd507f40
commit fbaf9a679e
12 changed files with 342 additions and 128 deletions

View file

@ -20,7 +20,7 @@ namespace Microsoft.DotNet.ProjectJsonMigration
public void Throw()
{
throw new MigrationException(GetFormattedErrorMessage());
throw new MigrationException(this, GetFormattedErrorMessage());
}
public string GetFormattedErrorMessage()

View file

@ -7,6 +7,10 @@ namespace Microsoft.DotNet.ProjectJsonMigration
{
public class MigrationException : Exception
{
public MigrationException(string message) : base(message) { }
public MigrationError Error { get; }
public MigrationException(MigrationError error, string message) : base(message)
{
Error = error;
}
}
}

View file

@ -0,0 +1,21 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Collections.Generic;
using Microsoft.Build.Construction;
using System.Linq;
using Microsoft.DotNet.ProjectModel;
using System.IO;
namespace Microsoft.DotNet.ProjectJsonMigration
{
public static class ProjectContextExtensions
{
public static string GetProjectName(this ProjectContext projectContext)
{
// _ here is just an arbitrary configuration value so we can obtain the output name
return Path.GetFileNameWithoutExtension(projectContext.GetOutputPaths("_").CompilationFiles.Assembly);
}
}
}

View file

@ -26,40 +26,47 @@ namespace Microsoft.DotNet.ProjectJsonMigration
_ruleSet = ruleSet;
}
public void Migrate(MigrationSettings rootSettings, bool skipProjectReferences = false)
public MigrationReport Migrate(MigrationSettings rootSettings, bool skipProjectReferences = false)
{
if (rootSettings == null)
{
throw new ArgumentNullException();
}
Exception exc = null;
// Try to read the project dependencies, ignore an unresolved exception for now
MigrationRuleInputs rootInputs = ComputeMigrationRuleInputs(rootSettings);
IEnumerable<ProjectDependency> projectDependencies = null;
var tempMSBuildProjectTemplate = rootSettings.MSBuildProjectTemplate.DeepClone();
try
{
// Verify up front so we can prefer these errors over an unresolved project dependency
VerifyInputs(rootInputs, rootSettings);
projectDependencies = ResolveTransitiveClosureProjectDependencies(
rootSettings.ProjectDirectory,
rootSettings.ProjectXProjFilePath);
}
catch (Exception e)
catch (MigrationException e)
{
exc = e;
return new MigrationReport(
new List<ProjectMigrationReport>
{
new ProjectMigrationReport(
rootSettings.ProjectDirectory,
rootInputs?.DefaultProjectContext.GetProjectName(),
new List<MigrationError> {e.Error},
null)
});
}
// Verify up front so we can prefer these errors over an unresolved project dependency
VerifyInputs(ComputeMigrationRuleInputs(rootSettings), rootSettings);
if (exc != null)
{
throw exc;
}
MigrateProject(rootSettings);
var projectMigrationReports = new List<ProjectMigrationReport>();
projectMigrationReports.Add(MigrateProject(rootSettings));
if (skipProjectReferences)
{
return;
return new MigrationReport(projectMigrationReports);
}
foreach(var project in projectDependencies)
@ -70,7 +77,10 @@ namespace Microsoft.DotNet.ProjectJsonMigration
rootSettings.SdkPackageVersion,
tempMSBuildProjectTemplate);
MigrateProject(settings);
projectMigrationReports.Add(MigrateProject(settings));
}
return new MigrationReport(projectMigrationReports);
}
private void DeleteProjectJsons(MigrationSettings rootsettings, IEnumerable<ProjectDependency> projectDependencies)
@ -118,15 +128,17 @@ namespace Microsoft.DotNet.ProjectJsonMigration
return projectsMap;
}
private void MigrateProject(MigrationSettings migrationSettings)
private ProjectMigrationReport MigrateProject(MigrationSettings migrationSettings)
{
var migrationRuleInputs = ComputeMigrationRuleInputs(migrationSettings);
var projectName = migrationRuleInputs.DefaultProjectContext.GetProjectName();
try
{
if (IsMigrated(migrationSettings, migrationRuleInputs))
{
// TODO : Adding user-visible logging
MigrationTrace.Instance.WriteLine($"{nameof(ProjectMigrator)}: Skip migrating {migrationSettings.ProjectDirectory}, it is already migrated.");
return;
return new ProjectMigrationReport(migrationSettings.ProjectDirectory, projectName, skipped: true);
}
VerifyInputs(migrationRuleInputs, migrationSettings);
@ -135,6 +147,19 @@ namespace Microsoft.DotNet.ProjectJsonMigration
_ruleSet.Apply(migrationSettings, migrationRuleInputs);
}
catch (MigrationException exc)
{
var error = new List<MigrationError>
{
exc.Error
};
return new ProjectMigrationReport(migrationSettings.ProjectDirectory, projectName, error, null);
}
var outputProject = Path.Combine(migrationSettings.OutputDirectory, projectName + ".csproj");
return new ProjectMigrationReport(migrationSettings.ProjectDirectory, projectName, outputProject, null);
}
private MigrationRuleInputs ComputeMigrationRuleInputs(MigrationSettings migrationSettings)
{
@ -230,8 +255,7 @@ namespace Microsoft.DotNet.ProjectJsonMigration
public bool IsMigrated(MigrationSettings migrationSettings, MigrationRuleInputs migrationRuleInputs)
{
var outputName = Path.GetFileNameWithoutExtension(
migrationRuleInputs.DefaultProjectContext.GetOutputPaths("_").CompilationFiles.Assembly);
var outputName = migrationRuleInputs.DefaultProjectContext.GetProjectName();
var outputProject = Path.Combine(migrationSettings.OutputDirectory, outputName + ".csproj");
return File.Exists(outputProject);

View file

@ -0,0 +1,65 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.DotNet.ProjectJsonMigration
{
/// Any changes to this need to be reflected in roslyn-project-system
/// TODO add link
public class MigrationReport
{
public List<ProjectMigrationReport> ProjectMigrationReports { get; }
public int MigratedProjectsCount => ProjectMigrationReports.Count;
public int SucceededProjectsCount => ProjectMigrationReports.Count(p => p.Succeeded);
public int FailedProjectsCount => ProjectMigrationReports.Count(p => p.Failed);
public bool AllSucceeded => ! ProjectMigrationReports.Any(p => p.Failed);
public MigrationReport Merge(MigrationReport otherReport)
{
var allReports = ProjectMigrationReports.Concat(otherReport.ProjectMigrationReports).ToList();
var dedupedReports = DedupeSkippedReports(allReports);
return new MigrationReport(dedupedReports);
}
private List<ProjectMigrationReport> DedupeSkippedReports(List<ProjectMigrationReport> allReports)
{
var reportDict = new Dictionary<string, ProjectMigrationReport>();
foreach (var report in allReports)
{
ProjectMigrationReport existingReport;
if (reportDict.TryGetValue(report.ProjectDirectory, out existingReport))
{
if (existingReport.Skipped)
{
reportDict[report.ProjectDirectory] = report;
}
else if (!report.Skipped)
{
MigrationTrace.Instance.WriteLine("Detected double project migration: {report.ProjectDirectory}");
}
}
else
{
reportDict[report.ProjectDirectory] = report;
}
}
return reportDict.Values.ToList();
}
public MigrationReport(List<ProjectMigrationReport> projectMigrationReports)
{
ProjectMigrationReports = projectMigrationReports;
}
}
}

View file

@ -0,0 +1,45 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.DotNet.ProjectJsonMigration
{
public class ProjectMigrationReport
{
public string ProjectDirectory { get; }
public string ProjectName { get; }
public string OutputMSBuildProject { get; }
public List<MigrationError> Errors { get; }
public List<string> Warnings { get; }
public bool Skipped { get; }
public bool Failed => Errors.Any();
public bool Succeeded => !Errors.Any();
public ProjectMigrationReport(string projectDirectory, string projectName, bool skipped)
: this(projectDirectory, projectName, null, null, null, skipped: skipped) { }
public ProjectMigrationReport(string projectDirectory, string projectName, List<MigrationError> errors, List<string> warnings)
: this(projectDirectory, projectName, null, errors, warnings) { }
public ProjectMigrationReport(string projectDirectory, string projectName, string outputMSBuildProject, List<string> warnings)
: this(projectDirectory, projectName, outputMSBuildProject, null, warnings) { }
private ProjectMigrationReport(string projectDirectory, string projectName, string outputMSBuildProject, List<MigrationError> errors, List<string> warnings, bool skipped=false)
{
ProjectDirectory = projectDirectory;
ProjectName = projectName;
OutputMSBuildProject = outputMSBuildProject;
Errors = errors ?? new List<MigrationError>();
Warnings = warnings ?? new List<string>();
Skipped=skipped;
}
}
}

View file

@ -9,7 +9,6 @@ using Microsoft.Build.Construction;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.ProjectJsonMigration.Transforms;
using Microsoft.DotNet.ProjectModel;
using Microsoft.DotNet.ProjectModel.Graph;
using Microsoft.DotNet.Tools.Common;
using NuGet.Frameworks;
using NuGet.LibraryModel;
@ -142,7 +141,6 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Rules
transform = PackageDependencyTransform();
if (packageDependency.Type == LibraryDependencyType.Build)
{
Console.WriteLine("Build type!!!");
transform = transform.WithMetadata("PrivateAssets", "all");
}
else if (packageDependency.SuppressParent != LibraryIncludeFlagUtils.DefaultSuppressParent)

View file

@ -10,8 +10,7 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Rules
{
public void Apply(MigrationSettings migrationSettings, MigrationRuleInputs migrationRuleInputs)
{
var outputName = Path.GetFileNameWithoutExtension(
migrationRuleInputs.DefaultProjectContext.GetOutputPaths("_").CompilationFiles.Assembly);
var outputName = migrationRuleInputs.DefaultProjectContext.GetProjectName();
var outputProject = Path.Combine(migrationSettings.OutputDirectory, outputName + ".csproj");

View file

@ -5,7 +5,9 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.Build.Construction;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.ProjectJsonMigration;
using Microsoft.DotNet.ProjectModel;
@ -18,10 +20,19 @@ namespace Microsoft.DotNet.Tools.Migrate
private readonly string _sdkVersion;
private readonly string _xprojFilePath;
private readonly bool _skipProjectReferences;
private readonly string _reportFile;
private readonly bool _reportFormatJson;
private readonly TemporaryDotnetNewTemplateProject _temporaryDotnetNewProject;
public MigrateCommand(string templateFile, string projectArg, string sdkVersion, string xprojFilePath, bool skipProjectReferences)
public MigrateCommand(
string templateFile,
string projectArg,
string sdkVersion,
string xprojFilePath,
string reportFile,
bool skipProjectReferences,
bool reportFormatJson)
{
_templateFile = templateFile;
_projectArg = projectArg ?? Directory.GetCurrentDirectory();
@ -29,6 +40,8 @@ namespace Microsoft.DotNet.Tools.Migrate
_xprojFilePath = xprojFilePath;
_skipProjectReferences = skipProjectReferences;
_temporaryDotnetNewProject = new TemporaryDotnetNewTemplateProject();
_reportFile = reportFile;
_reportFormatJson = reportFormatJson;
}
public int Execute()
@ -42,16 +55,133 @@ namespace Microsoft.DotNet.Tools.Migrate
EnsureNotNull(sdkVersion, "Null Sdk Version");
MigrationReport migrationReport = null;
foreach (var project in projectsToMigrate)
{
Console.WriteLine($"Migrating project {project}..");
var projectDirectory = Path.GetDirectoryName(project);
var outputDirectory = projectDirectory;
var migrationSettings = new MigrationSettings(projectDirectory, outputDirectory, sdkVersion, msBuildTemplate, _xprojFilePath);
new ProjectMigrator().Migrate(migrationSettings, _skipProjectReferences);
var projectMigrationReport = new ProjectMigrator().Migrate(migrationSettings, _skipProjectReferences);
if (migrationReport == null)
{
migrationReport = projectMigrationReport;
}
else
{
migrationReport = migrationReport.Merge(projectMigrationReport);
}
}
return 0;
WriteReport(migrationReport);
return migrationReport.FailedProjectsCount;
}
private void WriteReport(MigrationReport migrationReport)
{
if (!string.IsNullOrEmpty(_reportFile))
{
using (var outputTextWriter = GetReportFileOutputTextWriter())
{
outputTextWriter.Write(GetReportContent(migrationReport));
}
}
WriteReportToStdOut(migrationReport);
}
private void WriteReportToStdOut(MigrationReport migrationReport)
{
StringBuilder sb = new StringBuilder();
foreach (var projectMigrationReport in migrationReport.ProjectMigrationReports)
{
var errorContent = GetProjectReportErrorContent(projectMigrationReport, colored: true);
var successContent = GetProjectReportSuccessContent(projectMigrationReport, colored: true);
if (!string.IsNullOrEmpty(errorContent))
{
Reporter.Error.WriteLine(errorContent);
}
else
{
Reporter.Output.WriteLine(successContent);
}
}
Reporter.Output.WriteLine(GetReportSummary(migrationReport));
}
private string GetReportContent(MigrationReport migrationReport, bool colored = false)
{
if (_reportFormatJson)
{
return Newtonsoft.Json.JsonConvert.SerializeObject(migrationReport);
}
StringBuilder sb = new StringBuilder();
foreach (var projectMigrationReport in migrationReport.ProjectMigrationReports)
{
var errorContent = GetProjectReportErrorContent(projectMigrationReport, colored: colored);
var successContent = GetProjectReportSuccessContent(projectMigrationReport, colored: colored);
if (!string.IsNullOrEmpty(errorContent))
{
sb.AppendLine(errorContent);
}
else
{
sb.AppendLine(successContent);
}
}
sb.AppendLine(GetReportSummary(migrationReport));
return sb.ToString();
}
private string GetReportSummary(MigrationReport migrationReport)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("Summary");
sb.AppendLine($"Total Projects: {migrationReport.MigratedProjectsCount}");
sb.AppendLine($"Succeeded Projects: {migrationReport.SucceededProjectsCount}");
sb.AppendLine($"Failed Projects: {migrationReport.FailedProjectsCount}");
return sb.ToString();
}
private string GetProjectReportSuccessContent(ProjectMigrationReport projectMigrationReport, bool colored)
{
Func<string, string> GreenIfColored = (str) => colored ? str.Green() : str;
return GreenIfColored($"Project {projectMigrationReport.ProjectName} migration succeeded ({projectMigrationReport.ProjectDirectory})");
}
private string GetProjectReportErrorContent(ProjectMigrationReport projectMigrationReport, bool colored)
{
StringBuilder sb = new StringBuilder();
Func<string, string> RedIfColored = (str) => colored ? str.Red() : str;
if (projectMigrationReport.Errors.Any())
{
sb.AppendLine(RedIfColored($"Project {projectMigrationReport.ProjectName} migration failed ({projectMigrationReport.ProjectDirectory})"));
foreach (var error in projectMigrationReport.Errors.Select(e => e.GetFormattedErrorMessage()))
{
sb.AppendLine(RedIfColored(error));
}
}
return sb.ToString();
}
private TextWriter GetReportFileOutputTextWriter()
{
return File.CreateText(_reportFile);
}
private IEnumerable<string> GetProjectsToMigrate(string projectArg)

View file

@ -41,6 +41,9 @@ namespace Microsoft.DotNet.Tools.Migrate
CommandOption xprojFile = app.Option("-x|--xproj-file", "The path to the xproj file to use. Required when there is more than one xproj in a project directory.", CommandOptionType.SingleValue);
CommandOption skipProjectReferences = app.Option("-s|--skip-project-references", "Skip migrating project references. By default project references are migrated recursively", CommandOptionType.BoolValue);
CommandOption reportFile = app.Option("-r|--report-file", "Output migration report to a file in addition to the console.", CommandOptionType.SingleValue);
CommandOption structuredReportOutput = app.Option("--format-report-file-json", "Output migration report file as json rather than user messages", CommandOptionType.BoolValue);
app.OnExecute(() =>
{
MigrateCommand migrateCommand = new MigrateCommand(
@ -48,7 +51,9 @@ namespace Microsoft.DotNet.Tools.Migrate
projectArgument.Value,
sdkVersion.Value(),
xprojFile.Value(),
skipProjectReferences.BoolValue.HasValue ? skipProjectReferences.BoolValue.Value : false);
reportFile.Value(),
skipProjectReferences.BoolValue.HasValue ? skipProjectReferences.BoolValue.Value : false,
structuredReportOutput.BoolValue.HasValue ? structuredReportOutput.BoolValue.Value : false);
return migrateCommand.Execute();
});

View file

@ -1,80 +0,0 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.DotNet.ProjectJsonMigration;
namespace Microsoft.DotNet.Tools.Migrate
{
/// <summary>
/// Parses select data from a project.json without relying on ProjectModel.
/// Used to parse simple information.
/// </summary>
internal class ProjectJsonParser
{
public string SdkPackageVersion { get; }
public ProjectJsonParser(JObject projectJson)
{
SdkPackageVersion = GetPackageVersion(projectJson, ConstantPackageNames.CSdkPackageName);
}
private string GetPackageVersion(JObject projectJson, string packageName)
{
var sdkPackageNode = SelectJsonNodes(projectJson, property => property.Name == packageName).First();
if (sdkPackageNode.Value.Type == JTokenType.String)
{
return (string)sdkPackageNode.Value;
}
else if (sdkPackageNode.Type == JTokenType.Object)
{
var sdkPackageNodeValue = (JObject)sdkPackageNode.Value;
JToken versionNode;
if (sdkPackageNodeValue.TryGetValue("version", out versionNode))
{
return versionNode.Value<string>();
}
else
{
throw new Exception("Unable to determine sdk version, no version node in default template.");
}
}
else
{
throw new Exception("Unable to determine sdk version, no version information found");
}
}
private IEnumerable<JProperty> SelectJsonNodes(
JToken jsonNode,
Func<JProperty, bool> condition,
List<JProperty> nodeAccumulator = null)
{
nodeAccumulator = nodeAccumulator ?? new List<JProperty>();
if (jsonNode.Type == JTokenType.Object)
{
var eligibleNodes = jsonNode.Children<JProperty>().Where(j => condition(j));
nodeAccumulator.AddRange(eligibleNodes);
foreach (var child in jsonNode.Children<JProperty>())
{
SelectJsonNodes(child.Value, condition, nodeAccumulator: nodeAccumulator);
}
}
else if (jsonNode.Type == JTokenType.Array)
{
foreach (var child in jsonNode.Children())
{
SelectJsonNodes(child, condition, nodeAccumulator: nodeAccumulator);
}
}
return nodeAccumulator;
}
}
}

View file

@ -37,7 +37,7 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Tests
}
[Fact]
public void It_throws_when_migrating_a_deprecated_projectJson()
public void It_has_error_when_migrating_a_deprecated_projectJson()
{
var testProjectDirectory =
TestAssetsManager.CreateTestInstance("TestLibraryWithDeprecatedProjectFile", callingMethod: "z")
@ -47,16 +47,18 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Tests
var testSettings = new MigrationSettings(testProjectDirectory, testProjectDirectory, "1.0.0", mockProj);
var projectMigrator = new ProjectMigrator(new FakeEmptyMigrationRule());
Action migrateAction = () => projectMigrator.Migrate(testSettings);
var report = projectMigrator.Migrate(testSettings);
migrateAction.ShouldThrow<Exception>().Where(
e => e.Message.Contains("MIGRATE1011::Deprecated Project:")
&& e.Message.Contains("The 'packInclude' option is deprecated. Use 'files' in 'packOptions' instead. (line: 6, file:")
&& e.Message.Contains("The 'compilationOptions' option is deprecated. Use 'buildOptions' instead. (line: 3, file:"));
var projectReport = report.ProjectMigrationReports.First();
var errorMessage = projectReport.Errors.First().GetFormattedErrorMessage();
errorMessage.Should().Contain("MIGRATE1011::Deprecated Project:");
errorMessage.Should().Contain("The 'packInclude' option is deprecated. Use 'files' in 'packOptions' instead. (line: 6, file:");
errorMessage.Should().Contain("The 'compilationOptions' option is deprecated. Use 'buildOptions' instead. (line: 3, file:");
}
[Fact]
public void It_throws_when_migrating_a_non_csharp_app()
public void It_has_error_when_migrating_a_non_csharp_app()
{
var testProjectDirectory =
TestAssetsManager.CreateTestInstance("FSharpTestProjects/TestApp", callingMethod: "z")
@ -66,10 +68,11 @@ namespace Microsoft.DotNet.ProjectJsonMigration.Tests
var testSettings = new MigrationSettings(testProjectDirectory, testProjectDirectory, "1.0.0", mockProj);
var projectMigrator = new ProjectMigrator(new FakeEmptyMigrationRule());
Action migrateAction = () => projectMigrator.Migrate(testSettings);
var report = projectMigrator.Migrate(testSettings);
var projectReport = report.ProjectMigrationReports.First();
migrateAction.ShouldThrow<Exception>().Where(
e => e.Message.Contains("MIGRATE20013::Non-Csharp App: Cannot migrate project"));
var errorMessage = projectReport.Errors.First().GetFormattedErrorMessage();
errorMessage.Should().Contain("MIGRATE20013::Non-Csharp App: Cannot migrate project");
}
private IEnumerable<string> EnumerateFilesWithRelativePath(string testProjectDirectory)