diff --git a/src/SlnGen.Build.Tasks.UnitTests/MSBuildProjectLoaderTests.cs b/src/SlnGen.Build.Tasks.UnitTests/MSBuildProjectLoaderTests.cs new file mode 100644 index 00000000..aafb8c52 --- /dev/null +++ b/src/SlnGen.Build.Tasks.UnitTests/MSBuildProjectLoaderTests.cs @@ -0,0 +1,151 @@ +// Copyright (c) Jeff Kluge. All rights reserved. +// +// Licensed under the MIT license. + +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities.ProjectCreation; +using Shouldly; +using SlnGen.Build.Tasks.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace SlnGen.Build.Tasks.UnitTests +{ + public class MSBuildProjectLoaderTests : TestBase + { + private const string MSBuildToolsVersion = "15.0"; + + [Fact] + public void ArgumentNullException_BuildEngine() + { + ArgumentNullException exception = Should.Throw(() => + { + MSBuildProjectLoader unused = new MSBuildProjectLoader(globalProperties: null, toolsVersion: null, buildEngine: null); + }); + + exception.ParamName.ShouldBe("buildEngine"); + } + + [Fact] + public void BuildFailsIfError() + { + ProjectCreator dirsProj = ProjectCreator + .Create(GetTempFileName()) + .Property("IsTraversal", "true") + .ItemInclude("ProjectFile", "does not exist") + .Save(); + + BuildEngine buildEngine = BuildEngine.Create(); + + MSBuildProjectLoader loader = new MSBuildProjectLoader(null, MSBuildToolsVersion, buildEngine); + + loader.LoadProjectsAndReferences(new[] { dirsProj.FullPath }); + + buildEngine.Errors.ShouldHaveSingleItem().ShouldStartWith("The project file could not be loaded. Could not find file "); + } + + [Fact] + public void GlobalPropertiesSetCorrectly() + { + Dictionary expectedGlobalProperties = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["Property1"] = "1A836FEB3ABA43B183034DFDD5C4E375", + ["Property2"] = "CEEC5C9FF0F344DAA32A0F545460EB2C" + }; + + ProjectCreator projectA = ProjectCreator + .Create(GetTempFileName()) + .Save(); + + BuildEngine buildEngine = BuildEngine.Create(); + + MSBuildProjectLoader loader = new MSBuildProjectLoader(expectedGlobalProperties, MSBuildToolsVersion, buildEngine); + + ProjectCollection projectCollection = loader.LoadProjectsAndReferences(new[] { projectA.FullPath }); + + projectCollection.GlobalProperties.ShouldBe(expectedGlobalProperties); + } + + [Fact] + public void InvalidProjectsLogGoodInfo() + { + ProjectCreator projectA = ProjectCreator + .Create(GetTempFileName()) + .Import(@"$(Foo)\foo.props") + .Save(); + + ProjectCreator dirsProj = ProjectCreator + .Create(GetTempFileName()) + .Property("IsTraversal", "true") + .ItemInclude("ProjectFile", projectA.FullPath) + .Save(); + + BuildEngine buildEngine = BuildEngine.Create(); + + MSBuildProjectLoader loader = new MSBuildProjectLoader(null, MSBuildToolsVersion, buildEngine); + + loader.LoadProjectsAndReferences(new[] { dirsProj.FullPath }); + + BuildErrorEventArgs errorEventArgs = buildEngine.ErrorEvents.ShouldHaveSingleItem(); + + errorEventArgs.Code.ShouldBe("MSB4019"); + errorEventArgs.ColumnNumber.ShouldBe(3); + errorEventArgs.HelpKeyword.ShouldBe("MSBuild.ImportedProjectNotFound"); + errorEventArgs.LineNumber.ShouldBe(3); + errorEventArgs.File.ShouldBe(projectA.FullPath); + } + + [Fact] + public void ProjectReferencesWork() + { + ProjectCreator projectB = ProjectCreator + .Create(GetTempFileName()) + .Save(); + + ProjectCreator projectA = ProjectCreator + .Create(GetTempFileName()) + .ItemProjectReference(projectB) + .Save(); + + BuildEngine buildEngine = BuildEngine.Create(); + + MSBuildProjectLoader loader = new MSBuildProjectLoader(null, MSBuildToolsVersion, buildEngine); + + ProjectCollection projectCollection = loader.LoadProjectsAndReferences(new[] { projectA.FullPath }); + + projectCollection.LoadedProjects.Select(i => i.FullPath).ShouldBe(new[] { projectA.FullPath, projectB.FullPath }); + } + + [Fact] + public void TraversalReferencesWork() + { + ProjectCreator projectB = ProjectCreator + .Create(GetTempFileName()) + .Save(); + + ProjectCreator projectA = ProjectCreator + .Create(GetTempFileName()) + .ItemProjectReference(projectB) + .Save(); + + ProjectCreator dirsProj = ProjectCreator + .Create(GetTempFileName()) + .Property("IsTraversal", "true") + .ItemInclude("ProjectFile", projectA.FullPath) + .Save(); + + BuildEngine buildEngine = BuildEngine.Create(); + + MSBuildProjectLoader loader = new MSBuildProjectLoader(null, MSBuildToolsVersion, buildEngine); + + ProjectCollection projectCollection = loader.LoadProjectsAndReferences(new[] { dirsProj.FullPath }); + + projectCollection.LoadedProjects.Select(i => i.FullPath).ShouldBe( + new[] { dirsProj.FullPath, projectA.FullPath, projectB.FullPath }, + ignoreOrder: true); + } + } +} \ No newline at end of file diff --git a/src/SlnGen.Build.Tasks.UnitTests/SlnGen.Build.Tasks.UnitTests.csproj b/src/SlnGen.Build.Tasks.UnitTests/SlnGen.Build.Tasks.UnitTests.csproj index b2de5ae8..1d4eaac0 100644 --- a/src/SlnGen.Build.Tasks.UnitTests/SlnGen.Build.Tasks.UnitTests.csproj +++ b/src/SlnGen.Build.Tasks.UnitTests/SlnGen.Build.Tasks.UnitTests.csproj @@ -5,10 +5,9 @@ - + - diff --git a/src/SlnGen.Build.Tasks.UnitTests/TestBase.cs b/src/SlnGen.Build.Tasks.UnitTests/TestBase.cs index fc9b3900..6d510902 100644 --- a/src/SlnGen.Build.Tasks.UnitTests/TestBase.cs +++ b/src/SlnGen.Build.Tasks.UnitTests/TestBase.cs @@ -2,23 +2,16 @@ // // Licensed under the MIT license. -using Microsoft.Build.Locator; +using Microsoft.Build.Utilities.ProjectCreation; using System; using System.IO; namespace SlnGen.Build.Tasks.UnitTests { - public abstract class TestBase + public abstract class TestBase : MSBuildTestBase { - public static readonly VisualStudioInstance CurrentVisualStudioInstance = MSBuildLocator.RegisterDefaults(); - private readonly string _testRootPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - protected TestBase() - { - MSBuildPath = CurrentVisualStudioInstance.MSBuildPath; - } - public string TestRootPath { get diff --git a/src/SlnGen.Build.Tasks/ExtensionMethods.cs b/src/SlnGen.Build.Tasks/ExtensionMethods.cs index 458c5116..8910d66f 100644 --- a/src/SlnGen.Build.Tasks/ExtensionMethods.cs +++ b/src/SlnGen.Build.Tasks/ExtensionMethods.cs @@ -134,7 +134,7 @@ public static string ToFullPathInCorrectCase(this string str) if (!File.Exists(fullPath)) { - throw new FileNotFoundException($"Could not find part of the path \"{fullPath}\""); + return str; } string filename = Path.GetFileName(fullPath); diff --git a/src/SlnGen.Build.Tasks/Internal/MSBuildProjectLoader.cs b/src/SlnGen.Build.Tasks/Internal/MSBuildProjectLoader.cs index 7e7fbc4a..ac441f4f 100644 --- a/src/SlnGen.Build.Tasks/Internal/MSBuildProjectLoader.cs +++ b/src/SlnGen.Build.Tasks/Internal/MSBuildProjectLoader.cs @@ -23,6 +23,11 @@ internal sealed class MSBuildProjectLoader /// private const string ProjectReferenceItemName = "ProjectReference"; + /// + /// The name of the environment variable that configures MSBuild to ignore eager wildcard evaluations (like \**) + /// + private const string MSBuildSkipEagerWildcardEvaluationsEnvironmentVariableName = "MSBUILDSKIPEAGERWILDCARDEVALUATIONREGEXES"; + private readonly IBuildEngine _buildEngine; /// @@ -84,16 +89,30 @@ public MSBuildProjectLoader(IDictionary globalProperties, string /// A object containing the loaded projects. public ProjectCollection LoadProjectsAndReferences(IEnumerable projectPaths) { - // Create a ProjectCollection for this thread - ProjectCollection projectCollection = new ProjectCollection(_globalProperties) + // Store the current value of the environment variable that disables eager wildcard evaluations + string currentSkipEagerWildcardEvaluationsValue = Environment.GetEnvironmentVariable(MSBuildSkipEagerWildcardEvaluationsEnvironmentVariableName); + + try { - DefaultToolsVersion = _toolsVersion, - DisableMarkDirty = true, // Not sure but hoping this improves load performance - }; + // Indicate to MSBuild that any item that has two wildcards should be evaluated lazily + Environment.SetEnvironmentVariable(MSBuildSkipEagerWildcardEvaluationsEnvironmentVariableName, @"\*{2}"); - Parallel.ForEach(projectPaths, projectPath => { LoadProject(projectPath, projectCollection, _projectLoadSettings); }); + // Create a ProjectCollection for this thread + ProjectCollection projectCollection = new ProjectCollection(_globalProperties) + { + DefaultToolsVersion = _toolsVersion, + DisableMarkDirty = true, // Not sure but hoping this improves load performance + }; - return projectCollection; + Parallel.ForEach(projectPaths, projectPath => { LoadProject(projectPath, projectCollection, _projectLoadSettings); }); + + return projectCollection; + } + finally + { + // Restore the environment variable value + Environment.SetEnvironmentVariable(MSBuildSkipEagerWildcardEvaluationsEnvironmentVariableName, currentSkipEagerWildcardEvaluationsValue); + } } /// @@ -186,12 +205,12 @@ private bool TryLoadProject(string path, string toolsVersion, ProjectCollection _buildEngine.LogErrorEvent(new BuildErrorEventArgs( subcategory: null, code: null, - file: null, + file: path, lineNumber: 0, columnNumber: 0, endLineNumber: 0, endColumnNumber: 0, - message: $"Error loading project '{path}'. {e.Message}", + message: e.ToString(), helpKeyword: null, senderName: null));