diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index ac62fb7e2d..0864a31225 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -36,6 +36,7 @@ + diff --git a/src/Stryker.Abstractions/Options/IStrykerOptions.cs b/src/Stryker.Abstractions/Options/IStrykerOptions.cs index 7c7f21b66d..1095e154be 100644 --- a/src/Stryker.Abstractions/Options/IStrykerOptions.cs +++ b/src/Stryker.Abstractions/Options/IStrykerOptions.cs @@ -32,6 +32,7 @@ public interface IStrykerOptions MutationLevel MutationLevel { get; init; } OptimizationModes OptimizationMode { get; init; } string OutputPath { get; init; } + string Platform { get; } string ProjectName { get; set; } string ProjectPath { get; init; } string ProjectVersion { get; set; } diff --git a/src/Stryker.CLI/Stryker.CLI.UnitTest/CommandLineConfigReaderTests.cs b/src/Stryker.CLI/Stryker.CLI.UnitTest/CommandLineConfigReaderTests.cs index cfc6927193..4ace7574ec 100644 --- a/src/Stryker.CLI/Stryker.CLI.UnitTest/CommandLineConfigReaderTests.cs +++ b/src/Stryker.CLI/Stryker.CLI.UnitTest/CommandLineConfigReaderTests.cs @@ -41,7 +41,7 @@ public void ShouldHandleSingleValue() [TestMethod] public void ShouldHandleSingleOrNoValueWithNoValue() { - _target.ReadCommandLineConfig(new[] { "--since" }, _app, _inputs); + _target.ReadCommandLineConfig(["--since"], _app, _inputs); _inputs.SinceInput.SuppliedInput.ShouldBe(true); _inputs.SinceTargetInput.SuppliedInput.ShouldBe(null); @@ -50,7 +50,7 @@ public void ShouldHandleSingleOrNoValueWithNoValue() [TestMethod] public void ShouldHandleSingleOrNoValueWithValue() { - _target.ReadCommandLineConfig(new[] { "--since:test" }, _app, _inputs); + _target.ReadCommandLineConfig(["--since:test"], _app, _inputs); _inputs.SinceInput.SuppliedInput.ShouldBe(true); _inputs.SinceTargetInput.SuppliedInput.ShouldBe("test"); @@ -59,7 +59,7 @@ public void ShouldHandleSingleOrNoValueWithValue() [TestMethod] public void ShouldHandleMultiValue() { - _target.ReadCommandLineConfig(new[] { "--reporter test", "--reporter test2" }, _app, _inputs); + _target.ReadCommandLineConfig(["--reporter test", "--reporter test2"], _app, _inputs); _inputs.ReportersInput.SuppliedInput.Count().ShouldBe(2); _inputs.ReportersInput.SuppliedInput.First().ShouldBe("test"); diff --git a/src/Stryker.CLI/Stryker.CLI.UnitTest/packages.lock.json b/src/Stryker.CLI/Stryker.CLI.UnitTest/packages.lock.json index 4da7debf91..9c38a097d5 100644 --- a/src/Stryker.CLI/Stryker.CLI.UnitTest/packages.lock.json +++ b/src/Stryker.CLI/Stryker.CLI.UnitTest/packages.lock.json @@ -878,6 +878,7 @@ "Stryker.DataCollector": "[1.0.0, )", "Stryker.Regex.Parser": "[1.0.0, )", "Stryker.RegexMutators": "[1.0.0, )", + "Stryker.Solutions": "[1.0.0, )", "Stryker.TestRunner.VsTest": "[1.0.0, )", "Stryker.Utilities": "[1.0.0, )", "TestableIO.System.IO.Abstractions.Wrappers": "[22.1.0, )" @@ -919,6 +920,12 @@ "Stryker.Regex.Parser": "[1.0.0, )" } }, + "stryker.solutions": { + "type": "Project", + "dependencies": { + "Microsoft.VisualStudio.SolutionPersistence": "[1.0.52, )" + } + }, "stryker.testrunner": { "type": "Project", "dependencies": { @@ -1102,6 +1109,12 @@ "NETStandard.Library": "2.0.0" } }, + "Microsoft.VisualStudio.SolutionPersistence": { + "type": "CentralTransitive", + "requested": "[1.0.52, )", + "resolved": "1.0.52", + "contentHash": "oNv2JtYXhpdJrX63nibx1JT3uCESOBQ1LAk7Dtz/sr0+laW0KRM6eKp4CZ3MHDR2siIkKsY8MmUkeP5DKkQQ5w==" + }, "Microsoft.Web.LibraryManager.Build": { "type": "CentralTransitive", "requested": "[3.0.71, )", diff --git a/src/Stryker.CLI/Stryker.CLI/packages.lock.json b/src/Stryker.CLI/Stryker.CLI/packages.lock.json index 21a75afd8c..6d060f3857 100644 --- a/src/Stryker.CLI/Stryker.CLI/packages.lock.json +++ b/src/Stryker.CLI/Stryker.CLI/packages.lock.json @@ -666,6 +666,7 @@ "Stryker.DataCollector": "[1.0.0, )", "Stryker.Regex.Parser": "[1.0.0, )", "Stryker.RegexMutators": "[1.0.0, )", + "Stryker.Solutions": "[1.0.0, )", "Stryker.TestRunner.VsTest": "[1.0.0, )", "Stryker.Utilities": "[1.0.0, )", "TestableIO.System.IO.Abstractions.Wrappers": "[22.1.0, )" @@ -707,6 +708,12 @@ "Stryker.Regex.Parser": "[1.0.0, )" } }, + "stryker.solutions": { + "type": "Project", + "dependencies": { + "Microsoft.VisualStudio.SolutionPersistence": "[1.0.52, )" + } + }, "stryker.testrunner": { "type": "Project", "dependencies": { @@ -872,6 +879,12 @@ "NETStandard.Library": "2.0.0" } }, + "Microsoft.VisualStudio.SolutionPersistence": { + "type": "CentralTransitive", + "requested": "[1.0.52, )", + "resolved": "1.0.52", + "contentHash": "oNv2JtYXhpdJrX63nibx1JT3uCESOBQ1LAk7Dtz/sr0+laW0KRM6eKp4CZ3MHDR2siIkKsY8MmUkeP5DKkQQ5w==" + }, "Microsoft.Web.LibraryManager.Build": { "type": "CentralTransitive", "requested": "[3.0.71, )", diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/BuildAnalyzerTestsBase.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/BuildAnalyzerTestsBase.cs index b5b9ab37ed..ce1faa5320 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/BuildAnalyzerTestsBase.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/BuildAnalyzerTestsBase.cs @@ -7,15 +7,16 @@ using Buildalyzer.Construction; using Buildalyzer.Environment; using Moq; +using Stryker.Solutions; using Stryker.Utilities.Buildalyzer; namespace Stryker.Core.UnitTest.Initialisation; -public class BuildAnalyzerTestsBase : TestBase +public class BuildAnalyzerTestsBase : TestBase, ISolutionProvider { - protected internal const string DefaultFramework = "net6.0"; + protected const string DefaultFramework = "net6.0"; protected readonly MockFileSystem FileSystem = new(); - protected string ProjectPath; + protected readonly string ProjectPath; private readonly Dictionary> _projectCache = []; protected readonly Mock BuildalyzerProviderMock = new(MockBehavior.Strict); @@ -27,6 +28,7 @@ public BuildAnalyzerTestsBase() ProjectPath = FileSystem.Path.Combine(filesystemRoot, "sourceproject"); } + /// /// Build a simple production project /// @@ -288,6 +290,7 @@ protected Mock BuildBuildAnalyzerMock(Dictionary x.Projects) .Returns(analyzerResults); buildalyzerAnalyzerManagerMock.Setup(x => x.SetGlobalProperty(It.IsAny(), It.IsAny())); + buildalyzerAnalyzerManagerMock.Setup(x => x.RemoveGlobalProperty(It.IsAny())); buildalyzerAnalyzerManagerMock.Setup(x => x.SolutionFilePath).Returns((string)null); foreach (var analyzerResult in analyzerResults) @@ -296,10 +299,19 @@ protected Mock BuildBuildAnalyzerMock(Dictionary x.GetProject(filename)).Returns(analyzerResult.Value); } - BuildalyzerProviderMock.Setup(x => x.Provide(It.IsAny(), It.IsAny())) + BuildalyzerProviderMock.Setup(x => x.Provide(It.IsAny())) .Returns(buildalyzerAnalyzerManagerMock.Object); BuildalyzerProviderMock.Setup(x => x.Provide(It.IsAny())) .Returns(buildalyzerAnalyzerManagerMock.Object); return buildalyzerAnalyzerManagerMock; } + + public SolutionFile GetSolution(string solutionPath) + { + if (!FileSystem.FileExists(solutionPath)) + { + throw new InvalidOperationException($"Solution file {solutionPath} does not exist in the file system."); + } + return SolutionFile.BuildFromProjectList(_projectCache.Keys.ToList()); + } } diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InitialBuildProcessTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InitialBuildProcessTests.cs index 20aa3c2c54..ed3e856ac0 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InitialBuildProcessTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InitialBuildProcessTests.cs @@ -44,8 +44,8 @@ public void InitialBuildProcess_WithPathAsBuildCommand_ShouldThrowStrykerInputEx var target = new InitialBuildProcess(processMock.Object, _mockFileSystem, TestLoggerFactory.CreateLogger()); - Should.Throw(() => target.InitialBuild(true, null, _cProjectsExampleCsproj, null, null, - @"C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe")) + Should.Throw(() => target.InitialBuild(true, null, _cProjectsExampleCsproj, null, targetFramework: null, + msbuildPath: @"C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe")) .Details.ShouldBe("Initial build of targeted project failed. Please make sure the targeted project is buildable. You can reproduce this error yourself using: \"\"" + @"C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe" + "\" " + Path.GetFileName(_cProjectsExampleCsproj) + "\""); } @@ -60,7 +60,7 @@ public void InitialBuildProcess_WithPathAsBuildCommand_TriesWithMsBuildIfDotnetF var target = new InitialBuildProcess(processMock.Object, _mockFileSystem, TestLoggerFactory.CreateLogger()); - Should.Throw(() => target.InitialBuild(false, null, _cProjectsExampleCsproj, null, null, @"C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe")) + Should.Throw(() => target.InitialBuild(false, null, _cProjectsExampleCsproj, null, targetFramework: null, msbuildPath: @"C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe")) .Details.ShouldBe("Initial build of targeted project failed. Please make sure the targeted project is buildable. You can reproduce this error yourself using: \"\"" + @"C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe" + "\" " + _mockFileSystem.Path.GetFileName(_cProjectsExampleCsproj) + "\""); @@ -123,7 +123,7 @@ public void InitialBuildProcess_ShouldUseCustomMsbuildIfNotNull() var target = new InitialBuildProcess(processMock.Object, mockFileSystem, TestLoggerFactory.CreateLogger()); - target.InitialBuild(true, "/", "./ExampleProject.sln", null, null, CustomMsBuildPath); + target.InitialBuild(true, "/", "./ExampleProject.sln", null, targetFramework: null, msbuildPath: CustomMsBuildPath); processMock.Verify(x => x.Start(It.IsAny(), It.Is(applicationParam => applicationParam == CustomMsBuildPath), It.Is(argumentsParam => argumentsParam.Contains("ExampleProject.sln")), @@ -132,6 +132,46 @@ public void InitialBuildProcess_ShouldUseCustomMsbuildIfNotNull() Times.Once); } + [TestMethod] + public void InitialBuildProcess_ShouldUseProvidedConfiguration() + { + var processMock = new Mock(MockBehavior.Strict); + var mockFileSystem = new MockFileSystem(); + + processMock.SetupProcessMockToReturn(""); + + var target = new InitialBuildProcess(processMock.Object, mockFileSystem, TestLoggerFactory.CreateLogger()); + + target.InitialBuild(false, "/", "./ExampleProject.sln", "TheDebug"); + processMock.Verify(x => x.Start(It.IsAny(), + It.IsAny(), + It.Is(argumentsParam => argumentsParam.Contains("-c TheDebug")), + It.IsAny>>(), + It.IsAny()), + Times.Once); + } + + [TestMethod] + public void InitialBuildProcess_ShouldUseProvidedPlatform() + { + var processMock = new Mock(MockBehavior.Strict); + var mockFileSystem = new MockFileSystem(); + + processMock.SetupProcessMockToReturn(""); + + var target = new InitialBuildProcess(processMock.Object, mockFileSystem, TestLoggerFactory.CreateLogger()); + + target.InitialBuild(false, "/", "./ExampleProject.sln", "TheDebug", "AnyCPU"); + processMock.Verify(x => x.Start(It.IsAny(), + It.IsAny(), + It.Is(argumentsParam => argumentsParam.Contains("-c TheDebug") + && argumentsParam.Contains("--property:Platform=AnyCPU")), + It.IsAny>>(), + It.IsAny()), + Times.Once); + } + + [TestMethod] public void InitialBuildProcess_ShouldRunDotnetBuildIfNotDotnetFramework() { diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InitialisationProcessTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InitialisationProcessTests.cs index 5d0cab341b..a490d82da2 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InitialisationProcessTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InitialisationProcessTests.cs @@ -79,7 +79,9 @@ public void InitialisationProcess_ShouldThrowOnFailedInitialTestRun() }}); inputFileResolverMock.SetupGet(x => x.FileSystem).Returns(new FileSystem()); - initialBuildProcessMock.Setup(x => x.InitialBuild(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), null, It.IsAny())); + initialBuildProcessMock.Setup(x => x.InitialBuild(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), null, It.IsAny())); testRunnerMock.Setup(x => x.GetTests(It.IsAny())).Returns(new TestSet()); testRunnerMock.Setup(x => x.DiscoverTests(It.IsAny())).Returns(true); initialTestProcessMock.Setup(x => x.InitialTest(It.IsAny(), It.IsAny(), It.IsAny())).Throws(new InputException("")); // failing test @@ -114,7 +116,9 @@ public void InitialisationProcess_ShouldThrowIfHalfTestsAreFailing() new[] { new SourceProjectInfo { AnalyzerResult = TestHelper.SetupProjectAnalyzerResult(references: Array.Empty()).Object, TestProjectsInfo = new TestProjectsInfo(new MockFileSystem()) } }); inputFileResolverMock.SetupGet(x => x.FileSystem).Returns(fileSystemMock); - initialBuildProcessMock.Setup(x => x.InitialBuild(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), null, It.IsAny())); + initialBuildProcessMock.Setup(x => x.InitialBuild(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + null,It.IsAny())); var failedTest = "testid"; var ranTests = new TestIdentifierList(failedTest, "othertest"); var testSet = new TestSet(); @@ -164,7 +168,8 @@ public void InitialisationProcess_ShouldThrowOnTestTestIfAskedFor(bool breakOnIn }}); inputFileResolverMock.SetupGet(x => x.FileSystem).Returns(new FileSystem()); - initialBuildProcessMock.Setup(x => x.InitialBuild(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), null, It.IsAny())); + initialBuildProcessMock.Setup(x => x.InitialBuild(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), null, It.IsAny())); var failedTest = "testid"; var ranTests = new TestIdentifierList(failedTest, "othertest", "anothertest"); var testSet = new TestSet(); @@ -214,9 +219,9 @@ public void InitialisationProcess_ShouldRunTestSession() inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())).Returns( new[] { new SourceProjectInfo() { AnalyzerResult = TestHelper.SetupProjectAnalyzerResult(references: Array.Empty()).Object, TestProjectsInfo = new TestProjectsInfo(new MockFileSystem()) } }); - inputFileResolverMock.SetupGet(x => x.FileSystem).Returns(new FileSystem()); - initialBuildProcessMock.Setup(x => x.InitialBuild(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), null, It.IsAny())); + initialBuildProcessMock.Setup(x => x.InitialBuild(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), null, It.IsAny())); var testSet = new TestSet(); testSet.RegisterTest(new TestDescription("id", "name", "test.cs")); testRunnerMock.Setup(x => x.DiscoverTests(It.IsAny())).Returns(true); @@ -271,7 +276,8 @@ public void InitialisationProcess_ShouldThrowOnWhenNoTestDetected(string library TestProjectsInfo = new TestProjectsInfo(new MockFileSystem()){TestProjects = new List {new(new MockFileSystem(), testProjectAnalyzerResult)}} }}); - initialBuildProcessMock.Setup(x => x.InitialBuild(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), null, It.IsAny())); + initialBuildProcessMock.Setup(x => x.InitialBuild(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), null, It.IsAny())); testRunnerMock.Setup(x => x.DiscoverTests(It.IsAny())).Returns(false); testRunnerMock.Setup(x => x.GetTests(It.IsAny())).Returns(new TestSet()); initialTestProcessMock.Setup(x => x.InitialTest(It.IsAny(), It.IsAny(), It.IsAny())) @@ -320,7 +326,8 @@ public void InitialisationProcess_ShouldThrowOnWhenNoTestDetectedAndCorrectDepen TestProjectsInfo = new TestProjectsInfo(new MockFileSystem()){TestProjects = new List {new(new MockFileSystem(), testProjectAnalyzerResult)}} }}); - initialBuildProcessMock.Setup(x => x.InitialBuild(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), null, It.IsAny())); + initialBuildProcessMock.Setup(x => x.InitialBuild(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), null, It.IsAny())); testRunnerMock.Setup(x => x.DiscoverTests(It.IsAny())).Returns(false); testRunnerMock.Setup(x => x.GetTests(It.IsAny())).Returns(new TestSet()); initialTestProcessMock.Setup(x => x.InitialTest(It.IsAny(), It.IsAny(), It.IsAny())) diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InputFileResolverTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InputFileResolverTests.cs index 0f53e05ecb..02a1bba7ea 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InputFileResolverTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InputFileResolverTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Reflection; @@ -19,11 +20,13 @@ using Stryker.Core.ProjectComponents; using Stryker.Core.ProjectComponents.Csharp; using Stryker.Core.ProjectComponents.TestProjects; +using Stryker.Solutions; using Stryker.Utilities; using static NuGet.Frameworks.FrameworkConstants; namespace Stryker.Core.UnitTest.Initialisation; + [TestClass] public class InputFileResolverTests : BuildAnalyzerTestsBase { @@ -77,9 +80,14 @@ public InputFileResolverTests() _nugetMock = new Mock(); _nugetMock.Setup(x => x.RestorePackages(It.IsAny(), It.IsAny())); - } + private InputFileResolver BuildTestResolver(IFileSystem fileSystem) => BuildTestResolverWithSolutionProvider(fileSystem, + this); + + private InputFileResolver BuildTestResolverWithSolutionProvider(IFileSystem fileSystem, ISolutionProvider solutionProvider) + => new(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, solutionProvider ,TestLoggerFactory.CreateLogger()); + [TestMethod] [DataRow("netcoreapp2.1", FrameworkIdentifiers.NetCoreApp, "", 2, 1, 0, 0)] [DataRow("netstandard1.6", FrameworkIdentifiers.NetStandard, "", 1, 6, 0, 0)] @@ -145,7 +153,7 @@ public void InitializeShouldFindFilesRecursively() }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var result = target.ResolveSourceProjectInfos(_options).First(); @@ -180,7 +188,7 @@ public void InitializeShouldUseBuildalyzerResult() BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var result = target.ResolveSourceProjectInfos(_options).First(); @@ -218,7 +226,7 @@ public void ShouldUseCustomMsBuildPath() }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var options = new StrykerOptions { MsBuildPath = "\\msbuild.exe",ProjectPath = _testPath }; var result = target.ResolveSourceProjectInfos(options).First(); @@ -249,12 +257,102 @@ public void ShouldHandleFailedAnalysis() }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var action = () => target.ResolveSourceProjectInfos(_options).First(); action.ShouldThrow(); } + [TestMethod] + public void ShouldFailIfSolutionNotFound() + { + var fileSystem = new MockFileSystem(new Dictionary + { + { _sourceProjectPath, new MockFileData(_defaultTestProjectFileContents) }, + { _testProjectPath, new MockFileData(_defaultTestProjectFileContents)}, + { Path.Combine(_sourcePath, "Recursive.cs"), new MockFileData(_sourceFile) }, + { Path.Combine(_sourcePath, "Plain.cs"), new MockFileData(_sourceFile) }, + }); + + var sourceProjectManagerMock = SourceProjectAnalyzerMock(_sourceProjectPath, + fileSystem.AllFiles.Where(s => s.EndsWith(".cs")).ToArray()); + var testProjectManagerMock = TestProjectAnalyzerMock(_testProjectPath, _sourceProjectPath, ["netcoreapp2.1"], success: false); + + var analyzerResults = new Dictionary + { + { "MyProject", sourceProjectManagerMock.Object }, + { "MyProject.UnitTests", testProjectManagerMock.Object } + }; + BuildBuildAnalyzerMock(analyzerResults); + var options = new StrykerOptions() + { + ProjectPath = _sourcePath, + SolutionPath = Path.Combine(_sourcePath, "solution.nope") + }; + var target = BuildTestResolver(fileSystem); + + var action = () => target.ResolveSourceProjectInfos(options).First(); + action.ShouldThrow(); + } + + private class CustomSolutionProvider(Func loader) : ISolutionProvider + { + public SolutionFile GetSolution(string solutionPath) => loader(solutionPath); + } + + [TestMethod] + public void ShouldFailIfSolutionLoadFails() + { + var fileSystem = new MockFileSystem(new Dictionary()); + + var sourceProjectManagerMock = SourceProjectAnalyzerMock(_sourceProjectPath, + fileSystem.AllFiles.Where(s => s.EndsWith(".cs")).ToArray()); + var testProjectManagerMock = TestProjectAnalyzerMock(_testProjectPath, _sourceProjectPath, ["netcoreapp2.1"], success: false); + + var analyzerResults = new Dictionary + { + { "MyProject", sourceProjectManagerMock.Object }, + { "MyProject.UnitTests", testProjectManagerMock.Object } + }; + BuildBuildAnalyzerMock(analyzerResults); + var options = new StrykerOptions() + { + ProjectPath = _sourcePath, + SolutionPath = Path.Combine(_sourcePath, "solution.sln") + }; + + var target = BuildTestResolverWithSolutionProvider(fileSystem, + new CustomSolutionProvider(_ => throw new IOException("Failed to read solution"))); + + target.ResolveSourceProjectInfos(options).ShouldBeEmpty(); + } + + [TestMethod] + public void ShouldFailIfSolutionCantBeAccessed() + { + var fileSystem = new MockFileSystem(new Dictionary()); + + var sourceProjectManagerMock = SourceProjectAnalyzerMock(_sourceProjectPath, + fileSystem.AllFiles.Where(s => s.EndsWith(".cs")).ToArray()); + var testProjectManagerMock = TestProjectAnalyzerMock(_testProjectPath, _sourceProjectPath, ["netcoreapp2.1"], success: false); + + var analyzerResults = new Dictionary + { + { "MyProject", sourceProjectManagerMock.Object }, + { "MyProject.UnitTests", testProjectManagerMock.Object } + }; + BuildBuildAnalyzerMock(analyzerResults); + var options = new StrykerOptions() + { + ProjectPath = _sourcePath, + SolutionPath = Path.Combine(_sourcePath, "solution.slnx") + }; + var target = BuildTestResolverWithSolutionProvider(fileSystem, + new CustomSolutionProvider(_ => throw new UnauthorizedAccessException("Access forbidden"))); + + target.ResolveSourceProjectInfos(options).ShouldBeEmpty(); + } + [TestMethod] public void InitializeShouldNotSkipXamlFiles() { @@ -278,7 +376,7 @@ public void InitializeShouldNotSkipXamlFiles() }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var result = target.ResolveSourceProjectInfos(_options).First(); @@ -328,7 +426,7 @@ public void InitializeShouldMutateAssemblyInfo() }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var result = target.ResolveSourceProjectInfos(_options).First(); @@ -385,7 +483,7 @@ public void InitializeShouldNotMutateIncompleteAssemblyInfo() }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var result = target.ResolveSourceProjectInfos(_options).First(); @@ -414,7 +512,7 @@ public void InitializeShouldFindSpecifiedTestProjectFile() }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var result = target.ResolveSourceProjectInfos(_options).First(); @@ -471,7 +569,7 @@ public void InitializeShouldResolveImportedProject() }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var result = target.ResolveSourceProjectInfos(_options).First(); @@ -517,7 +615,7 @@ public void InitializeShouldNotResolveImportedPropsFile() }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var result = target.ResolveSourceProjectInfos(_options).First(); @@ -588,8 +686,7 @@ public void InitializeShouldResolveMultipleImportedProjects() }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); - + var target = BuildTestResolver(fileSystem); var result = target.ResolveSourceProjectInfos(_options).First(); result.ProjectContents.GetAllFiles().Count().ShouldBe(4); @@ -636,7 +733,7 @@ public void InitializeShouldThrowOnMissingSharedProject() }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); Should.Throw(() => target.ResolveSourceProjectInfos(_options)); } @@ -688,8 +785,7 @@ public void InitializeShouldResolvePropertiesInSharedProjectImports() }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); - + var target = BuildTestResolver(fileSystem); var result = target.ResolveSourceProjectInfos(_options).First(); var allFiles = result.ProjectContents.GetAllFiles(); @@ -741,7 +837,7 @@ public void InitializeShouldThrowIfImportPropertyCannotBeResolved() }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var exception = Should.Throw(() => target.ResolveSourceProjectInfos(_options)); exception.Message.ShouldBe($"Missing MSBuild property (SharedDir) in project reference (..{FilePathUtils.NormalizePathSeparators("/$(SharedDir)/Example.projitems")}). Please check your project file ({_sourceProjectPath}) and try again."); @@ -772,7 +868,7 @@ public void InitializeShouldIgnoreBinFolder(string folderName) }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var result = target.ResolveSourceProjectInfos(_options).First(); @@ -793,7 +889,7 @@ public void ShouldThrowExceptionOnNullPath() BuildBuildAnalyzerMock(new Dictionary()); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); Should.Throw(() => target.FindTestProject(null)); } @@ -807,7 +903,7 @@ public void ShouldThrowExceptionOnNoProjectFile() BuildBuildAnalyzerMock(new Dictionary()); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); Should.Throw(() => target.FindTestProject(Path.Combine(_sourcePath))); } @@ -821,7 +917,7 @@ public void ShouldThrowStrykerInputExceptionOnTwoProjectFiles_AndNoTestProjectFi { Path.Combine(_sourcePath, "Recursive.cs"), new MockFileData("content")} }); - var target = new InputFileResolver(fileSystem, new Mock().Object, new Mock().Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var errorMessage = $@"Expected exactly one .csproj file, found more than one: @@ -846,7 +942,7 @@ public void ShouldNotThrowExceptionOnTwoProjectFilesInDifferentLocations() { Path.Combine(_sourcePath, "Recursive.cs"), new MockFileData("content")} }); - var target = new InputFileResolver(fileSystem, new Mock().Object, new Mock().Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var actual = target.FindTestProject(Path.Combine(_sourcePath)); @@ -862,7 +958,7 @@ public void ShouldChooseGivenTestProjectFileIfPossible() { Path.Combine(_sourcePath, "TestProject.csproj"), new MockFileData(_defaultTestProjectFileContents)}, { Path.Combine(_sourcePath, "Recursive.cs"), new MockFileData("content")} }); - var target = new InputFileResolver(fileSystem, new Mock().Object, new Mock().Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var actual = target.FindTestProject(Path.Combine(_sourcePath, "TestProject.csproj")); @@ -877,7 +973,7 @@ public void ShouldThrowExceptionIfGivenTestFileDoesNotExist() { Path.Combine(_sourcePath, "AlternateProject.csproj"), new MockFileData(_defaultTestProjectFileContents)}, { Path.Combine(_sourcePath, "Recursive.cs"), new MockFileData("content")} }); - var target = new InputFileResolver(fileSystem, new Mock().Object, new Mock().Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var exception = Should.Throw(() => target.FindTestProject(Path.Combine(_sourcePath, "GivenTestProject.csproj"))); exception.Message.ShouldStartWith("No .csproj or .fsproj file found, please check your project directory at"); @@ -892,7 +988,7 @@ public void ShouldChooseGivenTestProjectFileIfPossible_AtRelativeLocation() { Path.Combine(_sourcePath, "SubFolder", "TestProject.csproj"), new MockFileData(_defaultTestProjectFileContents)}, { Path.Combine(_sourcePath, "Recursive.cs"), new MockFileData("content")} }); - var target = new InputFileResolver(fileSystem, new Mock().Object, new Mock().Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var actual = target.FindTestProject(Path.Combine(_sourcePath, "SubFolder", "TestProject.csproj")); @@ -908,7 +1004,7 @@ public void ShouldChooseGivenTestProjectFileIfPossible_AtFullPath() { Path.Combine(_sourcePath,"SubFolder", "TestProject.csproj"), new MockFileData(_defaultTestProjectFileContents)}, { Path.Combine(_sourcePath, "Recursive.cs"), new MockFileData("content")} }); - var target = new InputFileResolver(fileSystem, new Mock().Object, new Mock().Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var actual = target.FindTestProject(Path.Combine(_filesystemRoot, FilePathUtils.NormalizePathSeparators(Path.Combine(_sourcePath, "SubFolder")))); @@ -952,7 +1048,7 @@ public void ShouldSelectAvailableFramework_WhenDesiredNotFound(string targetFram }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); // Act var result = target.ResolveSourceProjectInfos(options).First(); @@ -968,14 +1064,16 @@ public void ShouldSelectAvailableFramework_WhenDesiredNotFound(string targetFram [DataRow("net3.0,net462", "net461,net2.0", "net3.0", "net3.0", "net2.0")] [DataRow("net3.0,net462", "net461,net2.0", "net461", "net3.0", "net2.0")] [DataRow("net3.0,net462", "net461,net2.0", "net462", "net462", "net461")] - public void ShouldSelectFrameworkBasedOnTestProject(string testFrameworks, string projectFrameworks, string targetFramework, string expectedTestFramework,string expectedFramework) + public void ShouldSelectFrameworkBasedOnTestProject(string testFrameworks, string projectFrameworks + , string targetFramework + , string expectedTestFramework,string expectedFramework) { // Arrange var basePath = Path.Combine(_sourcePath, "ExampleProject"); var testProjectPath = Path.Combine(_sourcePath, "TestProjectFolder", "TestProject.csproj"); var sourceProjectPath = Path.Combine(_sourcePath, "ExampleProject", "ExampleProject.csproj"); var sourceProjectNameFilter = "ExampleProject.csproj"; - + var fileSystem = new MockFileSystem(new Dictionary { { sourceProjectPath, new MockFileData(_defaultTestProjectFileContents)}, @@ -1002,7 +1100,7 @@ public void ShouldSelectFrameworkBasedOnTestProject(string testFrameworks, strin }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); // Act var result = target.ResolveSourceProjectInfos(options).First(); @@ -1027,7 +1125,7 @@ public void ShouldThrowOnMsTestV1Detected() targetFramework: "netcoreapp2.1", properties: new Dictionary() { { "Language", "C#" } }, projectFilePath: _testProjectPath, - references: new string[] { "Microsoft.VisualStudio.QualityTools.UnitTestFramework" }).Object; + references: ["Microsoft.VisualStudio.QualityTools.UnitTestFramework"]).Object; // Act var ex = Should.Throw(() => new TestProject(fileSystem, testProjectAnalyzerResult)); @@ -1058,7 +1156,7 @@ public void ShouldSkipXamlFiles() }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); // Act var result = target.ResolveSourceProjectInfos(_options).First(); @@ -1103,7 +1201,7 @@ public void ShouldFindAllTestProjects() BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); // Act var result = target.ResolveSourceProjectInfos(options).First(); @@ -1131,7 +1229,7 @@ public void ShouldFindSourceProjectWhenSingleProjectReferenceAndNoFilter() }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var result = target.ResolveSourceProjectInfos(_options).First(); @@ -1154,7 +1252,7 @@ public void ShouldThrowOnNoProjectReference() }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var action = () => target.ResolveSourceProjectInfos(_options); @@ -1186,7 +1284,7 @@ public void ShouldThrowOnMultipleProjectsWithoutFilter() }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); // Act var result = () => target.ResolveSourceProjectInfos(_options); @@ -1226,7 +1324,7 @@ public void ShouldNotThrowIfMultipleProjectButOneIsAlwaysReferenced() }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var options = new StrykerOptions { TestProjects = [_testProjectPath, test2Path], @@ -1269,7 +1367,7 @@ public void ShouldMatchFromMultipleProjectByName(string shouldMatch) }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var options = new StrykerOptions { SourceProjectName = shouldMatch, ProjectPath = _testPath }; var result = target.ResolveSourceProjectInfos(options).First(); @@ -1306,7 +1404,7 @@ public void ShouldThrowWhenTheNameMatchesMore(string shouldMatchMoreThanOne) }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var options = new StrykerOptions { SourceProjectName = shouldMatchMoreThanOne, ProjectPath = _testPath }; var result = () => target.ResolveSourceProjectInfos(options); @@ -1333,7 +1431,7 @@ public void ShouldThrowWhenTheNameMatchesNone() }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var options = new StrykerOptions { SourceProjectName = "wrong.csprj", ProjectPath = _testPath }; var result = () => target.ResolveSourceProjectInfos(options); @@ -1361,7 +1459,7 @@ public void ShouldFallbackToProjectReferenceIfDependencyNotFound() }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var result = target.ResolveSourceProjectInfos(_options).First(); @@ -1390,11 +1488,13 @@ public void ShouldMatchOnBothForwardAndBackwardsSlash(string shouldMatch) }; BuildBuildAnalyzerMock(analyzerResults); - var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object, TestLoggerFactory.CreateLogger()); + var target = BuildTestResolver(fileSystem); var options = new StrykerOptions { SourceProjectName = shouldMatch, ProjectPath = _testPath }; var result = target.ResolveSourceProjectInfos(_options).First(); result.AnalyzerResult.ProjectFilePath.ShouldBe(_sourceProjectPath); } + + public SolutionFile GetSolution(string solutionPath) => throw new NotImplementedException(); } diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/ProjectOrchestratorTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/ProjectOrchestratorTests.cs index 356b10ff46..6457cc713e 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/ProjectOrchestratorTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/ProjectOrchestratorTests.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions.TestingHelpers; using System.Linq; using Buildalyzer; using Microsoft.CodeAnalysis; @@ -132,7 +134,7 @@ public void ShouldPassWhenProjectNameIsGiven() } [TestMethod] - public void ShouldUsedDesiredConfigurationWhenDefined() + public void ShouldUseDesiredConfigurationWhenDefined() { // arrange // when a solutionPath is given, and it's inside the current directory (basePath) @@ -146,7 +148,7 @@ public void ShouldUsedDesiredConfigurationWhenDefined() }; var csPathName = FileSystem.Path.Combine(ProjectPath, "someFile.cs"); - var sourceProjectAnalyzerMock = SourceProjectAnalyzerMock(csprojPathName, new[] { csPathName }).Object; + var sourceProjectAnalyzerMock = SourceProjectAnalyzerMock(csprojPathName, [csPathName]).Object; var testProjectAnalyzerMock = TestProjectAnalyzerMock(testCsprojPathName, csprojPathName).Object; // The analyzer finds two projects var analyzerResults = new Dictionary @@ -160,7 +162,7 @@ public void ShouldUsedDesiredConfigurationWhenDefined() var result = target.MutateProjects(options, _reporterMock.Object, mockRunner.Object).ToList(); // assert - buildalyzerAnalyzerManagerMock.Verify(x => x.SetGlobalProperty("Configuration", "Release"), Times.Once()); + buildalyzerAnalyzerManagerMock.Verify(x => x.SetGlobalProperty("Configuration", "Release"), Times.AtLeastOnce); } [TestMethod] @@ -214,9 +216,11 @@ public void ShouldRestoreWhenAnalysisFails() var initialTestProcessMock = new Mock(); initialTestProcessMock.Setup(x => x.InitialTest(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new InitialTestRun(new TestRunResult(true), new TimeoutValueCalculator(500))); - var inputFileResolver = new InputFileResolver(FileSystem, BuildalyzerProviderMock.Object, nugetRestoreMock.Object, TestLoggerFactory.CreateLogger()); + var inputFileResolver = new InputFileResolver(FileSystem, BuildalyzerProviderMock.Object, + nugetRestoreMock.Object, this, + TestLoggerFactory.CreateLogger()); var initialisationProcess = new InitialisationProcess(inputFileResolver, initialBuildProcessMock.Object, initialTestProcessMock.Object, TestLoggerFactory.CreateLogger()); - + var serviceProviderMock = new Mock(); var mutationTestExecutorMock = new Mock(); var target = new ProjectOrchestrator(_projectMutatorMock.Object, @@ -270,12 +274,14 @@ public void ShouldFilterInSolutionMode() // when a solutionPath is given and it's inside the current directory (basePath) var testCsprojPathName = FileSystem.Path.Combine(ProjectPath, "testproject.csproj"); var csprojPathName = FileSystem.Path.Combine(ProjectPath, "sourceproject.csproj"); + var solutionPath = FileSystem.Path.Combine(ProjectPath, "MySolution.sln"); + FileSystem.AddFile(solutionPath, new MockFileData("empty")); var options = new StrykerOptions { ProjectPath = FileSystem.Path.GetFullPath(ProjectPath), // provide an invalid source project name which should normally fail SourceProjectName = "sourceprojec.csproj", - SolutionPath = FileSystem.Path.Combine(ProjectPath, "MySolution.sln") + SolutionPath = solutionPath }; var csPathName = FileSystem.Path.Combine(ProjectPath, "someFile.cs"); @@ -297,13 +303,16 @@ public void ShouldDiscoverUpstreamProject() // arrange var testCsprojPathName = FileSystem.Path.Combine(ProjectPath, "testproject.csproj"); var csprojPathName = FileSystem.Path.Combine(ProjectPath, "sourceproject.csproj"); + var solutionPath = FileSystem.Path.Combine(ProjectPath, "MySolution.sln"); + FileSystem.AddFile(solutionPath, new MockFileData("empty")); var options = new StrykerOptions { ProjectPath = ProjectPath, - SolutionPath = FileSystem.Path.Combine(ProjectPath, "MySolution.sln") + SolutionPath = solutionPath }; var libraryProject = FileSystem.Path.Combine(ProjectPath, "libraryproject.csproj"); + // The analyzer finds two projects var libraryAnalyzer = SourceProjectAnalyzerMock(libraryProject, [FileSystem.Path.Combine(ProjectPath, "mylib.cs")]).Object; var projectAnalyzer = SourceProjectAnalyzerMock(csprojPathName, [FileSystem.Path.Combine(ProjectPath, "someFile.cs")], [libraryProject]).Object; @@ -330,10 +339,12 @@ public void ShouldDiscoverUpstreamProjectWithInvalidCasing() // arrange var testCsprojPathName = FileSystem.Path.Combine(ProjectPath, "testproject.csproj"); var csprojPathName = FileSystem.Path.Combine(ProjectPath, "sourceproject.csproj"); + var solutionPath = FileSystem.Path.Combine(ProjectPath, "MySolution.sln"); + FileSystem.AddFile(solutionPath, new MockFileData("empty")); var options = new StrykerOptions { ProjectPath = ProjectPath, - SolutionPath = FileSystem.Path.Combine(ProjectPath, "MySolution.sln") + SolutionPath = solutionPath }; var libraryProject = FileSystem.Path.Combine(ProjectPath, "libraryproject.csproj"); @@ -364,10 +375,12 @@ public void ShouldDisregardInvalidReferences() // arrange var testCsprojPathName = FileSystem.Path.Combine(ProjectPath, "testproject.csproj"); var csprojPathName = FileSystem.Path.Combine(ProjectPath, "sourceproject.csproj"); + var solutionPath = FileSystem.Path.Combine(ProjectPath, "MySolution.sln"); + FileSystem.AddFile(solutionPath, new MockFileData("empty")); var options = new StrykerOptions { ProjectPath = ProjectPath, - SolutionPath = FileSystem.Path.Combine(ProjectPath, "MySolution.sln") + SolutionPath = solutionPath }; var libraryProject = FileSystem.Path.Combine(ProjectPath, "libraryproject.csproj"); @@ -464,9 +477,12 @@ private ProjectOrchestrator BuildProjectOrchestrator(Dictionary(); initialTestProcessMock.Setup(x => x.InitialTest(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new InitialTestRun(new TestRunResult(true), new TimeoutValueCalculator(500))); - var inputFileResolver = new InputFileResolver(FileSystem, BuildalyzerProviderMock.Object, new Mock().Object, TestLoggerFactory.CreateLogger()); + var inputFileResolver = new InputFileResolver(FileSystem, BuildalyzerProviderMock.Object, + new Mock().Object, + this, + TestLoggerFactory.CreateLogger()); var initialisationProcess = new InitialisationProcess(inputFileResolver, initialBuildProcessMock.Object, initialTestProcessMock.Object, TestLoggerFactory.CreateLogger()); - + var serviceProviderMock = new Mock(); var mutationTestExecutorMock = new Mock(); return new ProjectOrchestrator(_projectMutatorMock.Object, diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Options/Inputs/ConfigurationInputTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Options/Inputs/ConfigurationInputTests.cs index 0fe60a063e..69d38e099d 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Options/Inputs/ConfigurationInputTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Options/Inputs/ConfigurationInputTests.cs @@ -12,7 +12,7 @@ public class ConfigurationInputTests : TestBase public void ShouldHaveHelpText() { var target = new ConfigurationInput(); - target.HelpText.ShouldBe("Configuration to use when building the project(s)."); + target.HelpText.ShouldBe("Configuration to use when building the project(s) (e.g., 'Debug' or 'Release'). If not specified, the default configuration of the project(s) will be used."); } [TestMethod] @@ -34,14 +34,4 @@ public void ShouldReturnDefault() result.ShouldBeNull(); } - - [TestMethod] - public void ShouldThrowOnEmptyInput() - { - var target = new ConfigurationInput { SuppliedInput = " " }; - - var ex = Should.Throw(() => target.Validate()); - - ex.Message.ShouldBe("Please provide a non whitespace only configuration."); - } } diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Options/StrykerInputsTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Options/StrykerInputsTests.cs index 3d2e7a5adf..78f1e8be3d 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Options/StrykerInputsTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Options/StrykerInputsTests.cs @@ -63,6 +63,32 @@ public void PerTestInIsolationShouldSetOptimizationFlags() result.OptimizationMode.HasFlag(OptimizationModes.CaptureCoveragePerTest).ShouldBeTrue(); } + [TestMethod] + public void ShouldSetConfiguration() + { + _target.ConfigurationInput.SuppliedInput = "TheRelease"; + var result = _target.ValidateAll(); + result.Configuration.ShouldBe("TheRelease"); + } + + [TestMethod] + public void ShouldSetConfigurationAndPlatform() + { + _target.ConfigurationInput.SuppliedInput = "TheRelease|x64"; + var result = _target.ValidateAll(); + result.Configuration.ShouldBe("TheRelease"); + result.Platform.ShouldBe("x64"); + } + + [TestMethod] + public void ShouldIgnoreExtraInfoInConfiguration() + { + _target.ConfigurationInput.SuppliedInput = "TheRelease|x64|Disregarded"; + var result = _target.ValidateAll(); + result.Configuration.ShouldBe("TheRelease"); + result.Platform.ShouldBe("x64"); + } + [TestMethod] public void DisableBailShouldSetOptimizationFlags() { diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Stryker.Core.UnitTest.csproj b/src/Stryker.Core/Stryker.Core.UnitTest/Stryker.Core.UnitTest.csproj index 609aedb5a4..a347b7e606 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Stryker.Core.UnitTest.csproj +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Stryker.Core.UnitTest.csproj @@ -51,6 +51,7 @@ + diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/TestHelper.cs b/src/Stryker.Core/Stryker.Core.UnitTest/TestHelper.cs index 3b25eac836..05e9bb0239 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/TestHelper.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/TestHelper.cs @@ -31,6 +31,7 @@ public static Mock SetupProjectAnalyzerResult(Dictionary(); analyzerResultMock.Setup(x => x.Properties).Returns(properties); } + if (projectFilePath != null) { analyzerResultMock.Setup(x => x.ProjectFilePath).Returns(projectFilePath); @@ -75,4 +76,5 @@ public static Mock SetupProjectAnalyzerResult(Dictionary fullOptions = string.IsNullOrEmpty(command) ? [QuotesIfNeeded(projectFile)] : [command, QuotesIfNeeded(projectFile)]; if (!string.IsNullOrEmpty(configuration)) { - fullOptions.Add(usingMsBuild ? $"/property:Configuration={QuotesIfNeeded(configuration)}" : $"-c {QuotesIfNeeded(configuration)}"); + fullOptions.Add($"{(usingMsBuild ? "/property:Configuration=" : "-c ") + QuotesIfNeeded(configuration)}"); + } + + if (!string.IsNullOrEmpty(platform)) + { + fullOptions.Add($"{(usingMsBuild ? "/" : "--")}property:Platform={QuotesIfNeeded(platform)}"); } if (options is not null) diff --git a/src/Stryker.Core/Stryker.Core/Infrastructure/ServiceCollectionExtensions.cs b/src/Stryker.Core/Stryker.Core/Infrastructure/ServiceCollectionExtensions.cs index 59288d917c..f47d68e50f 100644 --- a/src/Stryker.Core/Stryker.Core/Infrastructure/ServiceCollectionExtensions.cs +++ b/src/Stryker.Core/Stryker.Core/Infrastructure/ServiceCollectionExtensions.cs @@ -7,6 +7,7 @@ using Stryker.Core.Initialisation; using Stryker.Core.MutationTest; using Stryker.Core.Reporters; +using Stryker.Solutions; using Stryker.TestRunner.VsTest; using Stryker.Utilities.Buildalyzer; @@ -33,6 +34,8 @@ public static IServiceCollection AddStrykerCore(this IServiceCollection services // Initialisation services - Transient as they perform per-run operations services.AddTransient(); services.AddTransient(); + + services.AddSingleton(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InitialBuildProcess.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InitialBuildProcess.cs index 6ad776765f..5387773128 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InitialBuildProcess.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InitialBuildProcess.cs @@ -11,8 +11,13 @@ namespace Stryker.Core.Initialisation; public interface IInitialBuildProcess { - void InitialBuild(bool fullFramework, string projectPath, string solutionPath, string configuration = null, - string targetFramework = null, string msbuildPath = null); + void InitialBuild(bool fullFramework, + string projectPath, + string solutionPath, + string configuration = null, + string platform = null, + string targetFramework = null, + string msbuildPath = null); } public class InitialBuildProcess : IInitialBuildProcess @@ -30,8 +35,9 @@ public InitialBuildProcess( _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - - public void InitialBuild(bool fullFramework, string projectPath, string solutionPath, string configuration = null, string targetFramework = null, + + public void InitialBuild(bool fullFramework, string projectPath, string solutionPath, string configuration = null, + string platform = null, string targetFramework = null, string msbuildPath = null) { if (fullFramework && string.IsNullOrEmpty(solutionPath)) @@ -50,6 +56,7 @@ public void InitialBuild(bool fullFramework, string projectPath, string solution buildPath, fullFramework, configuration: configuration, + platform: platform, forcedFramework: targetFramework); if (result.ExitCode != ExitCodes.Success && !string.IsNullOrEmpty(solutionPath)) @@ -61,7 +68,7 @@ public void InitialBuild(bool fullFramework, string projectPath, string solution buildPath, true, configuration, - "-t:restore -p:RestorePackagesConfig=true", forcedFramework: targetFramework); + options: "-t:restore -p:RestorePackagesConfig=true", forcedFramework: targetFramework); if (result.ExitCode != ExitCodes.Success) { @@ -71,8 +78,8 @@ public void InitialBuild(bool fullFramework, string projectPath, string solution (result, exe, args) = msBuildHelper.BuildProject(directoryName, buildPath, true, - configuration - , forcedFramework: targetFramework); + configuration, + forcedFramework: targetFramework); } CheckBuildResult(result, target, exe, args); diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InitialisationProcess.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InitialisationProcess.cs index b9e071a1cc..c058ccd2f6 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InitialisationProcess.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InitialisationProcess.cs @@ -73,7 +73,8 @@ public void BuildProjects(IStrykerOptions options, IEnumerable /// - Reads .csproj to find project under test /// - Scans project under test and store files to mutate @@ -32,10 +33,11 @@ public interface IInputFileResolver /// public class InputFileResolver : IInputFileResolver { - private readonly string[] _foldersToExclude = { "obj", "bin", "node_modules", "StrykerOutput" }; + private readonly string[] _foldersToExclude = ["obj", "bin", "node_modules", "StrykerOutput"]; private readonly ILogger _logger; private readonly IBuildalyzerProvider _analyzerProvider; - private static readonly HashSet importantProperties = + private readonly ISolutionProvider _solutionProvider; + private static readonly HashSet ImportantProperties = ["Configuration", "Platform", "AssemblyName", "Configurations"]; private readonly INugetRestoreProcess _nugetRestoreProcess; @@ -43,40 +45,82 @@ public class InputFileResolver : IInputFileResolver private readonly StringWriter _buildalyzerLog = new(); public InputFileResolver(IFileSystem fileSystem, - IBuildalyzerProvider analyzerProvider, INugetRestoreProcess nugetRestoreProcess, ILogger logger) + IBuildalyzerProvider analyzerProvider, + INugetRestoreProcess nugetRestoreProcess, + ISolutionProvider solutionProvider, + ILogger logger) { FileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); _analyzerProvider = analyzerProvider ?? throw new ArgumentNullException(nameof(analyzerProvider)); _nugetRestoreProcess = nugetRestoreProcess ?? throw new ArgumentNullException(nameof(nugetRestoreProcess)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _solutionProvider = solutionProvider ?? throw new ArgumentNullException(nameof(solutionProvider)); } public IFileSystem FileSystem { get; } public IReadOnlyCollection ResolveSourceProjectInfos(IStrykerOptions options) { - var manager = _analyzerProvider.Provide(options.SolutionPath, options.DevMode ? new AnalyzerManagerOptions { LogWriter = _buildalyzerLog } : null); + var manager = _analyzerProvider.Provide(options.DevMode ? new AnalyzerManagerOptions { LogWriter = _buildalyzerLog } : null); + + Dictionary> findMutableAnalyzerResults; if (options.IsSolutionContext) { - var projectList = manager.Projects.Values.Select(p => p.ProjectFile.Path).ToList(); + SolutionFile solution; + + try + { + solution = _solutionProvider.GetSolution(options.SolutionPath); + } + catch (IOException e) + { + _logger.LogError(e, "Failed to load solution file {0}.", options.SolutionPath); + return []; + } + catch (UnauthorizedAccessException e) + { + _logger.LogError(e, "Failed to access solution file {0}.", options.SolutionPath); + return []; + } + catch (AggregateException e) // Handles exceptions from .Result on Task + { + _logger.LogError(e, "Failed to load solution file {0}.", options.SolutionPath); + return []; + } _logger.LogInformation("Identifying projects to mutate in {solution}. This can take a while.", options.SolutionPath); - return AnalyzeAndIdentifyProjects(projectList, options, manager, ScanMode.NoScan); + // build all projects + var projectsWithDetails = solution.GetProjectsWithDetails(options.Configuration, options.Platform) + .Select(p => (p.file, options.TargetFramework, p.buildType)).ToList(); + _logger.LogDebug("Analyzing {0} projects.", projectsWithDetails.Count); + // we match test projects to mutable projects + findMutableAnalyzerResults = FindMutableAnalyzerResults(AnalyzeAllNeededProjects(projectsWithDetails, options, manager, ScanMode.NoScan)); + + return findMutableAnalyzerResults.Count != 0 ? AnalyzeAndIdentifyProjects(options, findMutableAnalyzerResults) + : throw new InputException("No project references found. Please add a project reference to your test project and retry."); } // we analyze the test project(s) and identify the project to be mutated - List testProjectFileNames; - if (options.TestProjects != null && options.TestProjects.Any()) - { - testProjectFileNames = options.TestProjects.Select(FindTestProject).ToList(); - } - else + var testProjectFileNames = options.TestProjects.Any() ? options.TestProjects.Select(FindTestProject).ToList() + : [FindTestProject(options.ProjectPath)]; + + _logger.LogInformation("Analyzing {0} test project(s).", testProjectFileNames.Count); + List<(string projectFile, string framework, string configuration)> projectList = + [..testProjectFileNames.Select(p => (p, options.TargetFramework, options.Configuration))]; + // we match test projects to mutable projects + findMutableAnalyzerResults = FindMutableAnalyzerResults(AnalyzeAllNeededProjects(projectList, options, manager, ScanMode.ScanTestProjectReferences)); + + if (findMutableAnalyzerResults.All(p => p.Value.All(r => !r.Succeeded)) ) { - testProjectFileNames = [FindTestProject(options.ProjectPath)]; + var failedProjects = findMutableAnalyzerResults + .Select(p => p.Key.ProjectFilePath) + .Distinct() + .ToList(); + _logger.LogError("Aborting, analysis failed for every projects: {FailedProjects}", string.Join(", ", failedProjects)); + // no mutable project found + throw new InputException("No project references found. Please add a project reference to your test project and retry."); } - - _logger.LogInformation("Analyzing {count} test project(s).", testProjectFileNames.Count); - var result = AnalyzeAndIdentifyProjects(testProjectFileNames, options, manager, ScanMode.ScanTestProjectReferences); + var result = AnalyzeAndIdentifyProjects(options, findMutableAnalyzerResults); if (result.Count <= 1) { return result; @@ -108,62 +152,65 @@ private enum ScanMode ScanTestProjectReferences = 1 // add test project references during scan } - private List AnalyzeAndIdentifyProjects(List projectList, IStrykerOptions options, - IAnalyzerManager manager, ScanMode mode) + private List AnalyzeAndIdentifyProjects(IStrykerOptions options, + Dictionary> findMutableAnalyzerResults) { - // build all projects - if (!string.IsNullOrEmpty(options.Configuration)) - { - manager.SetGlobalProperty("Configuration", options.Configuration); - } - _logger.LogDebug("Analyzing {count} projects.", manager.Projects.Count); - - // we match test projects to mutable projects - var findMutableAnalyzerResults = FindMutableAnalyzerResults(AnalyzeAllNeededProjects(projectList, options, manager, mode)); - if (findMutableAnalyzerResults.Count == 0) { // no mutable project found throw new InputException("No project references found. Please add a project reference to your test project and retry."); } + // we match test projects to mutable projects var analyzerResults = findMutableAnalyzerResults.Keys.GroupBy(p => p.ProjectFilePath).ToList(); var projectInfos = new List(); foreach (var group in analyzerResults) { // we must select projects according to framework settings if any var analyzerResult = SelectAnalyzerResult(group, options.TargetFramework); - projectInfos.Add(BuildSourceProjectInfo(options, analyzerResult, findMutableAnalyzerResults[analyzerResult])); } - if (projectInfos.Count == 0) + if (projectInfos.Count != 0) { - _logger.LogError("Project analysis failed."); - throw new InputException("No valid project analysis results could be found."); + return projectInfos; } - return projectInfos; + + _logger.LogError("Project analysis failed."); + throw new InputException("No valid project analysis results could be found."); } - private ConcurrentBag<(IEnumerable result, bool isTest)> AnalyzeAllNeededProjects(List projectList, IStrykerOptions options, IAnalyzerManager manager, ScanMode mode) + private ConcurrentBag<(IEnumerable result, bool isTest)> AnalyzeAllNeededProjects( + List<(string projectFile, string framework, string configuration)> projects, IStrykerOptions options + , IAnalyzerManager manager, ScanMode mode) { var mutableProjectsAnalyzerResults = new ConcurrentBag<(IEnumerable result, bool isTest)>(); + var list = new DynamicEnumerableQueue<(string projectFile, string framework, string configuration)>(projects); + const string Configuration = "Configuration"; try { - var list = new DynamicEnumerableQueue<(string projectFile, string framework)>(projectList.Select(p => (p, options.TargetFramework))); + var parallelOptions = new ParallelOptions + { MaxDegreeOfParallelism = options.DevMode ? 1 : Math.Max(options.Concurrency, 1) }; var normalizedProjectUnderTestNameFilter = !string.IsNullOrEmpty(options.SourceProjectName) ? options.SourceProjectName.Replace("\\", "/") : null; while (!list.Empty) { Parallel.ForEach(list.Consume(), - new ParallelOptions - { MaxDegreeOfParallelism = options.DevMode ? 1 : Math.Max(options.Concurrency, 1) }, entry => + parallelOptions, entry => { - var buildResult = GetProjectAndAddIt(options, manager, entry, normalizedProjectUnderTestNameFilter, mutableProjectsAnalyzerResults); - - foreach (var reference in ScanReferences(mode, buildResult)) + // specify configuration if any provided + if (!string.IsNullOrEmpty(entry.configuration)) + { + manager.SetGlobalProperty(Configuration, entry.configuration); + } + else { - list.Add((reference, null)); + manager.RemoveGlobalProperty(Configuration); } + var buildResult = AnalyzeThisProject(options, + manager.GetProject(entry.projectFile), entry.framework, normalizedProjectUnderTestNameFilter, + mutableProjectsAnalyzerResults); + // scan references if recursive scan is enabled + ScanReferences(mode, buildResult).ForEach(p => list.Add((p, entry.framework, options.Configuration))); } ); } @@ -176,27 +223,28 @@ private List AnalyzeAndIdentifyProjects(List projectL return mutableProjectsAnalyzerResults; } - private IEnumerable GetProjectAndAddIt(IStrykerOptions options, IAnalyzerManager manager, - (string projectFile, string framework) entry, string normalizedProjectUnderTestNameFilter, + private IEnumerable AnalyzeThisProject(IStrykerOptions options, IProjectAnalyzer project, + string framework, string normalizedProjectUnderTestNameFilter, ConcurrentBag<(IEnumerable result, bool isTest)> mutableProjectsAnalyzerResults) { - var project = manager.GetProject(entry.projectFile); IEnumerable buildResult = AnalyzeSingleProject(project, options); if (!buildResult.Any()) { - // analysis failed return buildResult; } + var isTestProject = buildResult.IsTestProject(); if (isTestProject) { - buildResult = [SelectAnalyzerResult(buildResult, entry.framework)]; + // filter frameworks for test projects (if one is selected) + buildResult = [SelectAnalyzerResult(buildResult, framework)]; } // apply project name filter (except for test projects) if (isTestProject || normalizedProjectUnderTestNameFilter == null || project.ProjectFile.Path.Replace('\\', '/') - .Contains(normalizedProjectUnderTestNameFilter, StringComparison.InvariantCultureIgnoreCase)) + .Contains(normalizedProjectUnderTestNameFilter, + StringComparison.InvariantCultureIgnoreCase)) { mutableProjectsAnalyzerResults.Add((buildResult, isTestProject)); } @@ -213,25 +261,15 @@ private IEnumerable GetProjectAndAddIt(IStrykerOptions options, private List ScanReferences(ScanMode mode, IEnumerable buildResult) { var referencesToAdd = new List(); - var isTestProject = buildResult.IsTestProject(); - if (mode == ScanMode.NoScan) + if (mode == ScanMode.NoScan || (mode == ScanMode.ScanTestProjectReferences && !buildResult.IsTestProject())) { return referencesToAdd; } // Stryker will recursively scan projects - // add any project reference to ease progressive discovery (when not using solution file) - foreach (var projectReference in buildResult.SelectMany(p => p.ProjectReferences)) - { - // in single level mode we only want to find the projects referenced by test project - if ((mode == ScanMode.ScanTestProjectReferences && !isTestProject) || !FileSystem.File.Exists(projectReference)) - { - continue; - } - - referencesToAdd.Add(projectReference); - } + // add any project reference for progressive discovery (when not using solution file) + referencesToAdd.AddRange(buildResult.SelectMany(p => p.ProjectReferences).Where(projectReference => FileSystem.File.Exists(projectReference))); return referencesToAdd; } @@ -243,7 +281,7 @@ private IAnalyzerResults AnalyzeSingleProject(IProjectAnalyzer project, IStryker // clear the logs for the next project _buildalyzerLog.GetStringBuilder().Clear(); } - var projectLogName = Path.GetRelativePath(options.WorkingDirectory, project.ProjectFile.Path); + var projectLogName = FileSystem.Path.GetRelativePath(options.WorkingDirectory, project.ProjectFile.Path); _logger.LogDebug("Analyzing {ProjectFilePath}", projectLogName); @@ -259,7 +297,7 @@ private IAnalyzerResults AnalyzeSingleProject(IProjectAnalyzer project, IStryker var buildResultOverallSuccess = buildResult.OverallSuccess || Array. TrueForAll(project.ProjectFile.TargetFrameworks, tf => - buildResult.Any(br => IsValid(br) && br.TargetFramework == tf)); + buildResult.Any(br => br.IsValidFor(tf))); if (!buildResultOverallSuccess) { @@ -274,26 +312,25 @@ private IAnalyzerResults AnalyzeSingleProject(IProjectAnalyzer project, IStryker return buildResult; } + // log failure details var failedFrameworks = project.ProjectFile.TargetFrameworks.Where(tf => - !buildResult.Any(br => IsValid(br) && br.TargetFramework == tf)).ToList(); + !buildResult.Any(br => br.IsValidFor(tf))).ToList(); _logger.LogWarning( "Analysis of project {projectFilePath} failed for frameworks {frameworkList}.", projectLogName, string.Join(',', failedFrameworks)); if (options.DevMode) { - _logger.LogWarning("Project analysis failed. The MsBuild log is below."); - _logger.LogInformation(_buildalyzerLog.ToString()); + _logger.LogWarning("Project analysis failed. The MsBuild log: {0}",_buildalyzerLog.ToString()); } - // if there is no valid result, drop it altogether - return buildResult.All(br => !IsValid(br)) ? new AnalyzerResults() : buildResult; + return buildResult; } private IAnalyzerResults RetryBuild(IProjectAnalyzer project, IStrykerOptions options, string projectLogName, IAnalyzerResults buildResult, out bool buildResultOverallSuccess) { - if (buildResult.Any(r => !IsValid(r) && r.TargetsFullFramework())) + if (buildResult.Any(r => !r.IsValid() && r.TargetsFullFramework())) { _logger.LogWarning("Project {projectFilePath} analysis failed. Stryker will retry after a nuget restore.", projectLogName); @@ -315,13 +352,13 @@ private IAnalyzerResults RetryBuild(IProjectAnalyzer project, IStrykerOptions op // check the new status buildResultOverallSuccess = Array.TrueForAll(project.ProjectFile.TargetFrameworks, tf => - buildResult.Any(br => IsValid(br) && br.TargetFramework == tf)); + buildResult.Any(br => br.IsValidFor(tf))); if (!buildResultOverallSuccess && !string.IsNullOrEmpty(options.TargetFramework)) { - // still failed, we can try using targeframework option + // still failed, we can try using target framework option buildResult = project.Build(options.TargetFramework); - buildResultOverallSuccess = buildResult.Any( br => IsValid(br) && br.TargetFramework == options.TargetFramework); + buildResultOverallSuccess = buildResult.Any( br => br.IsValidFor(options.TargetFramework)); } return buildResult; @@ -344,8 +381,8 @@ private void LogAnalyzerResult(IAnalyzerResults analyzerResults, IStrykerOptions log.AppendLine($"TargetFramework: {analyzerResult.TargetFramework}"); log.AppendLine($"Succeeded: {analyzerResult.Succeeded}"); - var properties = analyzerResult.Properties ?? new Dictionary(); - foreach (var property in importantProperties) + var properties = analyzerResult.Properties; + foreach (var property in ImportantProperties) { log.AppendLine($"Property {property}={properties.GetValueOrDefault(property) ?? "\"'undefined'\""}"); } @@ -356,14 +393,14 @@ private void LogAnalyzerResult(IAnalyzerResults analyzerResults, IStrykerOptions foreach (var reference in analyzerResult.References) { - log.AppendLine($"References: {Path.GetFileName(reference)} (in {Path.GetDirectoryName(reference)})"); + log.AppendLine($"References: {FileSystem.Path.GetFileName(reference)} (in {FileSystem.Path.GetDirectoryName(reference)})"); } foreach (var property in properties) { - if (importantProperties.Contains(property.Key)) + if (ImportantProperties.Contains(property.Key)) { - continue; // already logged + continue; // already logged } log.AppendLine($"Property {property.Key}={property.Value.Replace(Environment.NewLine, "\\n")}"); @@ -374,7 +411,7 @@ private void LogAnalyzerResult(IAnalyzerResults analyzerResults, IStrykerOptions _logger.LogTrace(log.ToString()); } - public IAnalyzerResult SelectAnalyzerResult(IEnumerable analyzerResults, string targetFramework) + private IAnalyzerResult SelectAnalyzerResult(IEnumerable analyzerResults, string targetFramework) { var validResults = analyzerResults.ToList(); var projectName = analyzerResults.First().ProjectFilePath; @@ -422,9 +459,6 @@ IAnalyzerResult PickFrameworkVersion() } } - // checks if an analyzer result is valid - private static bool IsValid(IAnalyzerResult br) => br.Succeeded || (br.SourceFiles.Length > 0 && br.References.Length > 0); - private Dictionary> FindMutableAnalyzerResults(ConcurrentBag<(IEnumerable result, bool isTest)> mutableProjectsAnalyzerResults) { var mutableToTestMap = new Dictionary>(); @@ -433,12 +467,14 @@ private Dictionary> FindMutableAnalyzerRe // for each test project foreach (var testProject in analyzerTestProjects) { - if (!ScanAssemblyReferences(mutableToTestMap, mutableProjects, testProject)) + if (ScanAssemblyReferences(mutableToTestMap, mutableProjects, testProject)) { - _logger.LogInformation("Could not find an assembly reference to a mutable assembly for project {0}. Will look into project references.", testProject.ProjectFilePath); - // we try to find a project reference - ScanProjectReferences(mutableToTestMap, mutableProjects, testProject); + continue; } + + _logger.LogInformation("Could not find an assembly reference to a mutable assembly for project {0}. Will look into project references.", testProject.ProjectFilePath); + // we try to find a project reference + ScanProjectReferences(mutableToTestMap, mutableProjects, testProject); } return mutableToTestMap; @@ -597,8 +633,8 @@ private sealed class DynamicEnumerableQueue public DynamicEnumerableQueue(IEnumerable init) { - _cache = new(init.ToDictionary(x => x, x => true)); - _queue = new(_cache.Keys); + _cache = new ConcurrentDictionary(init.ToDictionary(x => x, _ => true)); + _queue = new ConcurrentQueue(_cache.Keys); } public bool Empty => _queue.IsEmpty; @@ -614,7 +650,7 @@ public void Add(T entry) public IEnumerable Consume() { - while (_queue.Count > 0) + while (!_queue.IsEmpty) { if (_queue.TryDequeue(out var entry)) { diff --git a/src/Stryker.Core/Stryker.Core/Stryker.Core.csproj b/src/Stryker.Core/Stryker.Core/Stryker.Core.csproj index 4c25ce9dfb..8d41870490 100644 --- a/src/Stryker.Core/Stryker.Core/Stryker.Core.csproj +++ b/src/Stryker.Core/Stryker.Core/Stryker.Core.csproj @@ -98,6 +98,7 @@ + diff --git a/src/Stryker.Core/Stryker.Core/packages.lock.json b/src/Stryker.Core/Stryker.Core/packages.lock.json index 8fca8b1bb6..9286b8fb86 100644 --- a/src/Stryker.Core/Stryker.Core/packages.lock.json +++ b/src/Stryker.Core/Stryker.Core/packages.lock.json @@ -809,6 +809,12 @@ "Stryker.Regex.Parser": "[1.0.0, )" } }, + "stryker.solutions": { + "type": "Project", + "dependencies": { + "Microsoft.VisualStudio.SolutionPersistence": "[1.0.52, )" + } + }, "stryker.testrunner": { "type": "Project", "dependencies": { @@ -862,6 +868,12 @@ "System.Diagnostics.DiagnosticSource": "10.0.0" } }, + "Microsoft.VisualStudio.SolutionPersistence": { + "type": "CentralTransitive", + "requested": "[1.0.52, )", + "resolved": "1.0.52", + "contentHash": "oNv2JtYXhpdJrX63nibx1JT3uCESOBQ1LAk7Dtz/sr0+laW0KRM6eKp4CZ3MHDR2siIkKsY8MmUkeP5DKkQQ5w==" + }, "ResXResourceReader.NetStandard": { "type": "CentralTransitive", "requested": "[1.3.0, )", diff --git a/src/Stryker.Options/Options/Inputs/ConfigurationInput.cs b/src/Stryker.Options/Options/Inputs/ConfigurationInput.cs index f86d2227a1..f76223baec 100644 --- a/src/Stryker.Options/Options/Inputs/ConfigurationInput.cs +++ b/src/Stryker.Options/Options/Inputs/ConfigurationInput.cs @@ -5,7 +5,7 @@ namespace Stryker.Abstractions.Options.Inputs; public class ConfigurationInput : Input { public override string Default => null; - protected override string Description => "Configuration to use when building the project(s)."; + protected override string Description => "Configuration to use when building the project(s) (e.g., 'Debug' or 'Release'). If not specified, the default configuration of the project(s) will be used."; public string Validate() { @@ -13,10 +13,7 @@ public string Validate() { return Default; } - if (string.IsNullOrWhiteSpace(SuppliedInput)) - { - throw new InputException("Please provide a non whitespace only configuration."); - } - return SuppliedInput; + + return SuppliedInput; } } diff --git a/src/Stryker.Options/Options/StrykerOptions.cs b/src/Stryker.Options/Options/StrykerOptions.cs index 9ac02f6476..2dbe0ac7f3 100644 --- a/src/Stryker.Options/Options/StrykerOptions.cs +++ b/src/Stryker.Options/Options/StrykerOptions.cs @@ -64,7 +64,33 @@ public class StrykerOptions : IStrykerOptions /// /// The configuration (in the VS sense) that should be used when building the project under test. /// - public string Configuration { get; init; } + /// it may also contain the platform in the form of | + public string Configuration + { + get => _configuration; + init + { + if (string.IsNullOrWhiteSpace(value)) + { + _configuration = null; + return; + } + var blocks = value.Split('|'); + _configuration = blocks[0]; + if (blocks.Length > 1) + { + Platform = blocks[1]; + } + } + } + + /// + /// The desired platform. + /// + /// + /// Will be null if no platform is specified in . + /// + public string Platform { get; private set; } /// /// The detected target framework for the current project under test. @@ -87,7 +113,7 @@ public class StrykerOptions : IStrykerOptions public IThresholds Thresholds { get; init; } = new Thresholds() { Break = 0, Low = 60, High = 80 }; /// - /// The ammount of milliseconds that should be added to the timeout period when testing mutants. + /// How many milliseconds should be added to the timeout period when testing mutants. /// public int AdditionalTimeout { get; init; } @@ -225,13 +251,11 @@ public class StrykerOptions : IStrykerOptions /// public bool BreakOnInitialTestFailure { get; set; } - /// /// Get/set the mutation id provider /// public IProvideId MutantIdProvider {get; set;} - private readonly string _workingDirectoryField; - + private string _configuration; } diff --git a/src/Stryker.Solutions.Test/MSTestSettings.cs b/src/Stryker.Solutions.Test/MSTestSettings.cs new file mode 100644 index 0000000000..aaf278c844 --- /dev/null +++ b/src/Stryker.Solutions.Test/MSTestSettings.cs @@ -0,0 +1 @@ +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] diff --git a/src/Stryker.Solutions.Test/SolutionFileShould.cs b/src/Stryker.Solutions.Test/SolutionFileShould.cs new file mode 100644 index 0000000000..ff883e2968 --- /dev/null +++ b/src/Stryker.Solutions.Test/SolutionFileShould.cs @@ -0,0 +1,70 @@ +using Shouldly; + +namespace Stryker.Solutions.Test; + +[TestClass] +public sealed class SolutionFileShould +{ + [TestMethod] + public void LoadStrykerSlnFile() + { + // Arrange + // Act + var solution = new SolutionFile().GetSolution(Path.Combine("..","..","..","..","Stryker.sln")); + // Assert + Assert.IsNotNull(solution); + } + + [TestMethod] + public void IdentifyStrykerBuildTypes() + { + // Arrange + // Act + var solution = new SolutionFile().GetSolution(Path.Combine("..","..","..","..","Stryker.sln")); + // Assert + solution.GetBuildTypes().ShouldBe(["Debug", "Release"]); + } + + [TestMethod] + public void IdentifyStrykerPlatform() + { + // Arrange + // Act + var solution = new SolutionFile().GetSolution(Path.Combine("..","..","..","..","Stryker.sln")); + + // Assert + solution.ConfigurationExists("Debug", "Any CPU").ShouldBeTrue(); + + } + + [TestMethod] + public void ProvideProjectListForGivenConfiguration() + { + // Arrange + // Act + var solution = new SolutionFile().GetSolution(Path.Combine("..","..","..","..","Stryker.sln")); + // Assert + solution.ConfigurationExists("Debug", "Any CPU").ShouldBeTrue(); + + // it should report all projects that are built in Stryker's Debug configuration + var expectedProjects = new List + { + Path.Combine("Stryker.CLI", "Stryker.CLI", "Stryker.CLI.csproj"), + Path.Combine("Stryker.CLI", "Stryker.CLI.UnitTest", "Stryker.CLI.UnitTest.csproj"), + Path.Combine("Stryker.Core", "Stryker.Core", "Stryker.Core.csproj"), + Path.Combine("Stryker.Core", "Stryker.Core.UnitTest", "Stryker.Core.UnitTest.csproj"), + Path.Combine("Stryker.DataCollector", "Stryker.DataCollector", "Stryker.DataCollector.csproj"), + Path.Combine("Stryker.RegexMutators", "Stryker.RegexMutators", "Stryker.RegexMutators.csproj"), + Path.Combine("Stryker.RegexMutators", "Stryker.RegexMutators.UnitTest", "Stryker.RegexMutators.UnitTest.csproj"), + Path.Combine("Stryker.Abstractions", "Stryker.Abstractions.csproj"), + Path.Combine("Stryker.Options", "Stryker.Configuration.csproj"), + Path.Combine("Stryker.Utilities", "Stryker.Utilities.csproj"), + Path.Combine("Stryker.TestRunner", "Stryker.TestRunner.csproj"), + Path.Combine("Stryker.TestRunner.VsTest", "Stryker.TestRunner.VsTest.csproj"), + Path.Combine("Stryker.TestRunner.VsTest.UnitTest", "Stryker.TestRunner.VsTest.UnitTest.csproj"), + Path.Combine("Stryker.Solutions", "Stryker.Solutions.csproj"), + Path.Combine("Stryker.Solutions.Test", "Stryker.Solutions.Test.csproj") + }; + solution.GetProjects("Debug").ShouldBe(expectedProjects); + } +} diff --git a/src/Stryker.Solutions.Test/Stryker.Solutions.Test.csproj b/src/Stryker.Solutions.Test/Stryker.Solutions.Test.csproj new file mode 100644 index 0000000000..27fde46587 --- /dev/null +++ b/src/Stryker.Solutions.Test/Stryker.Solutions.Test.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + latest + enable + enable + + + + + + + + + + + + + + + + + diff --git a/src/Stryker.Solutions.Test/packages.lock.json b/src/Stryker.Solutions.Test/packages.lock.json new file mode 100644 index 0000000000..43c2d909dc --- /dev/null +++ b/src/Stryker.Solutions.Test/packages.lock.json @@ -0,0 +1,270 @@ +{ + "version": 2, + "dependencies": { + "net8.0": { + "DotNet.ReproducibleBuilds": { + "type": "Direct", + "requested": "[1.2.39, )", + "resolved": "1.2.39", + "contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg==" + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[18.0.1, )", + "resolved": "18.0.1", + "contentHash": "WNpu6vI2rA0pXY4r7NKxCN16XRWl5uHu6qjuyVLoDo6oYEggIQefrMjkRuibQHm/NslIUNCcKftvoWAN80MSAg==", + "dependencies": { + "Microsoft.CodeCoverage": "18.0.1", + "Microsoft.TestPlatform.TestHost": "18.0.1" + } + }, + "MSTest": { + "type": "Direct", + "requested": "[3.11.1, )", + "resolved": "3.11.1", + "contentHash": "MFRRUASELVVmWDYzO9hScQm7RNWhKYU2rcKbLBJBKywGBtf9mhcLN/ptQ3D6nDijVgh5Rha3IispEercNPuXig==", + "dependencies": { + "MSTest.TestAdapter": "3.11.1", + "MSTest.TestFramework": "3.11.1", + "Microsoft.NET.Test.Sdk": "17.13.0", + "Microsoft.Testing.Extensions.CodeCoverage": "18.0.4", + "Microsoft.Testing.Extensions.TrxReport": "1.9.1" + } + }, + "Shouldly": { + "type": "Direct", + "requested": "[4.3.0, )", + "resolved": "4.3.0", + "contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==", + "dependencies": { + "DiffEngine": "11.3.0", + "EmptyFiles": "4.4.0" + } + }, + "DiffEngine": { + "type": "Transitive", + "resolved": "11.3.0", + "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", + "dependencies": { + "EmptyFiles": "4.4.0", + "System.Management": "6.0.1" + } + }, + "EmptyFiles": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==", + "dependencies": { + "System.Diagnostics.DiagnosticSource": "5.0.0" + } + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "18.0.1", + "contentHash": "O+utSr97NAJowIQT/OVp3Lh9QgW/wALVTP4RG1m2AfFP4IyJmJz0ZBmFJUsRQiAPgq6IRC0t8AAzsiPIsaUDEA==" + }, + "Microsoft.DiaSymReader": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "QcZrCETsBJqy/vQpFtJc+jSXQ0K5sucQ6NUFbTNVHD4vfZZOwjZ/3sBzczkC4DityhD3AVO/+K/+9ioLs1AgRA==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "6.0.2", + "contentHash": "HS5YsudCGSVoCVdsYJ5FAO9vx0z04qSAXgVzpDJSQ1/w/X9q8hrQVGU2p+Yfui+2KcXLL+Zjc0SX3yJWtBmYiw==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Memory": "4.5.4", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encodings.Web": "6.0.1", + "System.Text.Json": "6.0.11" + } + }, + "Microsoft.Testing.Extensions.CodeCoverage": { + "type": "Transitive", + "resolved": "18.0.4", + "contentHash": "g4yWI018dft4DUwUgOFd9IPOezZyZc/IkDuewPBXdGZk+hr+6x1CNkEuYsknDVcB9O8/HK/rQzRR8xAaUAGjhg==", + "dependencies": { + "Microsoft.DiaSymReader": "2.0.0", + "Microsoft.Extensions.DependencyModel": "6.0.2", + "Microsoft.Testing.Platform": "1.8.4" + } + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Testing.Platform": "1.9.1" + } + }, + "Microsoft.Testing.Extensions.TrxReport": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "JssTbdxzI+uCUciUv7XbEtRHoJPNrIGZJ1p2nVPQHwVaFt3AClvDjHYyv81IYvYMVDtuIi+t+bjm2i8YnorZQA==", + "dependencies": { + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1", + "Microsoft.Testing.Platform": "1.9.1" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==", + "dependencies": { + "Microsoft.Testing.Platform": "1.9.1" + } + }, + "Microsoft.Testing.Extensions.VSTestBridge": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "k3sgJ6e6WeTS1lAR28SnsRP11IkhT4ymvWvoUM3xd++MNOyusR06bWaiC2iICeOGuiV/iO+Tr9q2XSvznwvQLw==", + "dependencies": { + "Microsoft.TestPlatform.AdapterUtilities": "17.13.0", + "Microsoft.TestPlatform.ObjectModel": "17.13.0", + "Microsoft.Testing.Extensions.Telemetry": "1.9.1", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1", + "Microsoft.Testing.Platform": "1.9.1" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==", + "dependencies": { + "Microsoft.Testing.Platform": "1.9.1" + } + }, + "Microsoft.TestPlatform.AdapterUtilities": { + "type": "Transitive", + "resolved": "17.13.0", + "contentHash": "bFZ3uAhosdXjyXKURDQy37fPosCJQwedB5DG/SzsXL1QXsrfsIYty2kQMqCRRUqm8sBZBRHWRp4BT9SmpWXcKQ==" + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "18.0.1", + "contentHash": "uDJKAEjFTaa2wHdWlfo6ektyoh+WD4/Eesrwb4FpBFKsLGehhACVnwwTI4qD3FrIlIEPlxdXg3SyrYRIcO+RRQ==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "18.0.1", + "Newtonsoft.Json": "13.0.3" + } + }, + "MSTest.Analyzers": { + "type": "Transitive", + "resolved": "3.11.1", + "contentHash": "+oKXORtJ7W3W+/Y3vUbUmWMQbaFQSzqy9YbmRvE3Le8i31gI5Wwe3lrz9dMWuxHSuCEP7CRpnAuAoTI9wGadrA==" + }, + "MSTest.TestAdapter": { + "type": "Transitive", + "resolved": "3.11.1", + "contentHash": "kM19LBh8U9cVXM/Mf9opGPh9Caq5ke9i9UCmLoe0z7bA1C3iZcAk1uiUW4IPiPLm4Xl5QDgaMcrzosLGZh2Jrg==", + "dependencies": { + "Microsoft.Testing.Extensions.VSTestBridge": "1.9.1", + "Microsoft.Testing.Platform.MSBuild": "1.9.1" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==" + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "tCQTzPsGZh/A9LhhA6zrqCRV4hOHsK90/G7q3Khxmn6tnB1PuNU0cRaKANP2AWcF9bn0zsuOoZOSrHuJk6oNBA==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", + "dependencies": { + "System.CodeDom": "6.0.0" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==" + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "ptvgrFh7PvWI8bcVqG5rsA/weWM09EnthFHR5SCnS6IN+P4mj6rE1lBDC4U8HL9/57htKAqy4KQ3bBj84cfYyQ==", + "dependencies": { + "System.Collections.Immutable": "8.0.0" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "E5M5AE2OUTlCrf4omZvzzziUJO9CofBl+lXHaN5IKePPJvHqYFYYpaDPgCpR4VwaFbEebfnjOxxEBtPtsqAxpQ==" + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "6.0.11", + "contentHash": "xqC1HIbJMBFhrpYs76oYP+NAskNVjc6v73HqLal7ECRDPIp4oRU5pPavkD//vNactCn9DA2aaald/I5N+uZ5/g==" + }, + "stryker.solutions": { + "type": "Project", + "dependencies": { + "Microsoft.VisualStudio.SolutionPersistence": "[1.0.52, )" + } + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "CentralTransitive", + "requested": "[18.0.1, )", + "resolved": "18.0.1", + "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==", + "dependencies": { + "System.Reflection.Metadata": "8.0.0" + } + }, + "Microsoft.VisualStudio.SolutionPersistence": { + "type": "CentralTransitive", + "requested": "[1.0.52, )", + "resolved": "1.0.52", + "contentHash": "oNv2JtYXhpdJrX63nibx1JT3uCESOBQ1LAk7Dtz/sr0+laW0KRM6eKp4CZ3MHDR2siIkKsY8MmUkeP5DKkQQ5w==" + }, + "MSTest.TestFramework": { + "type": "CentralTransitive", + "requested": "[3.11.1, )", + "resolved": "3.11.1", + "contentHash": "RMsYXK2639r/8hmP3W22DYU8+NdYep3uzUd3Tub25XrTr/b/FwGjsnr227PHnorVuitJJb8OmVIpBbUAA38i/Q==", + "dependencies": { + "MSTest.Analyzers": "3.11.1" + } + } + } + } +} \ No newline at end of file diff --git a/src/Stryker.Solutions/SolutionFile.cs b/src/Stryker.Solutions/SolutionFile.cs new file mode 100644 index 0000000000..e9e422d8a2 --- /dev/null +++ b/src/Stryker.Solutions/SolutionFile.cs @@ -0,0 +1,134 @@ +using System.Collections.Immutable; +using Microsoft.VisualStudio.SolutionPersistence.Model; +using Microsoft.VisualStudio.SolutionPersistence.Serializer; + +namespace Stryker.Solutions; +public interface ISolutionProvider +{ + SolutionFile GetSolution(string solutionPath); +} + +public class SolutionFile: ISolutionProvider +{ + private readonly Dictionary<(string buildType, string platform), Dictionary> _configurations = []; + + public HashSet GetBuildTypes() => _configurations.Keys.Select(x => x.buildType).ToHashSet(); + + /// + /// Checks if a given configuration (and optional platform) is defined in the solution + /// + /// solution configuration name (Debug, Release...) + /// platform (AnyCPU, x86, x64...) + /// true if at least one project exists in this configuration (and platform if specified) + public bool ConfigurationExists(string buildType, string? platform = null) => _configurations.Keys.Any(x => x.buildType == buildType && (platform == null || platform == x.platform)); + + /// + /// Gets all projects for a given solution configuration (and optional platform) + /// + /// solution configuration name (Debug, Release...) + /// platform (AnyCPU, x86, x64...) + /// A collection of projects' filenames that are defined for this configuration. + public IReadOnlyCollection GetProjects(string buildType, string? platform = null) + => _configurations + .Where(entry => entry.Key.buildType == buildType && (platform == null || entry.Key.platform == platform)) + .SelectMany(entry => entry.Value.Keys).ToImmutableList(); + + private const string DefaultBuildType = "Debug"; + + private string? GetBuildType(string? buildType) + { + if (!string.IsNullOrWhiteSpace(buildType)) + { + return buildType; + } + return (_configurations.Count == 0 || ConfigurationExists(DefaultBuildType)) ? DefaultBuildType : _configurations.Keys.First().buildType; + } + + /// + /// Gets all projects for a given solution configuration (and optional platform) with project details + /// + /// configuration name (Debug, Release...) + /// platform (AnyCPU, x86, x64...) + /// A collection of tuple with the projects' filename, project's configuration and project's platform for the provided criteria . + public IReadOnlyCollection<(string file, string buildType, string platform)> GetProjectsWithDetails(string buildType, string? platform = null) + { + buildType = GetBuildType(buildType); + return _configurations + .Where(entry => entry.Key.buildType == buildType && (platform == null || entry.Key.platform == platform)) + .SelectMany(entry => entry.Value).Select(p => (p.Key, p.Value.buildType, p.Value.platform)) + .ToImmutableList(); + } + + /// + /// Create a solution file from a list of projects, assuming all projects are built in Debug|Any CPU + /// + /// list of csproj filenames + /// a solution instance + /// this method is used for testing purposes, as the underlying solution parser do not support any form of mocking + public static SolutionFile BuildFromProjectList(List projects) + { + var result = new SolutionFile(); + // default to Debug|Any CPU + foreach (var buildType in (string[])["Debug", "Release"]) + { + var platform = "Any CPU"; + var projectDict = new Dictionary(); + foreach (var project in projects) + { + projectDict[project] = (buildType, platform); + } + result._configurations.Add((buildType, platform), projectDict); + } + return result; + } + + private static SolutionFile AnalyzeSolution(SolutionModel solution) + { + // extract needed information + var result = new SolutionFile(); + foreach (var buildType in solution.BuildTypes) + { + foreach (var solutionPlatform in solution.Platforms) + { + var projects = new Dictionary(); + // add all projects that are built with this configuration + foreach (var solutionProject in solution.SolutionProjects) + { + var (projectBuildType, projectPlatform, isBuilt, _) = solutionProject.GetProjectConfiguration(buildType, solutionPlatform); + if (!isBuilt || projectBuildType == null || projectPlatform == null) + { + continue; + } + + projects[solutionProject.FilePath] = (projectBuildType, projectPlatform); + } + if (projects.Count == 0) + { + continue; + } + result._configurations.Add((buildType, solutionPlatform), projects); + } + } + + return result; + } + + /// + /// Loads a solution file from disk + /// + /// path to a sln or slnx file + /// a solution instance + /// if the solution file format is not supported + public SolutionFile GetSolution(string solutionPath) + { + // Implementation to load a solution file + var serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath); + + if (serializer == null) + { + throw new InvalidOperationException($"No suitable solution serializer found for the given path ({solutionPath})."); + } + + return AnalyzeSolution(serializer.OpenAsync(solutionPath, CancellationToken.None).GetAwaiter().GetResult()); + } +} diff --git a/src/Stryker.Solutions/Stryker.Solutions.csproj b/src/Stryker.Solutions/Stryker.Solutions.csproj new file mode 100644 index 0000000000..d72fb4cc33 --- /dev/null +++ b/src/Stryker.Solutions/Stryker.Solutions.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/src/Stryker.Solutions/packages.lock.json b/src/Stryker.Solutions/packages.lock.json new file mode 100644 index 0000000000..e2e6a56ba8 --- /dev/null +++ b/src/Stryker.Solutions/packages.lock.json @@ -0,0 +1,19 @@ +{ + "version": 2, + "dependencies": { + "net8.0": { + "DotNet.ReproducibleBuilds": { + "type": "Direct", + "requested": "[1.2.39, )", + "resolved": "1.2.39", + "contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg==" + }, + "Microsoft.VisualStudio.SolutionPersistence": { + "type": "Direct", + "requested": "[1.0.52, )", + "resolved": "1.0.52", + "contentHash": "oNv2JtYXhpdJrX63nibx1JT3uCESOBQ1LAk7Dtz/sr0+laW0KRM6eKp4CZ3MHDR2siIkKsY8MmUkeP5DKkQQ5w==" + } + } + } +} \ No newline at end of file diff --git a/src/Stryker.TestRunner.VsTest.UnitTest/packages.lock.json b/src/Stryker.TestRunner.VsTest.UnitTest/packages.lock.json index 69d2e1ceb1..5d0e8f4f87 100644 --- a/src/Stryker.TestRunner.VsTest.UnitTest/packages.lock.json +++ b/src/Stryker.TestRunner.VsTest.UnitTest/packages.lock.json @@ -801,6 +801,7 @@ "Stryker.DataCollector": "[1.0.0, )", "Stryker.Regex.Parser": "[1.0.0, )", "Stryker.RegexMutators": "[1.0.0, )", + "Stryker.Solutions": "[1.0.0, )", "Stryker.TestRunner.VsTest": "[1.0.0, )", "Stryker.Utilities": "[1.0.0, )", "TestableIO.System.IO.Abstractions.Wrappers": "[22.1.0, )" @@ -841,6 +842,7 @@ "Spectre.Console.Testing": "[0.54.0, )", "Stryker.Abstractions": "[1.0.0, )", "Stryker.Regex.Parser": "[1.0.0, )", + "Stryker.Solutions": "[1.0.0, )", "Stryker.Utilities": "[1.0.0, )", "TestableIO.System.IO.Abstractions.TestingHelpers": "[22.1.0, )", "stryker": "[4.8.1, )" @@ -859,6 +861,12 @@ "Stryker.Regex.Parser": "[1.0.0, )" } }, + "stryker.solutions": { + "type": "Project", + "dependencies": { + "Microsoft.VisualStudio.SolutionPersistence": "[1.0.52, )" + } + }, "stryker.testrunner": { "type": "Project", "dependencies": { @@ -1051,6 +1059,12 @@ "NETStandard.Library": "2.0.0" } }, + "Microsoft.VisualStudio.SolutionPersistence": { + "type": "CentralTransitive", + "requested": "[1.0.52, )", + "resolved": "1.0.52", + "contentHash": "oNv2JtYXhpdJrX63nibx1JT3uCESOBQ1LAk7Dtz/sr0+laW0KRM6eKp4CZ3MHDR2siIkKsY8MmUkeP5DKkQQ5w==" + }, "Microsoft.Web.LibraryManager.Build": { "type": "CentralTransitive", "requested": "[3.0.71, )", diff --git a/src/Stryker.TestRunner.VsTest/VsTestRunner.cs b/src/Stryker.TestRunner.VsTest/VsTestRunner.cs index d7b36c1799..291d7b351f 100644 --- a/src/Stryker.TestRunner.VsTest/VsTestRunner.cs +++ b/src/Stryker.TestRunner.VsTest/VsTestRunner.cs @@ -258,7 +258,7 @@ public IRunResults RunCoverageSession(ITestIdentifiers testsToRun, IProjectAndTe } var runSettings = _context.GenerateRunSettings(timeOut, forCoverage, mutantTestsMap, projectAndTests.HelperNamespace, source.TargetFramework, source.TargetPlatform()); - _logger.LogTrace("{RunnerId}: testing assembly {source}.", RunnerId, source); + _logger.LogTrace("{RunnerId}: testing assembly {source}.", RunnerId, source.GetAssemblyFileName()); var activeId = -1; if (mutantTestsMap is { Count: 1 }) { diff --git a/src/Stryker.TestRunner.VsTest/packages.lock.json b/src/Stryker.TestRunner.VsTest/packages.lock.json index f540f830f6..7ef08a8ecb 100644 --- a/src/Stryker.TestRunner.VsTest/packages.lock.json +++ b/src/Stryker.TestRunner.VsTest/packages.lock.json @@ -411,4 +411,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/Stryker.Utilities/Buildalyzer/Buildalyzer.cs b/src/Stryker.Utilities/Buildalyzer/Buildalyzer.cs index af3fa09862..7444013e04 100644 --- a/src/Stryker.Utilities/Buildalyzer/Buildalyzer.cs +++ b/src/Stryker.Utilities/Buildalyzer/Buildalyzer.cs @@ -9,13 +9,10 @@ namespace Stryker.Utilities.Buildalyzer; public interface IBuildalyzerProvider { IAnalyzerManager Provide(AnalyzerManagerOptions options = null); - IAnalyzerManager Provide(string solutionFilePath, AnalyzerManagerOptions options = null); } [ExcludeFromCodeCoverage] public class BuildalyzerProvider : IBuildalyzerProvider { public IAnalyzerManager Provide(AnalyzerManagerOptions options = null) => new AnalyzerManager(options); - - public IAnalyzerManager Provide(string solutionFilePath, AnalyzerManagerOptions options = null) => new AnalyzerManager(solutionFilePath, options); } diff --git a/src/Stryker.Utilities/Buildalyzer/IAnalyzerResultExtensions.cs b/src/Stryker.Utilities/Buildalyzer/IAnalyzerResultExtensions.cs index 550959c3e8..23fb061de1 100644 --- a/src/Stryker.Utilities/Buildalyzer/IAnalyzerResultExtensions.cs +++ b/src/Stryker.Utilities/Buildalyzer/IAnalyzerResultExtensions.cs @@ -57,10 +57,12 @@ public static string GetSymbolFileName(this IAnalyzerResult analyzerResult) => public static string TargetPlatform(this IAnalyzerResult analyzerResult) => analyzerResult.GetPropertyOrDefault("TargetPlatform", "AnyCPU"); - public static string MsBuildPath(this IAnalyzerResult analyzerResult) => analyzerResult.Analyzer?.EnvironmentFactory.GetBuildEnvironment()?.MsBuildExePath; + public static string? MsBuildPath(this IAnalyzerResult analyzerResult) => analyzerResult.Analyzer?.EnvironmentFactory.GetBuildEnvironment()?.MsBuildExePath; public static IEnumerable GetSourceGenerators(this IAnalyzerResult analyzerResult, ILogger logger) { + ArgumentNullException.ThrowIfNull(logger); + var generators = new List(); foreach (var analyzer in analyzerResult.AnalyzerReferences) { @@ -68,16 +70,15 @@ public static IEnumerable GetSourceGenerators(this IAnalyzerRe { var analyzerFileReference = new AnalyzerFileReference(analyzer, AnalyzerAssemblyLoader.Instance); analyzerFileReference.AnalyzerLoadFailed += (sender, e) => LogAnalyzerLoadError(logger, sender, e); - foreach (var generator in analyzerFileReference.GetGenerators(LanguageNames.CSharp)) - { - generators.Add(generator); - } + generators.AddRange(analyzerFileReference.GetGenerators(LanguageNames.CSharp)); } catch (Exception e) { - logger?.LogWarning(e, - @"Analyzer/Generator assembly {analyzer} could not be loaded. -Generated source code may be missing.", analyzer); + logger.LogWarning(e, + """ + Analyzer/Generator assembly {0} could not be loaded. + Generated source code may be missing. + """, analyzer); } } @@ -85,9 +86,9 @@ public static IEnumerable GetSourceGenerators(this IAnalyzerRe } [ExcludeFromCodeCoverage(Justification = "Impossible to unit test")] - private static void LogAnalyzerLoadError(ILogger logger, object sender, AnalyzerLoadFailureEventArgs e) + private static void LogAnalyzerLoadError(ILogger? logger, object? sender, AnalyzerLoadFailureEventArgs e) { - var source = ((AnalyzerReference)sender)?.Display ?? "unknown"; + var source = (sender as AnalyzerReference)?.Display ?? "unknown"; logger?.LogWarning( "Failed to load analyzer '{Source}': {Message} (error : {Error}, analyzer: {Analyzer}).", source, e.Message, Enum.GetName(e.ErrorCode.GetType(), e.ErrorCode) ?? e.ErrorCode.ToString(), @@ -120,33 +121,7 @@ public static IEnumerable LoadReferences(this IAnalyzerResult } } - public sealed class AnalyzerAssemblyLoader : IAnalyzerAssemblyLoader - { - public static readonly IAnalyzerAssemblyLoader Instance = new AnalyzerAssemblyLoader(); - - private readonly Dictionary _cache = []; - - private AnalyzerAssemblyLoader() { } - - public void AddDependencyLocation(string fullPath) - { - if (!_cache.ContainsKey(fullPath)) - { - _cache[fullPath] = Assembly.LoadFrom(fullPath); //NOSONAR we actually need to load a specified file, not a specific assembly - } - } - - public Assembly LoadFromPath(string fullPath) - { - if (!_cache.TryGetValue(fullPath, out var assembly)) - { - _cache[fullPath] = assembly = Assembly.LoadFrom(fullPath); //NOSONAR we actually need to load a specified file, not a specific assembly - } - return assembly; - } - } - - public static NuGetFramework GetNuGetFramework(this IAnalyzerResult analyzerResult) + public static NuGetFramework? GetNuGetFramework(this IAnalyzerResult analyzerResult) { if (string.IsNullOrEmpty(analyzerResult.TargetFramework)) { @@ -178,9 +153,24 @@ public static Language GetLanguage(this IAnalyzerResult analyzerResult) => private static readonly string[] knownTestPackages = ["MSTest.TestFramework", "xunit", "NUnit"]; + /// + /// checks if an analyzer result is valid + /// + /// analyzer result used for determination + /// true if result is complete enough + public static bool IsValid(this IAnalyzerResult br) => br.Succeeded || (br.SourceFiles.Length > 0 && br.References.Length > 0); + + /// + /// checks if an analyzer result is valid for a specific framework + /// + /// analyzer result used for determination + /// framework to test for + /// true if result is complete enough + public static bool IsValidFor(this IAnalyzerResult br, string framework) => br.IsValid() && br.TargetFramework == framework; + public static bool IsTestProject(this IEnumerable analyzerResults) => analyzerResults.Any(x => x.IsTestProject()); - public static bool IsTestProject(this IAnalyzerResult analyzerResult) + private static bool IsTestProject(this IAnalyzerResult analyzerResult) { if (!bool.TryParse(analyzerResult.GetPropertyOrDefault("IsTestProject"), out var isTestProject)) { @@ -268,8 +258,11 @@ private static IEnumerable ParseDiagnostics(string diagnostics) public static int GetWarningLevel(this IAnalyzerResult analyzerResult) => int.Parse(analyzerResult.GetPropertyOrDefault("WarningLevel", "4")); - public static string GetRootNamespace(this IAnalyzerResult analyzerResult) => - analyzerResult.GetPropertyOrDefault("RootNamespace") ?? analyzerResult.GetAssemblyName(); + private static string GetRootNamespace(this IAnalyzerResult analyzerResult) => + analyzerResult.Properties.TryGetValue("RootNamespace", out var rootNamespace) && + !string.IsNullOrEmpty(rootNamespace) + ? rootNamespace + : analyzerResult.GetAssemblyName(); public static bool GetPropertyOrDefault(this IAnalyzerResult analyzerResult, string name, bool defaultBoolean) { @@ -284,5 +277,31 @@ public static string GetPropertyOrDefault(this IAnalyzerResult analyzerResult, s string defaultValue = default) => analyzerResult.Properties.GetValueOrDefault(name, defaultValue); - private static IEnumerable GetItem(this IAnalyzerResult analyzerResult, string name) => !analyzerResult.Items.TryGetValue(name, out var item) ? [] : item; + private static IProjectItem[] GetItem(this IAnalyzerResult analyzerResult, string name) => !analyzerResult.Items.TryGetValue(name, out var item) ? [] : item; + + private sealed class AnalyzerAssemblyLoader : IAnalyzerAssemblyLoader + { + public static readonly IAnalyzerAssemblyLoader Instance = new AnalyzerAssemblyLoader(); + + private readonly Dictionary _cache = []; + + private AnalyzerAssemblyLoader() { } + + public void AddDependencyLocation(string fullPath) + { + if (!_cache.ContainsKey(fullPath)) + { + _cache[fullPath] = Assembly.LoadFrom(fullPath); //NOSONAR we actually need to load a specified file, not a specific assembly + } + } + + public Assembly LoadFromPath(string fullPath) + { + if (!_cache.TryGetValue(fullPath, out var assembly)) + { + _cache[fullPath] = assembly = Assembly.LoadFrom(fullPath); //NOSONAR we actually need to load a specified file, not a specific assembly + } + return assembly; + } + } } diff --git a/src/Stryker.sln b/src/Stryker.sln index 78a544af32..c39c23b8d1 100644 --- a/src/Stryker.sln +++ b/src/Stryker.sln @@ -58,6 +58,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stryker.TestRunner.VsTest", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stryker.TestRunner.VsTest.UnitTest", "Stryker.TestRunner.VsTest.UnitTest\Stryker.TestRunner.VsTest.UnitTest.csproj", "{CD0D00F0-E9B5-4F73-9A46-DA7CDB78BE61}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stryker.Solutions", "Stryker.Solutions\Stryker.Solutions.csproj", "{948D3ECA-F864-4C9B-912C-B4043011E556}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stryker.Solutions.Test", "Stryker.Solutions.Test\Stryker.Solutions.Test.csproj", "{AE65C8F9-784C-4872-B8B3-04CE2FDBD589}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -116,6 +120,14 @@ Global {CD0D00F0-E9B5-4F73-9A46-DA7CDB78BE61}.Debug|Any CPU.Build.0 = Debug|Any CPU {CD0D00F0-E9B5-4F73-9A46-DA7CDB78BE61}.Release|Any CPU.ActiveCfg = Release|Any CPU {CD0D00F0-E9B5-4F73-9A46-DA7CDB78BE61}.Release|Any CPU.Build.0 = Release|Any CPU + {948D3ECA-F864-4C9B-912C-B4043011E556}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {948D3ECA-F864-4C9B-912C-B4043011E556}.Debug|Any CPU.Build.0 = Debug|Any CPU + {948D3ECA-F864-4C9B-912C-B4043011E556}.Release|Any CPU.ActiveCfg = Release|Any CPU + {948D3ECA-F864-4C9B-912C-B4043011E556}.Release|Any CPU.Build.0 = Release|Any CPU + {AE65C8F9-784C-4872-B8B3-04CE2FDBD589}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AE65C8F9-784C-4872-B8B3-04CE2FDBD589}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AE65C8F9-784C-4872-B8B3-04CE2FDBD589}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AE65C8F9-784C-4872-B8B3-04CE2FDBD589}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE