From 53ceb13d682425f254b116beec3c601f1b0bbf6e Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Mon, 9 Feb 2026 10:19:36 +0100 Subject: [PATCH 01/41] WIP --- src/.run/FullFrameworkApp.Test.run.xml | 44 +++--- .../Stryker.CLI/Logging/LoggingInitializer.cs | 6 +- .../Initialisation/InputFileResolver.cs | 145 ++++++++++++------ .../Buildalyzer/IAnalyzerResultExtensions.cs | 17 ++ 4 files changed, 144 insertions(+), 68 deletions(-) diff --git a/src/.run/FullFrameworkApp.Test.run.xml b/src/.run/FullFrameworkApp.Test.run.xml index 19d8762fe1..efea798be0 100644 --- a/src/.run/FullFrameworkApp.Test.run.xml +++ b/src/.run/FullFrameworkApp.Test.run.xml @@ -1,23 +1,23 @@ - - - + + + \ No newline at end of file diff --git a/src/Stryker.CLI/Stryker.CLI/Logging/LoggingInitializer.cs b/src/Stryker.CLI/Stryker.CLI/Logging/LoggingInitializer.cs index a5b9f5a62c..ed940c9eba 100644 --- a/src/Stryker.CLI/Stryker.CLI/Logging/LoggingInitializer.cs +++ b/src/Stryker.CLI/Stryker.CLI/Logging/LoggingInitializer.cs @@ -28,7 +28,11 @@ public void SetupLogOptions(IStrykerInputs inputs, IFileSystem fileSystem = null inputs.OutputPathInput.SuppliedInput = outputPath; var diagnoseMode = inputs.DiagModeInput.Validate(); - var logLevel = diagnoseMode ? LogEventLevel.Verbose : inputs.VerbosityInput.Validate(); + var logLevel = inputs.VerbosityInput.Validate(); + if (diagnoseMode && logLevel>LogEventLevel.Debug) + { + logLevel = LogEventLevel.Debug; + } var logToFile = inputs.LogToFileInput.Validate(outputPath) || diagnoseMode; ApplicationLogging.ConfigureLogger(logLevel, logToFile, diagnoseMode, outputPath); diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index 0c74441c3a..d2642c67e8 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -25,7 +25,6 @@ public interface IInputFileResolver IFileSystem FileSystem { get; } } - /// /// - Reads .csproj to find project under test /// - Scans project under test and store files to mutate @@ -38,7 +37,7 @@ public class InputFileResolver : IInputFileResolver private readonly IBuildalyzerProvider _analyzerProvider; private readonly ISolutionProvider _solutionProvider; private static readonly HashSet ImportantProperties = - ["Configuration", "Platform", "AssemblyName", "Configurations"]; + ["Configuration", "Platform", "AssemblyName", "Configurations", "TargetPath"]; private readonly INugetRestoreProcess _nugetRestoreProcess; private readonly Dictionary _buildLogs = []; @@ -58,6 +57,12 @@ public InputFileResolver(IFileSystem fileSystem, public IFileSystem FileSystem { get; } + /// + /// Identifies the project(s) to mutate and their associated test project(s) according to provided options, and returns a collection of describing them. + /// + /// Stryker options + /// a collection of describing mutable project + /// Thrown if the method fails during analysis. public IReadOnlyCollection ResolveSourceProjectInfos(IStrykerOptions options) { var normalizedProjectUnderTestNameFilter = NormalizePath(options.SourceProjectName); @@ -109,6 +114,14 @@ public IReadOnlyCollection ResolveSourceProjectInfos(IStryker throw new InputException(stringBuilder.ToString()); } + /// + /// Identifies the project(s) to mutate and their associated test project(s) according to provided options, and returns a collection of describing them, when not mutating + /// the whole solution. + /// + /// Stryker options + /// solution file if any + /// name filter to apply to the mutated projects + /// identified mutable projects matching the provided name filter (when provided). Can be empty private List SourceProjectInfos(IStrykerOptions options, SolutionFile solution, string normalizedProjectUnderTestNameFilter) { @@ -130,7 +143,8 @@ private List SourceProjectInfos(IStrykerOptions options, Solu _logger.LogInformation("Analyzing {ProjectCount} test project(s).", testProjectFileNames.Count); List<(string projectFile, string framework, string configuration)> projectList = [..testProjectFileNames.Select(p => (p, options.TargetFramework, options.Configuration))]; - // if test project is provided but no source project + // if test project is provided but no source project => working directory should contain the project to mutate, we try to detect + // it and use it as a filter for the analysis. This allows users to just cd into a project and run stryker var targetProjectMode = testProjectsSpecified && string.IsNullOrEmpty(options.SourceProjectName); if (targetProjectMode) { @@ -141,13 +155,15 @@ private List SourceProjectInfos(IStrykerOptions options, Solu if (!targetProjectMode) { // we detected a test project, discard it + // Stryker will need to detect mutable projects out from test project references _logger.LogDebug("Working directory contains a test project."); normalizedProjectUnderTestNameFilter = null; } } - // we match test projects to mutable projects + // we analyze test projects var analyzeAllNeededProjects = AnalyzeAllNeededProjects(projectList, normalizedProjectUnderTestNameFilter, options, ScanMode.ScanTestProjectReferences); + // we match test projects to mutable projects var (findMutableAnalyzerResults, orphans) = FindMutableAnalyzerResults(analyzeAllNeededProjects); var result = AnalyzeAndIdentifyProjects(options, solutionInfo, findMutableAnalyzerResults, orphans); @@ -406,7 +422,7 @@ private IEnumerable AnalyzeThisProject(IProjectAnalyzer project /// /// scan mode /// analyzer results to parse - /// A list of project to analyse + /// A list of project to analyze private List ScanReferences(ScanMode mode, IEnumerable buildResult) { var referencesToAdd = new List(); @@ -429,7 +445,6 @@ private IAnalyzerResults AnalyzeSingleProject(IProjectAnalyzer project, StringWr _logger.LogDebug("Analyzing {ProjectFilePath}", projectLogName); var env = new EnvironmentOptions(); - if (!string.IsNullOrEmpty(options.MsBuildPath)) { // we need to forward this path to buildalyzer @@ -437,15 +452,13 @@ private IAnalyzerResults AnalyzeSingleProject(IProjectAnalyzer project, StringWr } var buildResult = project.Build(env); - // store the build log _buildLogs[projectLogName] = buildLogger.ToString(); - // clear the log buildLogger.GetStringBuilder().Clear(); - var buildResultOverallSuccess = buildResult.OverallSuccess || Array. - TrueForAll(project.ProjectFile.TargetFrameworks, tf => - buildResult.Any(br => br.IsValidFor(tf))); + var projectFileTargetFrameworks = GetTargetFrameworks(project, options, projectLogName, buildResult); + var buildResultOverallSuccess = buildResult.IsValidFor(projectFileTargetFrameworks); + // retry if it failed if (!buildResultOverallSuccess) { if (options.DiagMode) @@ -455,6 +468,10 @@ private IAnalyzerResults AnalyzeSingleProject(IProjectAnalyzer project, StringWr // if this is a full framework project, we can retry after a nuget restore buildResult = RetryBuild(project, options, projectLogName, buildResult, out buildResultOverallSuccess); + // store the build log + _buildLogs[projectLogName] = buildLogger.ToString(); + // clear the log + buildLogger.GetStringBuilder().Clear(); } LogAnalyzerResult(buildResult, options); @@ -465,7 +482,7 @@ private IAnalyzerResults AnalyzeSingleProject(IProjectAnalyzer project, StringWr } // log failure details - var failedFrameworks = project.ProjectFile.TargetFrameworks.Where(tf => + var failedFrameworks = projectFileTargetFrameworks.Where(tf => !buildResult.Any(br => br.IsValidFor(tf))).ToList(); _logger.LogWarning( "Analysis of project {ProjectFilePath} failed for frameworks {FrameworkList}.", @@ -473,12 +490,38 @@ private IAnalyzerResults AnalyzeSingleProject(IProjectAnalyzer project, StringWr if (options.DiagMode) { - _logger.LogWarning("Project analysis failed. The MsBuild log: {BuildLog}", buildLogger.ToString()); + _logger.LogWarning("Project {ProjectFilePath} analysis failed. The MsBuild log is: {Log}", projectLogName, _buildLogs[projectLogName]); } return buildResult; } + // get the list of target frameworks + private string[] GetTargetFrameworks(IProjectAnalyzer project, IStrykerOptions options, string projectLogName, + IAnalyzerResults buildResult) + { + var projectFileTargetFrameworks = project.ProjectFile.TargetFrameworks; + if (projectFileTargetFrameworks.Length > 0) + { + _logger.LogDebug("Project {ProjectFilePath} supported frameworks: {FrameworkList}.", projectLogName, string.Join(',', projectFileTargetFrameworks)); + } + else + { + if (!string.IsNullOrEmpty(options.TargetFramework)) + { + _logger.LogWarning("Failed to identify target frameworks for project {ProjectFilePath}. Assuming selected framework ({framework}) is present.", projectLogName, options.TargetFramework); + projectFileTargetFrameworks=[options.TargetFramework]; + } + else + { + projectFileTargetFrameworks = buildResult.Select(br => br.TargetFramework).ToArray(); + _logger.LogWarning("Failed to identify target frameworks for project {ProjectFilePath}. Using analysis results: {frameworks}", projectLogName, string.Join(',', projectFileTargetFrameworks)); + } + } + + return projectFileTargetFrameworks; + } + private IAnalyzerResults RetryBuild(IProjectAnalyzer project, IStrykerOptions options, string projectLogName, IAnalyzerResults buildResult, out bool buildResultOverallSuccess) { @@ -502,17 +545,17 @@ private IAnalyzerResults RetryBuild(IProjectAnalyzer project, IStrykerOptions op buildResult = project.Build(buildOptions); // check the new status - buildResultOverallSuccess = project.ProjectFile.TargetFrameworks.Length > 0 && - Array.TrueForAll(project.ProjectFile.TargetFrameworks, tf => - buildResult.Any(br => br.IsValidFor(tf))); + buildResultOverallSuccess = buildResult.IsValidFor(project.ProjectFile.TargetFrameworks); - if (!buildResultOverallSuccess && !string.IsNullOrEmpty(options.TargetFramework)) + if (buildResultOverallSuccess || string.IsNullOrEmpty(options.TargetFramework)) { - // still failed, we can try using target framework option - buildResult = project.Build(options.TargetFramework); - buildResultOverallSuccess = buildResult.Any( br => br.IsValidFor(options.TargetFramework)); + return buildResult; } + // still failed, we can try using target framework option + buildResult = project.Build(options.TargetFramework); + buildResultOverallSuccess = buildResult.Any( br => br.IsValidFor(options.TargetFramework)); + return buildResult; } @@ -543,29 +586,42 @@ private void LogAnalyzerResult(IAnalyzerResults analyzerResults, IStrykerOptions { log.AppendLine($"Property {property}={properties.GetValueOrDefault(property) ?? "\"'undefined'\""}"); } - foreach (var sourceFile in analyzerResult.SourceFiles) + if (analyzerResult.SourceFiles.Length == 0) { - log.AppendLine($"SourceFile {sourceFile}"); + log.AppendLine("** No source files identified **"); } - - foreach (var reference in analyzerResult.References) + else { - log.AppendLine($"References: {FileSystem.Path.GetFileName(reference)} (in {FileSystem.Path.GetDirectoryName(reference)})"); + foreach (var sourceFile in analyzerResult.SourceFiles) + { + log.AppendLine($"SourceFile {sourceFile}"); + } } - - foreach (var property in properties) + if (analyzerResult.References.Length == 0) + { + log.AppendLine("** No references Identified **"); + } + else { - if (ImportantProperties.Contains(property.Key)) + foreach (var reference in analyzerResult.References) { - continue; // already logged + log.AppendLine($"References: {FileSystem.Path.GetFileName(reference)} (in {FileSystem.Path.GetDirectoryName(reference)})"); } + } - log.AppendLine($"Property {property.Key}={property.Value.Replace(Environment.NewLine, "\\n")}"); + if (_logger.IsEnabled(LogLevel.Trace)) + { + // dumps all other properties as well, as they can be useful for diagnosing build issues + foreach (var property in properties.Where( p => !ImportantProperties.Contains(p.Key) )) + { + log.AppendLine($"Property {property.Key}={property.Value.Replace(Environment.NewLine, "\\n")}"); + } } + log.AppendLine(); } log.AppendLine("**** End Buildalyzer result ****"); - _logger.LogTrace(log.ToString()); + _logger.LogDebug(log.ToString()); } private IAnalyzerResult SelectAnalyzerResult(IEnumerable analyzerResults, string targetFramework) @@ -600,24 +656,24 @@ private IAnalyzerResult SelectAnalyzerResult(IEnumerable analyz var firstAnalyzerResult = PickFrameworkVersion(); var availableFrameworks = validResults.Select(a => a.TargetFramework).Distinct(); - var firstFramework = firstAnalyzerResult.TargetFramework; _logger.LogWarning( """ Could not find a valid analysis for target {0} for project '{1}'. The available target frameworks are: {2}. selected version is {3}. - """, targetFramework, projectName, string.Join(',', availableFrameworks), firstFramework); + """, targetFramework, projectName, string.Join(',', availableFrameworks), firstAnalyzerResult.TargetFramework); return firstAnalyzerResult; IAnalyzerResult PickFrameworkVersion() { - return validResults.Find(a => a.Succeeded && !a.TargetsFullFramework()) ?? validResults[0]; + return validResults.Find(a => a.IsValid() && !a.TargetsFullFramework()) ?? validResults[0]; } } private (Dictionary>, List) FindMutableAnalyzerResults(ConcurrentBag<(IEnumerable result, bool isTest)> mutableProjectsAnalyzerResults) { + // separate test projects from mutable projects, and keep only analyzer results building an assembly (exclude solution folders and such) var analyzerTestProjects = mutableProjectsAnalyzerResults.Where(p => p.isTest).SelectMany(p => p.result).Where(p => p.BuildsAnAssembly()); var mutableProjects = mutableProjectsAnalyzerResults.Where(p => !p.isTest).SelectMany(p => p.result).Where(p => p.BuildsAnAssembly()).ToArray(); @@ -661,7 +717,7 @@ private static bool ScanProjectReferences(Dictionary> mutableToTestMap, IAnalyzerResult[] mutableProjects, IAnalyzerResult testProject) { var foundOneProject = false; - // we identify which project are referenced by it + // we identify which projects are referenced by it foreach (var mutableProject in mutableProjects) { var assemblyPath = mutableProject.GetAssemblyPath(); @@ -709,17 +765,16 @@ private SourceProjectInfo BuildSourceProjectInfo(IStrykerOptions options, "Mutation testing of F# projects is not ready yet. No mutants will be generated.")); } - var builder = (ProjectComponentsBuilder)(language switch + ProjectComponentsBuilder builder; + if (language == Language.Csharp) { - Language.Csharp => new CsharpProjectComponentsBuilder( - targetProjectInfo, - options, - _foldersToExclude, - _logger, - FileSystem), - - _ => throw new NotSupportedException($"Language not supported: {language}") - }); + builder = new CsharpProjectComponentsBuilder(targetProjectInfo, options, _foldersToExclude, _logger, + FileSystem); + } + else + { + throw new NotSupportedException($"Language not supported: {language}"); + } var inputFiles = builder.Build(); builder.InjectHelpers(inputFiles); diff --git a/src/Stryker.Utilities/Buildalyzer/IAnalyzerResultExtensions.cs b/src/Stryker.Utilities/Buildalyzer/IAnalyzerResultExtensions.cs index a8336318f9..51d6d1b63d 100644 --- a/src/Stryker.Utilities/Buildalyzer/IAnalyzerResultExtensions.cs +++ b/src/Stryker.Utilities/Buildalyzer/IAnalyzerResultExtensions.cs @@ -168,6 +168,23 @@ public static bool IsValid(this IAnalyzerResult br) => br.Succeeded || (br.Sourc /// true if result is complete enough public static bool IsValidFor(this IAnalyzerResult br, string framework) => br.IsValid() && br.TargetFramework == framework; + /// + /// Checks if a project analysis is at least partially successful + /// + /// Analysis results + /// true if analysis was successful + public static bool HasValidResult(this IAnalyzerResults br) => br.OverallSuccess || br.Results.Any(IsValid); + + /// + /// Checks is a project analysis is valid for all given target frameworks. If no target frameworks are given, it checks if the overall analysis was successful. + /// + /// Analysis results. + /// list of frameworks to check for + /// true if analysis was successful + public static bool IsValidFor(this IAnalyzerResults br, string[] targetFrameworks) => br.OverallSuccess + || (targetFrameworks.Length>0 + && Array.TrueForAll(targetFrameworks, fmw => br.Results.Any( r=> r.IsValidFor(fmw)))); + public static bool IsTestProject(this IEnumerable analyzerResults) => analyzerResults.Any(x => x.IsTestProject()); private static bool IsTestProject(this IAnalyzerResult analyzerResult) From 85a580c1b3a17f3e37add67a2b3cd73a688cdbf3 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Tue, 17 Feb 2026 14:38:07 +0100 Subject: [PATCH 02/41] fix: fix flaky test Dispose_ShouldDisposeAllRunnersInPool --- .../MicrosoftTestPlatformRunnerPoolTests.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Stryker.TestRunner.MicrosoftTestPlatform.UnitTest/MicrosoftTestPlatformRunnerPoolTests.cs b/src/Stryker.TestRunner.MicrosoftTestPlatform.UnitTest/MicrosoftTestPlatformRunnerPoolTests.cs index 21463f7c5d..bb991db999 100644 --- a/src/Stryker.TestRunner.MicrosoftTestPlatform.UnitTest/MicrosoftTestPlatformRunnerPoolTests.cs +++ b/src/Stryker.TestRunner.MicrosoftTestPlatform.UnitTest/MicrosoftTestPlatformRunnerPoolTests.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -176,12 +177,25 @@ public void Dispose_ShouldDisposeAllRunnersInPool() (id, testsByAssembly, testDescriptions, testSet, discoveryLock, logger) => { var testRunner = new TestableRunner(id, () => disposedRunners.Add(id)); - createdRunners.Add(id); + lock (createdRunners) + { + createdRunners.Add(id); + Monitor.Pulse(createdRunners); + } return testRunner; }); var pool = new MicrosoftTestPlatformRunnerPool(options.Object, NullLogger.Instance, runnerFactory.Object); - + var timeout = new Stopwatch(); + timeout.Start(); + var start = timeout.ElapsedMilliseconds; + lock (createdRunners) + { + while (createdRunners.Count<3 && timeout.ElapsedMilliseconds-start<2000) + { + Monitor.Wait(createdRunners, 200); + } + } createdRunners.Count.ShouldBe(3, "All 3 runners should have been created before disposal"); // Act From 051a7fba1160cd037ff0a7474a58714ee836bba9 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Tue, 24 Feb 2026 18:16:40 +0100 Subject: [PATCH 03/41] chore: massive refactoring of InputFileResolver to migrate some responsibility to new class 'ProjectAnalyzerContext' (WIP) --- .../Initialisation/InputFileResolverTests.cs | 9 +- .../ProjectOrchestratorTests.cs | 13 +- .../CollectionExpressionMutatorTests.cs | 36 +-- .../Initialisation/InitialisationProcess.cs | 2 +- .../Initialisation/InputFileResolver.cs | 243 +++++------------- .../Initialisation/ProjectAnalyzerContext.cs | 158 ++++++++++++ .../Initialisation/TargetsForMutation.cs | 114 ++++++++ .../SourceProjects/SourceProjectInfo.cs | 10 +- .../SolutionFileShould.cs | 6 +- src/Stryker.Solutions/SolutionFile.cs | 22 +- 10 files changed, 390 insertions(+), 223 deletions(-) create mode 100644 src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs create mode 100644 src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InputFileResolverTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InputFileResolverTests.cs index 09af3fafba..e8e2297e1e 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InputFileResolverTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InputFileResolverTests.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using Buildalyzer; +using Buildalyzer.Environment; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -1567,7 +1568,7 @@ public void ShouldPassPlatformToBuildalyzerWhenSpecified() target.ResolveSourceProjectInfos(options); // Assert - managerMock.Verify(x => x.SetGlobalProperty("Platform", "x64"), Times.AtLeastOnce); + sourceProjectManagerMock.Verify(x => x.Build(It.Is( env => env.GlobalProperties["Platform"] == "x64")), Times.AtLeastOnce); } [TestMethod] @@ -1642,7 +1643,7 @@ public void ShouldPassPlatformFromSolutionToBuildalyzer() target.ResolveSourceProjectInfos(options); // Assert - managerMock.Verify(x => x.SetGlobalProperty("Platform", "x64"), Times.AtLeastOnce); + sourceProjectManagerMock.Verify(x => x.Build(It.Is( env => env.GlobalProperties["Platform"] == "x64")), Times.AtLeastOnce); } [TestMethod] @@ -1716,7 +1717,7 @@ public void ShouldUseNormalizedSolutionConfigurationWhenGetMatchingFallsBack() target.ResolveSourceProjectInfos(options); // Assert: Buildalyzer receives the solution-normalized platform "x86", not the requested "x64" - analyzerManagerMock.Verify(x => x.SetGlobalProperty("Platform", "x86"), Times.AtLeastOnce); - analyzerManagerMock.Verify(x => x.SetGlobalProperty("Platform", "x64"), Times.Never); + sourceProjectManagerMock.Verify(x => x.Build(It.Is( env => env.GlobalProperties["Platform"] == "x86")), Times.AtLeastOnce); + sourceProjectManagerMock.Verify(x => x.Build(It.Is( env => env.GlobalProperties["Platform"] == "x64")), Times.Never); } } diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/ProjectOrchestratorTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/ProjectOrchestratorTests.cs index 4fcfa580af..ae829e178e 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/ProjectOrchestratorTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/ProjectOrchestratorTests.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading.Tasks; using Buildalyzer; +using Buildalyzer.Environment; using Microsoft.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -154,13 +155,13 @@ public async Task ShouldUseDesiredConfigurationWhenDefined() }; var csPathName = FileSystem.Path.Combine(ProjectPath, "someFile.cs"); - var sourceProjectAnalyzerMock = SourceProjectAnalyzerMock(csprojPathName, [csPathName]).Object; - var testProjectAnalyzerMock = TestProjectAnalyzerMock(testCsprojPathName, csprojPathName).Object; + var sourceProjectAnalyzerMock = SourceProjectAnalyzerMock(csprojPathName, [csPathName]); + var testProjectAnalyzerMock = TestProjectAnalyzerMock(testCsprojPathName, csprojPathName); // The analyzer finds two projects var analyzerResults = new Dictionary { - { "MyProject", sourceProjectAnalyzerMock }, - { "MyProject.UnitTests", testProjectAnalyzerMock } + { "MyProject", sourceProjectAnalyzerMock.Object }, + { "MyProject.UnitTests", testProjectAnalyzerMock.Object } }; var target = BuildProjectOrchestrator(analyzerResults, out var mockRunner, out var buildalyzerAnalyzerManagerMock); @@ -168,7 +169,7 @@ public async Task ShouldUseDesiredConfigurationWhenDefined() await target.MutateProjectsAsync(options, _reporterMock.Object, mockRunner.Object); // assert - buildalyzerAnalyzerManagerMock.Verify(x => x.SetGlobalProperty("Configuration", "Release"), Times.AtLeastOnce); + sourceProjectAnalyzerMock.Verify(x => x.Build(It.Is(env => env.GlobalProperties["Configuration"]=="Release")), Times.AtLeastOnce); } [TestMethodWithIgnoreIfSupport] @@ -302,7 +303,7 @@ public async Task ShouldFilterInSolutionMode() // act var result = async() => (await target.MutateProjectsAsync(options, _reporterMock.Object, mockRunner.Object)).ToList(); - + // assert result.ShouldThrow(); } diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/CollectionExpressionMutatorTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/CollectionExpressionMutatorTests.cs index 1415e412cd..0cee079fce 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/CollectionExpressionMutatorTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/CollectionExpressionMutatorTests.cs @@ -43,7 +43,7 @@ public void ShouldAddValueToEmptyCollectionExpression(string expression) var expressionSyntax = SyntaxFactory.ParseExpression(expression) as CollectionExpressionSyntax; var target = new CollectionExpressionMutator(); var result = target.ApplyMutations(expressionSyntax, null); - + var mutation = result.ShouldHaveSingleItem(); mutation.DisplayName.ShouldBe("Collection expression mutation"); var replacement = mutation.ReplacementNode.ShouldBeOfType(); @@ -151,7 +151,7 @@ namespace ExampleProject; class ClassName { public IEnumerable M() => Iter([ 1 ]); - + public IEnumerable Iter(IList list) { foreach (var l in list) { yield return l; @@ -168,25 +168,25 @@ namespace ExampleProject; class ClassName { public IEnumerable M() => Iter([ 1 ]); - + public IEnumerable Iter(IList list) { foreach (var l in list) { yield return l; } } - + public IEnumerable Iter(IReadOnlyCollection list) { foreach (var l in list) { yield return l; } } - + public IEnumerable Iter(T[] list) { foreach (var l in list) { yield return l; } } - + public IEnumerable Iter(ReadOnlyMemory list) { for (var i = 0; i < list.Length; i++) { yield return list.Span[i]; @@ -317,7 +317,7 @@ static ReadOnlySpan AsSpanConstants() { return [1, 2, 3]; // ok: span refers to assembly data section } - + static ReadOnlySpan AsSpan3(T x, T y, T z) { return (T[])[x, y, z]; // ok: span refers to T[] on heap @@ -340,7 +340,7 @@ class ClassName { public static void Method() { var a = AsArray([1, 2, 3]); // AsArray(int[]) var b = AsListOfArray([[4, 5], []]); // AsListOfArray(List) - + static T[] AsArray(T[] arg) => arg; static List AsListOfArray(List arg) => arg; } @@ -360,19 +360,19 @@ namespace ExampleProject; class ClassName { static void Generic(Span value) { } static void Generic(T[] value) { } - + static void SpanDerived(Span value) { } static void SpanDerived(object[] value) { } - + static void ArrayDerived(Span value) { } static void ArrayDerived(string[] value) { } - + // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-12.0/collection-expressions#overload-resolution public static void Method() { // Array initializers Generic(new[] { "" }); // string[] ArrayDerived(new[] { "" }); // string[] - + // Collection expressions Generic([""]); // Span SpanDerived([""]); // Span @@ -393,13 +393,13 @@ namespace ExampleProject; class ClassName { static void Generic(Span value) { } static void Generic(T[] value) { } - + static void SpanDerived(Span value) { } static void SpanDerived(object[] value) { } - + static void ArrayDerived(Span value) { } static void ArrayDerived(string[] value) { } - + // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-12.0/collection-expressions#syntax-ambiguities public static void Method() { Range range1 = default; @@ -423,13 +423,13 @@ namespace ExampleProject; class ClassName { static void Generic(Span value) { } static void Generic(T[] value) { } - + static void SpanDerived(Span value) { } static void SpanDerived(object[] value) { } - + static void ArrayDerived(Span value) { } static void ArrayDerived(string[] value) { } - + // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-12.0/collection-expressions#resolved-questions public static void Method() { void DoWork(IEnumerable values) { } diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InitialisationProcess.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InitialisationProcess.cs index 60cf74979b..dd8e189075 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InitialisationProcess.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InitialisationProcess.cs @@ -66,7 +66,7 @@ public IReadOnlyCollection GetMutableProjectsInfo(IStrykerOpt /// public void BuildProjects(IStrykerOptions options, IEnumerable projects) { - var solutionInfo = projects.First().SolutionInfo; + var solutionInfo = projects.First().TargetsForMutation; // pick configuration and platform from solution if available var configuration = solutionInfo?.Configuration ?? options.Configuration; var platform = solutionInfo?.Platform ?? options.Platform; diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index 6458b5e3f5..e34be83dd3 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -7,7 +7,6 @@ using System.Text; using System.Threading.Tasks; using Buildalyzer; -using Buildalyzer.Environment; using Microsoft.Extensions.Logging; using Stryker.Abstractions; using Stryker.Abstractions.Exceptions; @@ -30,32 +29,25 @@ public interface IInputFileResolver /// - Scans project under test and store files to mutate /// - Build composite for files /// -public class InputFileResolver : IInputFileResolver +public class InputFileResolver( + IFileSystem fileSystem, + IBuildalyzerProvider analyzerProvider, + INugetRestoreProcess nugetRestoreProcess, + ISolutionProvider solutionProvider, + ILogger logger) + : IInputFileResolver { private readonly string[] _foldersToExclude = ["obj", "bin", "node_modules", "StrykerOutput"]; - private readonly ILogger _logger; - private readonly IBuildalyzerProvider _analyzerProvider; - private readonly ISolutionProvider _solutionProvider; + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IBuildalyzerProvider _analyzerProvider = analyzerProvider ?? throw new ArgumentNullException(nameof(analyzerProvider)); + private readonly ISolutionProvider _solutionProvider = solutionProvider ?? throw new ArgumentNullException(nameof(solutionProvider)); private static readonly HashSet ImportantProperties = - ["Configuration", "Platform", "AssemblyName", "Configurations", "TargetPath"]; + ["Configuration", "Platform", "AssemblyName", "Configurations", "TargetPath", "OS"]; - private readonly INugetRestoreProcess _nugetRestoreProcess; + private readonly INugetRestoreProcess _nugetRestoreProcess = nugetRestoreProcess ?? throw new ArgumentNullException(nameof(nugetRestoreProcess)); private readonly Dictionary _buildLogs = []; - public InputFileResolver(IFileSystem fileSystem, - 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 IFileSystem FileSystem { get; } = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); /// /// Identifies the project(s) to mutate and their associated test project(s) according to provided options, and returns a collection of describing them. @@ -97,12 +89,13 @@ public IReadOnlyCollection ResolveSourceProjectInfos(IStryker } } + var solutionInfo = new TargetsForMutation(solution, options, _analyzerProvider ,_logger, _nugetRestoreProcess) { TargetFramework = options.TargetFramework }; if (options.IsSolutionContext) { - return ScanInSolutionMode(options, solution, normalizedProjectUnderTestNameFilter); + return FindProjectsInSolutionMode(options, solutionInfo, normalizedProjectUnderTestNameFilter); } - var result = SourceProjectInfos(options, solution, normalizedProjectUnderTestNameFilter); + var result = SourceProjectInfos(options, solutionInfo, normalizedProjectUnderTestNameFilter); if (result.Count<=1) { return result; @@ -122,32 +115,15 @@ public IReadOnlyCollection ResolveSourceProjectInfos(IStryker /// solution file if any /// name filter to apply to the mutated projects /// identified mutable projects matching the provided name filter (when provided). Can be empty - private List SourceProjectInfos(IStrykerOptions options, SolutionFile solution, + private List SourceProjectInfos(IStrykerOptions options, TargetsForMutation solution, string normalizedProjectUnderTestNameFilter) { - SolutionInfo solutionInfo = null; - var configuration = options.Configuration; - // "Any CPU" is the solution-level name; MSBuild requires "AnyCPU" - var platform = NormalizePlatform(options.Platform); - - // identify the target configuration and platform - if (solution != null) - { - var (actualBuildType, actualPlatform) = solution.GetMatching(options.Configuration, options.Platform); - _logger.LogDebug("Using solution configuration/platform '{Configuration}|{Platform}'.", actualBuildType, actualPlatform); - solutionInfo = new SolutionInfo(solution.FileName, actualBuildType, actualPlatform); - configuration = actualBuildType; - platform = NormalizePlatform(actualPlatform); - } - // we analyze the test project(s) and identify the project to be mutated var testProjectsSpecified = options.TestProjects.Any(); var testProjectFileNames = testProjectsSpecified ? options.TestProjects.Select(FindTestProject).ToList() : [FindTestProject(options.ProjectPath)]; _logger.LogInformation("Analyzing {ProjectCount} test project(s).", testProjectFileNames.Count); - List<(string projectFile, string framework, string configuration, string platform)> projectList = - [..testProjectFileNames.Select(p => (p, options.TargetFramework, configuration, platform))]; // if test project is provided but no source project => working directory should contain the project to mutate, we try to detect // it and use it as a filter for the analysis. This allows users to just cd into a project and run stryker var targetProjectMode = testProjectsSpecified && string.IsNullOrEmpty(options.SourceProjectName); @@ -165,13 +141,13 @@ private List SourceProjectInfos(IStrykerOptions options, Solu normalizedProjectUnderTestNameFilter = null; } } - + solution.AddProjects(testProjectFileNames); // we analyze test projects - var analyzeAllNeededProjects = AnalyzeAllNeededProjects(projectList, normalizedProjectUnderTestNameFilter, options, ScanMode.ScanTestProjectReferences); + var analyzeAllNeededProjects = AnalyzeAllNeededProjects(solution, normalizedProjectUnderTestNameFilter, options, ScanMode.ScanTestProjectReferences); // we match test projects to mutable projects var (findMutableAnalyzerResults, orphans) = FindMutableAnalyzerResults(analyzeAllNeededProjects); - var result = AnalyzeAndIdentifyProjects(options, solutionInfo, findMutableAnalyzerResults, orphans); + var result = AnalyzeAndIdentifyProjects(options, solution, findMutableAnalyzerResults, orphans); var mutableProjectsFound = result.Count; if (mutableProjectsFound == 1) { @@ -203,38 +179,24 @@ private List SourceProjectInfos(IStrykerOptions options, Solu return result; } - private IReadOnlyCollection ScanInSolutionMode(IStrykerOptions options, SolutionFile solution, + private IReadOnlyCollection FindProjectsInSolutionMode(IStrykerOptions options, TargetsForMutation solutionInfo, string normalizedProjectUnderTestNameFilter) { _logger.LogInformation("Stryker will mutate solution {Solution}.", FileSystem.Path.GetFileNameWithoutExtension(options.SolutionPath)); - // identify actual configuration/platform to use - var (actualBuildType, actualPlatform) = solution!.GetMatching(options.Configuration, options.Platform); - if ((!string.IsNullOrEmpty(options.Configuration) && options.Configuration != actualBuildType) || - (!string.IsNullOrEmpty(options.Platform) && options.Platform != actualPlatform)) - { - _logger.LogWarning("Using configuration/platform '{ActualBuildType}|{ActualPlatform}' instead of requested '{Configuration}|{Platform}'.", - actualBuildType, actualPlatform, options.Configuration, options.Platform); - } - else - { - _logger.LogInformation("Using configuration/platform '{Configuration}|{Platform}'.", actualBuildType, actualPlatform); - } - _logger.LogInformation("Identifying projects to mutate in {Solution}. This can take a while.", options.SolutionPath); - var solutionInfo = new SolutionInfo(solution.FileName, actualBuildType, actualPlatform); // analyze all projects - var projectsWithDetails = solution.GetProjectsWithDetails(actualBuildType, actualPlatform) - .Select(p => (p.file, options.TargetFramework, p.buildType, NormalizePlatform(p.platform))).ToList(); - - _logger.LogDebug("Analyzing {0} projects.", projectsWithDetails.Count); + solutionInfo.SelectAllProjects(); + _logger.LogDebug("Analyzing {0} projects.", solutionInfo.ProjectCount); // we match test projects to mutable projects - var mutableProjectsAnalyzerResults = AnalyzeAllNeededProjects(projectsWithDetails, + var mutableProjectsAnalyzerResults = AnalyzeAllNeededProjects(solutionInfo, normalizedProjectUnderTestNameFilter, options, ScanMode.NoScan); - var (findMutableAnalyzerResults, orphanedProjects) = FindMutableAnalyzerResults(mutableProjectsAnalyzerResults); + var (findMutableAnalyzerResults, orphanedProjects) = + FindMutableAnalyzerResults(mutableProjectsAnalyzerResults); + solutionInfo.SelectAllProjects(); return AnalyzeAndIdentifyProjects(options, solutionInfo, findMutableAnalyzerResults, orphanedProjects); } @@ -254,7 +216,7 @@ private enum ScanMode // analyze projects, do same for their upstream dependencies if activated, and identify which one(s) // to proceed with private List AnalyzeAndIdentifyProjects(IStrykerOptions options, - SolutionInfo solutionInfo, + TargetsForMutation solutionInfo, Dictionary> findMutableAnalyzerResults, List unusedTestProjects) { @@ -344,13 +306,13 @@ private void LogAnalysis(Dictionary> find } private ConcurrentBag<(IEnumerable result, bool isTest)> AnalyzeAllNeededProjects( - List<(string projectFile, string framework, string configuration, string platform)> projects, string normalizedProjectUnderTestNameFilter, + TargetsForMutation solutionInfo, + string normalizedProjectUnderTestNameFilter, IStrykerOptions options, ScanMode mode) { var mutableProjectsAnalyzerResults = new ConcurrentBag<(IEnumerable result, bool isTest)>(); - var list = new DynamicEnumerableQueue<(string projectFile, string framework, string configuration, string platform)>(projects); - const string Configuration = "Configuration"; - const string Platform = "Platform"; + + var list = new DynamicEnumerableQueue(solutionInfo.SelectedProjects); try { var parallelOptions = new ParallelOptions @@ -360,29 +322,13 @@ private void LogAnalysis(Dictionary> find Parallel.ForEach(list.Consume(), parallelOptions, entry => { - var logger = new StringWriter(); - var manager = _analyzerProvider.Provide(new AnalyzerManagerOptions { LogWriter = logger }); - - // specify configuration if any provided - if (!string.IsNullOrEmpty(entry.configuration)) - { - manager.SetGlobalProperty(Configuration, entry.configuration); - } - - // specify platform if any provided - if (!string.IsNullOrEmpty(entry.platform)) - { - manager.SetGlobalProperty(Platform, entry.platform); - } - - var buildResult = AnalyzeThisProject(manager.GetProject(entry.projectFile), - entry.framework, + var buildResult = AnalyzeThisProject( + solutionInfo.GetProjectAnalysisContext(entry), normalizedProjectUnderTestNameFilter, - logger, options, mutableProjectsAnalyzerResults); // scan references if recursive scan is enabled - ScanReferences(mode, buildResult).ForEach(p => list.Add((p, entry.framework, options.Configuration, entry.platform))); + ScanReferences(mode, buildResult).ForEach(p => list.Add(p)); } ); } @@ -395,18 +341,16 @@ private void LogAnalysis(Dictionary> find return mutableProjectsAnalyzerResults; } - private IEnumerable AnalyzeThisProject(IProjectAnalyzer project, - string framework, + private IEnumerable AnalyzeThisProject(ProjectAnalyzerContext project, string normalizedProjectUnderTestNameFilter, - StringWriter buildLogger, IStrykerOptions options, ConcurrentBag<(IEnumerable result, bool isTest)> mutableProjectsAnalyzerResults) { - IEnumerable buildResult = AnalyzeSingleProject(project, buildLogger, options); + IEnumerable buildResult = AnalyzeSingleProject(project, options); if (!buildResult.Any()) { - mutableProjectsAnalyzerResults.Add((buildResult, false)); // analysis failed + mutableProjectsAnalyzerResults.Add((buildResult, false)); return buildResult; } @@ -414,12 +358,12 @@ private IEnumerable AnalyzeThisProject(IProjectAnalyzer project if (isTestProject) { // filter frameworks for test projects (if one is selected) - buildResult = [SelectAnalyzerResult(buildResult, framework)]; + buildResult = [SelectAnalyzerResult(buildResult, options.TargetFramework)]; } // apply project name filter (except for test projects) if (isTestProject || normalizedProjectUnderTestNameFilter == null || - project.ProjectFile.Path.Replace('\\', '/') + project.ProjectFileName.Replace('\\', '/') .Contains(normalizedProjectUnderTestNameFilter, StringComparison.InvariantCultureIgnoreCase)) { @@ -451,24 +395,14 @@ private List ScanReferences(ScanMode mode, IEnumerable return referencesToAdd; } - private IAnalyzerResults AnalyzeSingleProject(IProjectAnalyzer project, StringWriter buildLogger, IStrykerOptions options) + private IAnalyzerResults AnalyzeSingleProject(ProjectAnalyzerContext project, IStrykerOptions options) { - var projectLogName = FileSystem.Path.GetRelativePath(options.WorkingDirectory, project.ProjectFile.Path); + var projectLogName = FileSystem.Path.GetRelativePath(options.WorkingDirectory, project.ProjectFileName); _logger.LogDebug("Analyzing {ProjectFilePath}", projectLogName); - var env = new EnvironmentOptions(); - if (!string.IsNullOrEmpty(options.MsBuildPath)) - { - // we need to forward this path to buildalyzer - env.EnvironmentVariables[EnvironmentVariables.MSBUILD_EXE_PATH] = options.MsBuildPath; - } - - var buildResult = project.Build(env); - _buildLogs[projectLogName] = buildLogger.ToString(); - buildLogger.GetStringBuilder().Clear(); - - var projectFileTargetFrameworks = GetTargetFrameworks(project, options, projectLogName, buildResult); - var buildResultOverallSuccess = buildResult.IsValidFor(projectFileTargetFrameworks); + var buildResult = project.Analyze(); + _buildLogs[projectLogName] = project.LastBuildLog; + var buildResultOverallSuccess = project.HasValidResults(); // retry if it failed if (!buildResultOverallSuccess) @@ -479,11 +413,10 @@ private IAnalyzerResults AnalyzeSingleProject(IProjectAnalyzer project, StringWr } // if this is a full framework project, we can retry after a nuget restore - buildResult = RetryBuild(project, options, projectLogName, buildResult, out buildResultOverallSuccess); + buildResult = RetryBuild(project, options, out buildResultOverallSuccess); // store the build log - _buildLogs[projectLogName] = buildLogger.ToString(); + _buildLogs[projectLogName] = project.LastBuildLog.ToString(); // clear the log - buildLogger.GetStringBuilder().Clear(); } LogAnalyzerResult(buildResult, options); @@ -494,11 +427,9 @@ private IAnalyzerResults AnalyzeSingleProject(IProjectAnalyzer project, StringWr } // log failure details - var failedFrameworks = projectFileTargetFrameworks.Where(tf => - !buildResult.Any(br => br.IsValidFor(tf))).ToList(); _logger.LogWarning( "Analysis of project {ProjectFilePath} failed for frameworks {FrameworkList}.", - projectLogName, string.Join(',', failedFrameworks)); + projectLogName, string.Join(',', project.FailedFrameworks)); if (options.DiagMode) { @@ -508,56 +439,13 @@ private IAnalyzerResults AnalyzeSingleProject(IProjectAnalyzer project, StringWr return buildResult; } - // get the list of target frameworks - private string[] GetTargetFrameworks(IProjectAnalyzer project, IStrykerOptions options, string projectLogName, - IAnalyzerResults buildResult) - { - var projectFileTargetFrameworks = project.ProjectFile.TargetFrameworks; - if (projectFileTargetFrameworks.Length > 0) - { - _logger.LogDebug("Project {ProjectFilePath} supported frameworks: {FrameworkList}.", projectLogName, string.Join(',', projectFileTargetFrameworks)); - } - else - { - if (!string.IsNullOrEmpty(options.TargetFramework)) - { - _logger.LogWarning("Failed to identify target frameworks for project {ProjectFilePath}. Assuming selected framework ({framework}) is present.", projectLogName, options.TargetFramework); - projectFileTargetFrameworks=[options.TargetFramework]; - } - else - { - projectFileTargetFrameworks = buildResult.Select(br => br.TargetFramework).ToArray(); - _logger.LogWarning("Failed to identify target frameworks for project {ProjectFilePath}. Using analysis results: {frameworks}", projectLogName, string.Join(',', projectFileTargetFrameworks)); - } - } - - return projectFileTargetFrameworks; - } - private IAnalyzerResults RetryBuild(IProjectAnalyzer project, IStrykerOptions options, string projectLogName, - IAnalyzerResults buildResult, out bool buildResultOverallSuccess) + private IAnalyzerResults RetryBuild(ProjectAnalyzerContext analyzer, IStrykerOptions options, out bool buildResultOverallSuccess) { - if (Environment.OSVersion.Platform == PlatformID.Win32NT && buildResult.Any(r => !r.IsValid() && r.TargetsFullFramework())) - { - _logger.LogWarning("Project {projectFilePath} analysis failed. Stryker will retry after a nuget restore.", projectLogName); - - if (options.DiagMode) - { - _logger.LogWarning("The MsBuild log is below."); - _logger.LogInformation(_buildLogs[projectLogName]); - } - - _nugetRestoreProcess.RestorePackages(options.SolutionPath, options.MsBuildPath ?? buildResult.First().MsBuildPath()); - } - var buildOptions = new EnvironmentOptions - { - Restore = true - }; - // retry the analysis - buildResult = project.Build(buildOptions); + var buildResult = analyzer.Analyze(withRestore: true); // check the new status - buildResultOverallSuccess = buildResult.IsValidFor(project.ProjectFile.TargetFrameworks); + buildResultOverallSuccess = buildResult.HasValidResult(); if (buildResultOverallSuccess || string.IsNullOrEmpty(options.TargetFramework)) { @@ -565,7 +453,7 @@ private IAnalyzerResults RetryBuild(IProjectAnalyzer project, IStrykerOptions op } // still failed, we can try using target framework option - buildResult = project.Build(options.TargetFramework); + buildResult = analyzer.Analyze(forceFramework: true); buildResultOverallSuccess = buildResult.Any( br => br.IsValidFor(options.TargetFramework)); return buildResult; @@ -580,7 +468,7 @@ private void LogAnalyzerResult(IAnalyzerResults analyzerResults, IStrykerOptions } if (analyzerResults.Count == 0) { - _logger.LogTrace("No analyzer results to log. This indicates an early failure in analysis, check log file for details."); + _logger.LogTrace("No analyzer results to log. This indicates an early failure in analysis, check build log for details."); return; } var log = new StringBuilder(); @@ -592,6 +480,7 @@ private void LogAnalyzerResult(IAnalyzerResults analyzerResults, IStrykerOptions { log.AppendLine($"TargetFramework: {analyzerResult.TargetFramework}"); log.AppendLine($"Succeeded: {analyzerResult.Succeeded}"); + log.AppendLine($"Compiler command: {analyzerResult.Command}"); var properties = analyzerResult.Properties; foreach (var property in ImportantProperties) @@ -710,7 +599,8 @@ IAnalyzerResult PickFrameworkVersion() return (mutableToTestMap, unusedTestProjects); } - private static bool ScanProjectReferences(Dictionary> mutableToTestMap, IAnalyzerResult[] mutableProjects, IAnalyzerResult testProject) + private static bool ScanProjectReferences(Dictionary> mutableToTestMap, + IAnalyzerResult[] mutableProjects, IAnalyzerResult testProject) { var mutableProject = mutableProjects.FirstOrDefault(p => testProject.ProjectReferences.Contains(p.ProjectFilePath)); if (mutableProject == null) @@ -726,7 +616,8 @@ private static bool ScanProjectReferences(Dictionary> mutableToTestMap, IAnalyzerResult[] mutableProjects, IAnalyzerResult testProject) + private static bool ScanAssemblyReferences(Dictionary> mutableToTestMap, + IAnalyzerResult[] mutableProjects, IAnalyzerResult testProject) { var foundOneProject = false; // we identify which projects are referenced by it @@ -760,7 +651,7 @@ private static bool ScanAssemblyReferences(Dictionarytest project(s) buildalyzer result(s) /// private SourceProjectInfo BuildSourceProjectInfo(IStrykerOptions options, - SolutionInfo solutionInfo, + TargetsForMutation solutionInfo, IAnalyzerResult analyzerResult, IEnumerable analyzerResults) { @@ -777,22 +668,16 @@ private SourceProjectInfo BuildSourceProjectInfo(IStrykerOptions options, "Mutation testing of F# projects is not ready yet. No mutants will be generated.")); } - ProjectComponentsBuilder builder; - if (language == Language.Csharp) - { - builder = new CsharpProjectComponentsBuilder(targetProjectInfo, options, _foldersToExclude, _logger, - FileSystem); - } - else - { - throw new NotSupportedException($"Language not supported: {language}"); - } + ProjectComponentsBuilder builder = language == Language.Csharp + ? new CsharpProjectComponentsBuilder(targetProjectInfo, options, _foldersToExclude, _logger, + FileSystem) + : throw new NotSupportedException($"Language not supported: {language}"); var inputFiles = builder.Build(); builder.InjectHelpers(inputFiles); targetProjectInfo.OnProjectBuilt = builder.PostBuildAction(); targetProjectInfo.ProjectContents = inputFiles; - targetProjectInfo.SolutionInfo = solutionInfo; + targetProjectInfo.TargetsForMutation = solutionInfo; _logger.LogInformation("Found project {ProjectFileName} to mutate.", analyzerResult.ProjectFilePath); targetProjectInfo.TestProjectsInfo = new TestProjectsInfo(FileSystem) { diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs new file mode 100644 index 0000000000..6a36d48be5 --- /dev/null +++ b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs @@ -0,0 +1,158 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Buildalyzer; +using Buildalyzer.Environment; +using Microsoft.Extensions.Logging; +using Stryker.Abstractions.Exceptions; +using Stryker.Abstractions.Options; +using Stryker.Utilities.Buildalyzer; + +namespace Stryker.Core.Initialisation; + +public class ProjectAnalyzerContext +{ + private readonly IProjectAnalyzer _analyzer; + private readonly TargetsForMutation _targetsForMutation; + private readonly IStrykerOptions _options; + private readonly string _configuration; + private readonly string _platform; + private readonly string _framework; + private readonly ILogger _logger; + private readonly StringWriter _buildLogger; + private IAnalyzerResults _analyzerLastResults; + private string[] _targetFrameworks; + + public ProjectAnalyzerContext(IBuildalyzerProvider buildalyzerProvider, + string projectFile, + IStrykerOptions options, + string configuration, + string platform, + string framework, + ILogger logger, + TargetsForMutation targetsForMutation) + { + _buildLogger = new StringWriter(); + var manager = buildalyzerProvider.Provide(new AnalyzerManagerOptions{LogWriter = _buildLogger}); + var analyzer = manager.GetProject(projectFile); + + _analyzer = analyzer; + ProjectFileName = projectFile; + _targetsForMutation = targetsForMutation; + _options = options; + _configuration = configuration; + _platform = platform; + _framework = framework; + _logger = logger; + } + + public string LastBuildLog => _buildLogger.ToString(); + + public string ProjectFileName { get; } + + public IAnalyzerResults Analyze(bool withRestore = false, bool forceFramework = false) + { + if (withRestore && _analyzerLastResults?.Any(ar => ar.TargetsFullFramework()) == true) + { + _targetsForMutation.RestoreSolution(_analyzerLastResults); + } + _buildLogger.GetStringBuilder().Clear(); + var env = new EnvironmentOptions + { + Restore = withRestore + }; + if (!string.IsNullOrEmpty(_options.MsBuildPath)) + { + // we need to forward this path to buildalyzer + env.EnvironmentVariables[EnvironmentVariables.MSBUILD_EXE_PATH] = _options.MsBuildPath; + } + if (!string.IsNullOrEmpty(_configuration)) + { + env.GlobalProperties["Configuration"] = _configuration; + } + if (!string.IsNullOrEmpty(_platform)) + { + env.GlobalProperties["Platform"] = _platform; + } + + _analyzerLastResults = forceFramework ? _analyzer.Build(_options.TargetFramework, env) : _analyzer.Build(env); + InitializeTargetFrameworks(); + return _analyzerLastResults; + } + + private void InitializeTargetFrameworks() + { + var projectFileTargetFrameworks = _analyzer.ProjectFile.TargetFrameworks; + if (projectFileTargetFrameworks.Length > 0) + { + _logger.LogDebug("Project {ProjectFilePath} supported frameworks: {FrameworkList}.", ProjectFileName, string.Join(',', projectFileTargetFrameworks)); + } + else + { + if (!string.IsNullOrEmpty(_options.TargetFramework)) + { + projectFileTargetFrameworks=[_options.TargetFramework]; + _logger.LogWarning("Failed to identify target frameworks for project {ProjectFilePath}. Assuming selected framework ({framework}) is present.", ProjectFileName, _options.TargetFramework); + } + else + { + projectFileTargetFrameworks = _analyzerLastResults.Select(br => br.TargetFramework).ToArray(); + _logger.LogWarning("Failed to identify target frameworks for project {ProjectFilePath}. Using analysis results: {frameworks}", ProjectFileName, string.Join(',', projectFileTargetFrameworks)); + } + } + + _targetFrameworks = projectFileTargetFrameworks; + } + + public IEnumerable FailedFrameworks => _targetFrameworks.Where(tf => + !_analyzerLastResults.Any( ar => ar.TargetFramework == tf && ar.IsValid())); + + public bool HasValidResults() => _analyzerLastResults != null && _analyzerLastResults.IsValidFor(_targetFrameworks); + + private IAnalyzerResult SelectAnalyzerResult(IEnumerable analyzerResults, string targetFramework) + { + var validResults = analyzerResults.ToList(); + if (validResults.Count == 0) + { + throw new InputException($"No valid project analysis results could be found for '{ProjectFileName}'."); + } + + if (targetFramework is null) + { + // we try to avoid desktop versions + return PickFrameworkVersion(); + } + + var resultForRequestedFramework = validResults.Find(a => a.TargetFramework == targetFramework); + if (resultForRequestedFramework is not null) + { + return resultForRequestedFramework; + } + // if there is only one available framework version, we log an info + if (validResults.Count == 1) + { + var singleAnalyzerResult = validResults[0]; + _logger.LogInformation( + "Could not find a valid analysis for target {0} for project '{1}'. Selected version is {2}.", + targetFramework, ProjectFileName, singleAnalyzerResult.TargetFramework); + return singleAnalyzerResult; + } + + var firstAnalyzerResult = PickFrameworkVersion(); + var availableFrameworks = validResults.Select(a => a.TargetFramework).Distinct(); + _logger.LogWarning( + """ + Could not find a valid analysis for target {0} for project '{1}'. + The available target frameworks are: {2}. + selected version is {3}. + """, targetFramework, ProjectFileName, string.Join(',', availableFrameworks), firstAnalyzerResult.TargetFramework); + + return firstAnalyzerResult; + + IAnalyzerResult PickFrameworkVersion() + { + return validResults.Find(a => a.IsValid() && !a.TargetsFullFramework()) ?? validResults[0]; + } + } + +} diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs b/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs new file mode 100644 index 0000000000..545274dfe5 --- /dev/null +++ b/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Buildalyzer; +using Microsoft.Extensions.Logging; +using Stryker.Abstractions.Options; +using Stryker.Solutions; +using Stryker.Utilities.Buildalyzer; + +namespace Stryker.Core.Initialisation; + +public class TargetsForMutation +{ + private List _selectedProjects = []; + private bool _solutionRestored; + + private readonly IStrykerOptions _options; + private readonly INugetRestoreProcess _nugetRestoreProcess; + private readonly IBuildalyzerProvider _buildalyzerProvider; + private readonly ILogger _logger; + + public TargetsForMutation(SolutionFile file, IStrykerOptions options, IBuildalyzerProvider buildalyzerProvider ,ILogger logger, INugetRestoreProcess nugetRestoreProcess) + { + _options = options; + _buildalyzerProvider = buildalyzerProvider; + _logger = logger; + _nugetRestoreProcess = nugetRestoreProcess; + Solution = file; + SelectConfiguration(); + } + + private SolutionFile Solution { get; init; } + + public string SolutionFilePath => Solution?.FileName; + + public string Configuration { get; private set; } + + public string Platform { get; private set; } + + public string TargetFramework { get; set; } + + public int ProjectCount => _selectedProjects.Count; + + public List SelectedProjects => _selectedProjects; + + public void AddProjects(IEnumerable projectFiles) => _selectedProjects.AddRange(projectFiles); + + private void SelectConfiguration() + { + var configuration = _options.Configuration; + var platform = _options.Platform; + if (Solution != null) + { + // we use the solution to determine the configuration and platform to use, as the project files may not contain all configurations and platforms that are defined in the solution + (Configuration, Platform) = Solution.GetMatching(configuration, platform); + _logger.LogDebug("Using solution configuration/platform '{Configuration}|{Platform}'.", Configuration, Platform); + if ((!string.IsNullOrEmpty(configuration) && configuration != Configuration) || + (!string.IsNullOrEmpty(platform) && platform != Platform)) + { + _logger.LogWarning("Using solution configuration/platform '{ActualBuildType}|{ActualPlatform}' instead of requested '{Configuration}|{Platform}'.", + Configuration, Platform, configuration, platform); + } + else + { + _logger.LogInformation("Using solution configuration/platform '{Configuration}|{Platform}'.", Configuration, Platform); + } + } + else + { + Configuration = configuration; + // "Any CPU" is default platform at solution level, but in project files it is "AnyCPU", so we need to convert it to match the project files + Platform = platform == "Any CPU" ? "AnyCPU" : platform; + _logger.LogDebug("Using project configuration/platform '{Configuration}|{Platform}'.", Configuration, Platform); + } + } + + /// + /// Select all projects from solution + /// + public void SelectAllProjects() => _selectedProjects = Solution?.GetProjects(Configuration, Platform).ToList() ?? []; + + // the method is at solution level because it needs only be called once + internal void RestoreSolution(IAnalyzerResults results) + { + if (_solutionRestored) + { + return; + } + + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + { + _logger.LogWarning("Project analysis failed. Stryker will retry after a solution level nuget restore"); + _nugetRestoreProcess.RestorePackages(_options.SolutionPath, _options.MsBuildPath ?? results.First().MsBuildPath()); + } + + _solutionRestored = true; + } + + public ProjectAnalyzerContext GetProjectAnalysisContext(string projectFile) + { + string configuration; + string platform; + if (Solution != null) + { + (configuration, platform) = + Solution.GetProjectConfiguration(projectFile, Configuration, Platform); + } + else + { + (configuration, platform) = (Configuration, Platform); + } + return new ProjectAnalyzerContext(_buildalyzerProvider, projectFile, _options, configuration, platform, _options.TargetFramework, _logger, this); + } +} diff --git a/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/SourceProjectInfo.cs b/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/SourceProjectInfo.cs index ff1773cfa0..46e4af0488 100644 --- a/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/SourceProjectInfo.cs +++ b/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/SourceProjectInfo.cs @@ -3,25 +3,19 @@ using Buildalyzer; using Stryker.Abstractions; using Stryker.Abstractions.ProjectComponents; +using Stryker.Core.Initialisation; using Stryker.Core.InjectedHelpers; using Stryker.Utilities.Buildalyzer; namespace Stryker.Core.ProjectComponents.SourceProjects; -public class SolutionInfo(string file, string configuration, string platform) -{ - public string SolutionFilePath { get; init; } = file; - public string Configuration { get; init; } = configuration; - public string Platform { get; init; } = platform; -} - public class SourceProjectInfo : IProjectAndTests { private readonly List _warnings = []; public Action OnProjectBuilt { get; set; } - public SolutionInfo SolutionInfo { get; set; } + public TargetsForMutation TargetsForMutation { get; set; } public IAnalyzerResult AnalyzerResult { get; init; } diff --git a/src/Stryker.Solutions.Test/SolutionFileShould.cs b/src/Stryker.Solutions.Test/SolutionFileShould.cs index 7e25d6f485..4440e4bfa8 100644 --- a/src/Stryker.Solutions.Test/SolutionFileShould.cs +++ b/src/Stryker.Solutions.Test/SolutionFileShould.cs @@ -128,7 +128,7 @@ public void ProvideProjectListForGivenConfigurationOnSolutionWithMultiplePlatfor Path.Combine("MicrosoftTestPlatform", "UnitTests.TUnit", "UnitTests.TUnit.csproj"), }; solution.GetProjects("Debug").ShouldBe(expectedProjects, ignoreOrder: true); - + var projectsWithDetails = solution.GetProjectsWithDetails("Debug").ToList(); projectsWithDetails.Select(x => x.file).ShouldBe(expectedProjects, ignoreOrder: true); projectsWithDetails.All(x => x.buildType == "Debug").ShouldBeTrue(); @@ -165,7 +165,7 @@ public void FallBackOnAnyCPU() var match = solution.GetMatching("Debug", "x64"); // Assert - match.ShouldBe(("Debug", "AnyCPU")); + match.ShouldBe(("Debug", "Any CPU")); } [TestMethod] @@ -176,6 +176,6 @@ public void PickFirstIfNoMatch() var match = solution.GetMatching("Stryker", "x64"); // Assert - match.ShouldBe(("Debug", "AnyCPU")); + match.ShouldBe(("Debug", "Any CPU")); } } diff --git a/src/Stryker.Solutions/SolutionFile.cs b/src/Stryker.Solutions/SolutionFile.cs index b1991f6d00..066e73b738 100644 --- a/src/Stryker.Solutions/SolutionFile.cs +++ b/src/Stryker.Solutions/SolutionFile.cs @@ -23,11 +23,10 @@ public class SolutionFile public HashSet GetBuildTypes() => _configurations.Keys.Select(x => x.buildType).ToHashSet(); - public string FileName { get; init; } = string.Empty; + public string FileName { get; private init; } = string.Empty; private HashSet GetPlatforms() => _configurations.Keys.Select(x => x.platform).ToHashSet(); - private string DefaultPlatform { get @@ -162,6 +161,20 @@ private string GetEffectiveBuildType(string? buildType) .ToImmutableList(); } + public (string configuration, string platform) GetProjectConfiguration(string filename, string configuration, string platform) + { + configuration = GetEffectiveBuildType(configuration); + platform ??= DefaultPlatform; + foreach (var entry in _configurations) + { + if (entry.Value.TryGetValue(filename, out var projectConfig)) + { + return (projectConfig.buildType, projectConfig.platform); + } + } + return (configuration, platform); + } + /// /// Create a solution file from a list of projects having two build types, Debug and Release, and the provided platforms. /// @@ -172,7 +185,7 @@ private string GetEffectiveBuildType(string? buildType) public static SolutionFile BuildFromProjectList(List projects, string[]? platforms = null) { var result = new SolutionFile(); - platforms ??= [ DefaultSolutionType, "x86" ]; + platforms ??= [ "AnyCPU", "x86" ]; // default to Debug|Any CPU string[] buildTypes = ["Debug", "Release"]; foreach (var buildType in buildTypes) @@ -184,7 +197,8 @@ public static SolutionFile BuildFromProjectList(List projects, string[]? { projectDict[project] = (buildType, platform); } - result._configurations.Add((buildType, platform), projectDict); + var solutionPlatform = platform == "AnyCPU" ? "Any CPU" : platform; + result._configurations.Add((buildType, solutionPlatform), projectDict); } } return result; From 1854661336e1b49ddbbbffc5f47ce93cf8d7ebe0 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Tue, 31 Mar 2026 07:41:45 +0200 Subject: [PATCH 04/41] chore: split inputsolver responsibilities in several classes for readibility --- .../Initialisation/InputFileResolverTests.cs | 74 ++- .../Initialisation/InputFileResolver.cs | 591 ++++++------------ .../Initialisation/MutableProjectTarget.cs | 101 +++ .../Initialisation/MutableProjectTree.cs | 84 +++ .../Initialisation/ProjectAnalyzerContext.cs | 130 +++- .../Initialisation/TargetsForMutation.cs | 9 +- 6 files changed, 562 insertions(+), 427 deletions(-) create mode 100644 src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTarget.cs create mode 100644 src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTree.cs diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InputFileResolverTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InputFileResolverTests.cs index e8e2297e1e..f3b7929813 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InputFileResolverTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InputFileResolverTests.cs @@ -1089,15 +1089,69 @@ public void ShouldSelectAvailableFramework_WhenDesiredNotFound(string targetFram } [TestMethod] - [DataRow("net3.0,net462", "net461,net2.0", null, "net3.0", "net2.0")] - [DataRow("net3.0,net2.0", "net2.0,net3.0", null, "net3.0", "net3.0")] - [DataRow("net3.0,net462", "net461,net2.0", "net2.0", "net3.0", "net2.0")] - [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 + [DataRow("netcoreapp3.0,net462", "netcoreapp2.0,net461", null, "netcoreapp3.0", "netcoreapp2.0")] + [DataRow("netcoreapp3.0,netcoreapp2.0", "netcoreapp3.0,netcoreapp2.0", null, "netcoreapp3.0", "netcoreapp3.0")] + [DataRow("netcoreapp3.0,net462", "netcoreapp2.0,net461", "netcoreapp2.0", "netcoreapp3.0", "netcoreapp2.0")] + [DataRow("netcoreapp3.0,net462", "netcoreapp2.0,net461", "netcoreapp3.0", "netcoreapp3.0", "netcoreapp2.0")] + [DataRow("netcoreapp3.0,net462", "netcoreapp2.0,net461", "net461", "net462", "net461")] + [DataRow("netcoreapp3.0,net462", "netcoreapp2.0,net461", "net462", "net462", "net461")] + public void ShouldSelectFrameworkBasedOnTestProject(string testFrameworks + , string projectFrameworks , string targetFramework - , string expectedTestFramework,string expectedFramework) + , 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)}, + { testProjectPath, new MockFileData(_defaultTestProjectFileContents)}, + { Path.Combine(_sourcePath, "Recursive.cs"), new MockFileData("content")} + }); + + var options = new StrykerOptions() + { + ProjectPath = basePath, + SourceProjectName = sourceProjectNameFilter, + TestProjects = new List { testProjectPath }, + TargetFramework = targetFramework + }; + + var sourceProjectManagerMock = SourceProjectAnalyzerMock(sourceProjectPath, + fileSystem.AllFiles.Where(s => s.EndsWith(".cs")).ToArray(), null, projectFrameworks.Split(',')); + var testProjectManagerMock = TestProjectAnalyzerMock(testProjectPath, sourceProjectPath, frameworks: testFrameworks.Split(',')); + + var analyzerResults = new Dictionary + { + { "MyProject", sourceProjectManagerMock.Object }, + { "MyProject.UnitTests", testProjectManagerMock.Object } + }; + BuildBuildAnalyzerMock(analyzerResults); + + var target = BuildTestResolver(fileSystem); + + // Act + var result = target.ResolveSourceProjectInfos(options).First(); + + // Assert + result.AnalyzerResult.TargetFramework.ShouldBe(expectedFramework); + result.TestProjectsInfo.AnalyzerResults.First().TargetFramework.ShouldBe(expectedTestFramework); + } + + // Stryker will ignore NetFramework target when runnning on non windows platforms + [TestMethodWithIgnoreIfSupport] + [IgnoreIf(nameof(Is.NotWindows))] + [DataRow("net462,netcoreapp3.0", "net461,netcoreapp2.0", null, "net462", "net461")] + public void ShouldSelectFrameworkBasedOnTestProjectOnWindows(string testFrameworks + , string projectFrameworks + , string targetFramework + , string expectedTestFramework, + string expectedFramework) { // Arrange var basePath = Path.Combine(_sourcePath, "ExampleProject"); @@ -1321,7 +1375,7 @@ public void ShouldThrowOnMultipleProjectsWithoutFilter() // Assert var ex = result.ShouldThrow(); - ex.Message.ShouldContain("Test project contains more than one project reference. Please set the project option"); + ex.Message.ShouldContain("Multiple projects identified as potential candidate for mutation testing. Please set the project option"); ex.Message.ShouldContain("Choose one of the following references:"); } @@ -1482,7 +1536,7 @@ public void ShouldThrowWhenTheNameMatchesMore(string shouldMatchMoreThanOne) var options = new StrykerOptions { SourceProjectName = shouldMatchMoreThanOne, ProjectPath = _testPath }; var result = () => target.ResolveSourceProjectInfos(options); - result.ShouldThrow().Message.ShouldContain("more than one project"); + result.ShouldThrow().Message.ShouldContain("Multiple projects identified"); } [TestMethod] diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index e34be83dd3..cae74d622b 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -8,11 +8,9 @@ using System.Threading.Tasks; using Buildalyzer; using Microsoft.Extensions.Logging; -using Stryker.Abstractions; using Stryker.Abstractions.Exceptions; using Stryker.Abstractions.Options; using Stryker.Core.ProjectComponents.SourceProjects; -using Stryker.Core.ProjectComponents.TestProjects; using Stryker.Solutions; using Stryker.Utilities.Buildalyzer; @@ -37,12 +35,9 @@ public class InputFileResolver( ILogger logger) : IInputFileResolver { - private readonly string[] _foldersToExclude = ["obj", "bin", "node_modules", "StrykerOutput"]; private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly IBuildalyzerProvider _analyzerProvider = analyzerProvider ?? throw new ArgumentNullException(nameof(analyzerProvider)); private readonly ISolutionProvider _solutionProvider = solutionProvider ?? throw new ArgumentNullException(nameof(solutionProvider)); - private static readonly HashSet ImportantProperties = - ["Configuration", "Platform", "AssemblyName", "Configurations", "TargetPath", "OS"]; private readonly INugetRestoreProcess _nugetRestoreProcess = nugetRestoreProcess ?? throw new ArgumentNullException(nameof(nugetRestoreProcess)); private readonly Dictionary _buildLogs = []; @@ -53,7 +48,7 @@ public class InputFileResolver( /// Identifies the project(s) to mutate and their associated test project(s) according to provided options, and returns a collection of describing them. /// /// Stryker options - /// a collection of describing mutable project + /// a collection of describing mutable project /// Thrown if the method fails during analysis. public IReadOnlyCollection ResolveSourceProjectInfos(IStrykerOptions options) { @@ -89,22 +84,31 @@ public IReadOnlyCollection ResolveSourceProjectInfos(IStryker } } - var solutionInfo = new TargetsForMutation(solution, options, _analyzerProvider ,_logger, _nugetRestoreProcess) { TargetFramework = options.TargetFramework }; - if (options.IsSolutionContext) - { - return FindProjectsInSolutionMode(options, solutionInfo, normalizedProjectUnderTestNameFilter); - } + var solutionInfo = new TargetsForMutation(solution, options, _analyzerProvider ,_logger, _nugetRestoreProcess) + { TargetFramework = options.TargetFramework }; + return options.IsSolutionContext ? FindProjectsInSolutionMode(options, solutionInfo, normalizedProjectUnderTestNameFilter) + : FindProjectInTargetProjectMode(options, solutionInfo, normalizedProjectUnderTestNameFilter); + } - var result = SourceProjectInfos(options, solutionInfo, normalizedProjectUnderTestNameFilter); - if (result.Count<=1) - { - return result; - } - // still ambiguous - var stringBuilder = new StringBuilder().AppendLine( - "Test project contains more than one project reference. Please set the project option (https://stryker-mutator.io/docs/stryker-net/configuration#project-file-name) to specify which project to mutate.") - .Append(BuildReferenceChoice(result.Select(p => p.AnalyzerResult.ProjectFilePath))); - throw new InputException(stringBuilder.ToString()); + private IReadOnlyCollection FindProjectsInSolutionMode(IStrykerOptions options, TargetsForMutation solutionInfo, + string normalizedProjectUnderTestNameFilter) + { + _logger.LogInformation("Identifying projects to mutate in {solution}. This can take a while.", + FileSystem.Path.GetFileNameWithoutExtension(solutionInfo.SolutionFilePath)); + + // analyze all projects + solutionInfo.SelectAllProjects(); + _logger.LogDebug("Analyzing {numProjects} projects.", solutionInfo.ProjectCount); + // we analyze every project in the solution + var mutableProjectsAnalyzerResults = AnalyzeAllNeededProjects(solutionInfo, + normalizedProjectUnderTestNameFilter, + options, + ScanMode.NoScan); + // we identify target projects and their associated test projects + var (findMutableAnalyzerResults, orphanedProjects) = + FindMutableAnalyzerResults(mutableProjectsAnalyzerResults); + // we keep only suitable candidates + return AnalyzeAndIdentifyProjects(options, solutionInfo, findMutableAnalyzerResults, orphanedProjects); } /// @@ -115,7 +119,7 @@ public IReadOnlyCollection ResolveSourceProjectInfos(IStryker /// solution file if any /// name filter to apply to the mutated projects /// identified mutable projects matching the provided name filter (when provided). Can be empty - private List SourceProjectInfos(IStrykerOptions options, TargetsForMutation solution, + private List FindProjectInTargetProjectMode(IStrykerOptions options, TargetsForMutation solution, string normalizedProjectUnderTestNameFilter) { // we analyze the test project(s) and identify the project to be mutated @@ -141,69 +145,69 @@ private List SourceProjectInfos(IStrykerOptions options, Targ normalizedProjectUnderTestNameFilter = null; } } + solution.AddProjects(testProjectFileNames); // we analyze test projects - var analyzeAllNeededProjects = AnalyzeAllNeededProjects(solution, normalizedProjectUnderTestNameFilter, options, ScanMode.ScanTestProjectReferences); + var analyzeAllNeededProjects = AnalyzeAllNeededProjects(solution, + normalizedProjectUnderTestNameFilter, options, ScanMode.ScanTestProjectReferences); // we match test projects to mutable projects var (findMutableAnalyzerResults, orphans) = FindMutableAnalyzerResults(analyzeAllNeededProjects); var result = AnalyzeAndIdentifyProjects(options, solution, findMutableAnalyzerResults, orphans); - var mutableProjectsFound = result.Count; - if (mutableProjectsFound == 1) - { - return result; - } + return SelectSingleProject(normalizedProjectUnderTestNameFilter, result, targetProjectMode, testProjectFileNames); + } - if (mutableProjectsFound == 0) + private List SelectSingleProject(string normalizedProjectUnderTestNameFilter, List result, bool targetProjectMode, + List testProjectFileNames) + { + switch (result.Count) { - if (targetProjectMode) + case 1: + return result; + case 0: { - _logger.LogError("Project {ProjectFile} could not be found as a project referenced by the provided test projects.", normalizedProjectUnderTestNameFilter); + if (targetProjectMode) + { + _logger.LogError("Project {ProjectFile} could not be found as a project referenced by the provided test projects.", normalizedProjectUnderTestNameFilter); + } + else + { + _logger.LogError("No project could be found as a project referenced by the provi ded test projects."); + } + + return result; } - else + default: { - _logger.LogError("No project could be found as a project referenced by the provided test projects."); - } - - return result; - } + // Too many references found + // look for one project that references all provided test projects + result = + [ + .. result.Where(p => + testProjectFileNames.TrueForAll(n => + p.TestProjectsInfo.TestProjects.Any(t => t.ProjectFilePath == n))) + ]; + if (result.Count == 1) + { + _logger.LogInformation( + "Selected project {ProjectFile} as it is referenced by all provided test projects.", + result[0].AnalyzerResult.ProjectFilePath); + return result; + } - // Too many references found - // look for one project that references all provided test projects - result = [.. result.Where(p => testProjectFileNames.TrueForAll(n => p.TestProjectsInfo.TestProjects.Any(t => t.ProjectFilePath == n)))]; - if (result.Count == 1) - { - _logger.LogInformation("Selected project {ProjectFile} as it is referenced by all provided test projects.", result[0].AnalyzerResult.ProjectFilePath); + // still ambiguous + var stringBuilder = new StringBuilder().AppendLine( + "Multiple projects identified as potential candidate for mutation testing. Please set the project option (https://stryker-mutator.io/docs/stryker-net/configuration#project-file-name) to specify which project to mutate.") + .Append(BuildReferenceChoice(result.Select(p => p.AnalyzerResult.ProjectFilePath))); + throw new InputException(stringBuilder.ToString()); + } } - - return result; - } - - private IReadOnlyCollection FindProjectsInSolutionMode(IStrykerOptions options, TargetsForMutation solutionInfo, - string normalizedProjectUnderTestNameFilter) - { - _logger.LogInformation("Stryker will mutate solution {Solution}.", FileSystem.Path.GetFileNameWithoutExtension(options.SolutionPath)); - _logger.LogInformation("Identifying projects to mutate in {Solution}. This can take a while.", options.SolutionPath); - - // analyze all projects - solutionInfo.SelectAllProjects(); - _logger.LogDebug("Analyzing {0} projects.", solutionInfo.ProjectCount); - // we match test projects to mutable projects - var mutableProjectsAnalyzerResults = AnalyzeAllNeededProjects(solutionInfo, - normalizedProjectUnderTestNameFilter, - options, - ScanMode.NoScan); - var (findMutableAnalyzerResults, orphanedProjects) = - FindMutableAnalyzerResults(mutableProjectsAnalyzerResults); - - solutionInfo.SelectAllProjects(); - return AnalyzeAndIdentifyProjects(options, solutionInfo, findMutableAnalyzerResults, orphanedProjects); } public string FindTestProject(string path) { var projectFile = FindProjectFile(path); - _logger.LogDebug("Using {0} as test project", projectFile); + _logger.LogDebug("Using {filename} as test project", projectFile); return projectFile; } @@ -217,29 +221,32 @@ private enum ScanMode // to proceed with private List AnalyzeAndIdentifyProjects(IStrykerOptions options, TargetsForMutation solutionInfo, - Dictionary> findMutableAnalyzerResults, - List unusedTestProjects) + List findMutableAnalyzerResults, + List unusedTestProjects) { // build all projects - _logger.LogDebug("Analyzing {Count} projects.", findMutableAnalyzerResults.Count); + _logger.LogDebug("Scanning {Count} possible targets.", findMutableAnalyzerResults.Count); - // we match test projects to mutable projects - if (findMutableAnalyzerResults.All(r => - !r.Key.IsValid() || r.Value.All(r2 => !r2.IsValid()))) + var suitableCandidates = + findMutableAnalyzerResults.Where(p => p.IsValidTarget).ToList(); + + // do we have at least one valid target? + if (suitableCandidates.Count == 0) { // no mutable project found LogAnalysis(findMutableAnalyzerResults, unusedTestProjects, options.DiagMode); throw new InputException("Failed to analyze project builds. Stryker cannot continue."); } + // we keep only on target framework per project + foreach (var candidate in suitableCandidates) + { + candidate.KeepOnlyOneTarget(options.TargetFramework); + } // keep only projects with one or more test projects - var analyzerResults = findMutableAnalyzerResults - .Where(p => p.Value.Count > 0) - .Select(p => p.Key).GroupBy(p => p.ProjectFilePath); // we must select projects according to framework settings if any - var projectInfos = analyzerResults - .Select(g => SelectAnalyzerResult(g, options.TargetFramework)) - .Select(analyzerResult => BuildSourceProjectInfo(options, solutionInfo, analyzerResult, findMutableAnalyzerResults[analyzerResult])) + var projectInfos = suitableCandidates.Where(p => p.Targets.Count > 0) + .Select(analyzerResult => analyzerResult.Targets.First().BuildSourceProjectInfo(options, solutionInfo, FileSystem)) .ToList(); if (projectInfos.Count != 0) @@ -252,51 +259,28 @@ private List AnalyzeAndIdentifyProjects(IStrykerOptions optio } // Log the analysis results - private void LogAnalysis(Dictionary> findMutableAnalyzerResults, - List unusedTestProjects, bool optionsDiagMode) + private void LogAnalysis(List findMutableAnalyzerResults, + List unusedTestProjects, bool optionsDiagMode) { if (findMutableAnalyzerResults.Count == 0) { - _logger.LogWarning(""" + _logger.LogWarning( optionsDiagMode ? "No project found, check settings and ensure project file is not corrupted.": + """ No project found, check settings and ensure project file is not corrupted. Use --diag option to have the analysis logs in the log file. """); return; } - foreach (var (mutableProject, testProjects) in findMutableAnalyzerResults) + foreach (var projectTree in findMutableAnalyzerResults) { - _logger.LogInformation("Project {ProjectPath} analysis {Result}.", - mutableProject.ProjectFilePath, - mutableProject.IsValid() ? "succeeded" : "failed hence can't be mutated"); - if (testProjects.Count == 0) - { - _logger.LogWarning(" can't be mutated because no test project references it. If this is a test project, " + - "ensure it has the property: true in its project file."); - continue; - } - // dump associated test projects - foreach (var testProject in testProjects) - { - _logger.LogInformation(" referenced by test project {ProjectName}, analysis {Result}.", - testProject.ProjectFilePath, - testProject.IsValid() ? "succeeded" : "failed"); - } - // provide synthetic status - if (testProjects.Any(r => r.IsValid())) - { - _logger.LogInformation(" can be mutated."); - } - else - { - _logger.LogWarning(" can't be mutated because all referencing test projects' analysis failed."); - } + projectTree.DumpForAnalysis(); } // dump test projects that do not reference any mutable project foreach (var unusedTestProject in unusedTestProjects) { _logger.LogInformation("Test project {ProjectName} does not appear to test any mutable project, analysis {Result}.", - unusedTestProject.ProjectFilePath, - unusedTestProject.IsValid() ? "succeeded" : "failed"); + unusedTestProject.ProjectFileName, + unusedTestProject.HasValidResults() ? "succeeded" : "failed"); } if (!optionsDiagMode) @@ -305,12 +289,12 @@ private void LogAnalysis(Dictionary> find } } - private ConcurrentBag<(IEnumerable result, bool isTest)> AnalyzeAllNeededProjects( + private ConcurrentBag AnalyzeAllNeededProjects( TargetsForMutation solutionInfo, string normalizedProjectUnderTestNameFilter, IStrykerOptions options, ScanMode mode) { - var mutableProjectsAnalyzerResults = new ConcurrentBag<(IEnumerable result, bool isTest)>(); + var mutableProjectsAnalyzerResults = new ConcurrentBag(); var list = new DynamicEnumerableQueue(solutionInfo.SelectedProjects); try @@ -322,13 +306,32 @@ private void LogAnalysis(Dictionary> find Parallel.ForEach(list.Consume(), parallelOptions, entry => { - var buildResult = AnalyzeThisProject( - solutionInfo.GetProjectAnalysisContext(entry), - normalizedProjectUnderTestNameFilter, - options, - mutableProjectsAnalyzerResults); + var projectAnalysisContext = solutionInfo.GetProjectAnalysisContext(entry); + + IEnumerable buildResult = AnalyzeSingleProject(projectAnalysisContext, options); + + // apply project name filter (except for test projects) + if (!(normalizedProjectUnderTestNameFilter == null + || buildResult.IsTestProject() + || projectAnalysisContext.ProjectFileName.Replace('\\', '/') + .Contains(normalizedProjectUnderTestNameFilter, + StringComparison.InvariantCultureIgnoreCase))) + { + return; + } + + mutableProjectsAnalyzerResults.Add(projectAnalysisContext); + if (mode == ScanMode.NoScan + || (mode == ScanMode.ScanTestProjectReferences && !projectAnalysisContext.IsTestProject())) + { + return; + } + // scan references if recursive scan is enabled - ScanReferences(mode, buildResult).ForEach(p => list.Add(p)); + // Stryker will recursively scan projects + // add any project reference for progressive discovery (when not using solution file) + list.Add(projectAnalysisContext.GetProjectReferences() + .Where(projectReference => FileSystem.File.Exists(projectReference))); } ); } @@ -341,60 +344,6 @@ private void LogAnalysis(Dictionary> find return mutableProjectsAnalyzerResults; } - private IEnumerable AnalyzeThisProject(ProjectAnalyzerContext project, - string normalizedProjectUnderTestNameFilter, - IStrykerOptions options, - ConcurrentBag<(IEnumerable result, bool isTest)> mutableProjectsAnalyzerResults) - { - IEnumerable buildResult = AnalyzeSingleProject(project, options); - if (!buildResult.Any()) - { - // analysis failed - mutableProjectsAnalyzerResults.Add((buildResult, false)); - return buildResult; - } - - var isTestProject = buildResult.IsTestProject(); - if (isTestProject) - { - // filter frameworks for test projects (if one is selected) - buildResult = [SelectAnalyzerResult(buildResult, options.TargetFramework)]; - } - - // apply project name filter (except for test projects) - if (isTestProject || normalizedProjectUnderTestNameFilter == null || - project.ProjectFileName.Replace('\\', '/') - .Contains(normalizedProjectUnderTestNameFilter, - StringComparison.InvariantCultureIgnoreCase)) - { - mutableProjectsAnalyzerResults.Add((buildResult, isTestProject)); - } - - return buildResult; - } - - /// - /// Scan the references of a project and add them for analysis according to scan option - /// - /// scan mode - /// analyzer results to parse - /// A list of project to analyze - private List ScanReferences(ScanMode mode, IEnumerable buildResult) - { - var referencesToAdd = new List(); - - if (mode == ScanMode.NoScan || (mode == ScanMode.ScanTestProjectReferences && !buildResult.IsTestProject())) - { - return referencesToAdd; - } - - // Stryker will recursively scan projects - // 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; - } - private IAnalyzerResults AnalyzeSingleProject(ProjectAnalyzerContext project, IStrykerOptions options) { var projectLogName = FileSystem.Path.GetRelativePath(options.WorkingDirectory, project.ProjectFileName); @@ -413,13 +362,27 @@ private IAnalyzerResults AnalyzeSingleProject(ProjectAnalyzerContext project, IS } // if this is a full framework project, we can retry after a nuget restore - buildResult = RetryBuild(project, options, out buildResultOverallSuccess); + buildResult = project.Analyze(withRestore: true); + + // check the new status + buildResultOverallSuccess = project.HasValidResults(); + + if (!buildResultOverallSuccess && !string.IsNullOrEmpty(options.TargetFramework)) + { + // still failed, we can try using target framework option + buildResult = project.Analyze(forceFramework: true); + buildResultOverallSuccess = project.HasValidResults(); + } + // store the build log - _buildLogs[projectLogName] = project.LastBuildLog.ToString(); - // clear the log + _buildLogs[projectLogName] = project.LastBuildLog; + } + + if (options.DiagMode || _logger.IsEnabled(LogLevel.Debug)) + { + project.LogAnalyzerResult(); } - LogAnalyzerResult(buildResult, options); if (buildResultOverallSuccess) { _logger.LogDebug("Analysis of project {projectFilePath} succeeded.", projectLogName); @@ -439,147 +402,16 @@ private IAnalyzerResults AnalyzeSingleProject(ProjectAnalyzerContext project, IS return buildResult; } - - private IAnalyzerResults RetryBuild(ProjectAnalyzerContext analyzer, IStrykerOptions options, out bool buildResultOverallSuccess) - { - var buildResult = analyzer.Analyze(withRestore: true); - - // check the new status - buildResultOverallSuccess = buildResult.HasValidResult(); - - if (buildResultOverallSuccess || string.IsNullOrEmpty(options.TargetFramework)) - { - return buildResult; - } - - // still failed, we can try using target framework option - buildResult = analyzer.Analyze(forceFramework: true); - buildResultOverallSuccess = buildResult.Any( br => br.IsValidFor(options.TargetFramework)); - - return buildResult; - } - - private void LogAnalyzerResult(IAnalyzerResults analyzerResults, IStrykerOptions options) - { - // do not log if trace is not enabled - if (!_logger.IsEnabled(LogLevel.Trace) || !options.DiagMode) - { - return; - } - if (analyzerResults.Count == 0) - { - _logger.LogTrace("No analyzer results to log. This indicates an early failure in analysis, check build log for details."); - return; - } - var log = new StringBuilder(); - // dump all properties as it can help diagnosing build issues for user project. - log.AppendLine("**** Buildalyzer result ****"); - - log.AppendLine($"Project: {analyzerResults.First().ProjectFilePath}"); - foreach (var analyzerResult in analyzerResults) - { - log.AppendLine($"TargetFramework: {analyzerResult.TargetFramework}"); - log.AppendLine($"Succeeded: {analyzerResult.Succeeded}"); - log.AppendLine($"Compiler command: {analyzerResult.Command}"); - - var properties = analyzerResult.Properties; - foreach (var property in ImportantProperties) - { - log.AppendLine($"Property {property}={properties.GetValueOrDefault(property) ?? "\"'undefined'\""}"); - } - if (analyzerResult.SourceFiles.Length == 0) - { - log.AppendLine("** No source files identified **"); - } - else - { - foreach (var sourceFile in analyzerResult.SourceFiles) - { - log.AppendLine($"SourceFile {sourceFile}"); - } - } - if (analyzerResult.References.Length == 0) - { - log.AppendLine("** No references Identified **"); - } - else - { - foreach (var reference in analyzerResult.References) - { - log.AppendLine($"References: {FileSystem.Path.GetFileName(reference)} (in {FileSystem.Path.GetDirectoryName(reference)})"); - } - } - - if (_logger.IsEnabled(LogLevel.Trace)) - { - // dumps all other properties as well, as they can be useful for diagnosing build issues - foreach (var property in properties.Where( p => !ImportantProperties.Contains(p.Key) )) - { - log.AppendLine($"Property {property.Key}={property.Value.Replace(Environment.NewLine, "\\n")}"); - } - } - - log.AppendLine(); - } - log.AppendLine("**** End Buildalyzer result ****"); - _logger.LogDebug(log.ToString()); - } - - private IAnalyzerResult SelectAnalyzerResult(IEnumerable analyzerResults, string targetFramework) - { - var validResults = analyzerResults.ToList(); - var projectName = analyzerResults.First().ProjectFilePath; - if (validResults.Count == 0) - { - throw new InputException($"No valid project analysis results could be found for '{projectName}'."); - } - - if (targetFramework is null) - { - // we try to avoid desktop versions - return PickFrameworkVersion(); - } - - var resultForRequestedFramework = validResults.Find(a => a.TargetFramework == targetFramework); - if (resultForRequestedFramework is not null) - { - return resultForRequestedFramework; - } - // if there is only one available framework version, we log an info - if (validResults.Count == 1) - { - var singleAnalyzerResult = validResults[0]; - _logger.LogInformation( - "Could not find a valid analysis for target {0} for project '{1}'. Selected version is {2}.", - targetFramework, projectName, singleAnalyzerResult.TargetFramework); - return singleAnalyzerResult; - } - - var firstAnalyzerResult = PickFrameworkVersion(); - var availableFrameworks = validResults.Select(a => a.TargetFramework).Distinct(); - _logger.LogWarning( - """ - Could not find a valid analysis for target {0} for project '{1}'. - The available target frameworks are: {2}. - selected version is {3}. - """, targetFramework, projectName, string.Join(',', availableFrameworks), firstAnalyzerResult.TargetFramework); - - return firstAnalyzerResult; - - IAnalyzerResult PickFrameworkVersion() - { - return validResults.Find(a => a.IsValid() && !a.TargetsFullFramework()) ?? validResults[0]; - } - } - - private (Dictionary>, List) FindMutableAnalyzerResults(ConcurrentBag<(IEnumerable result, bool isTest)> mutableProjectsAnalyzerResults) + private (List, List) FindMutableAnalyzerResults( + IEnumerable mutableProjectsAnalyzerResults) { // separate test projects from mutable projects, and keep only analyzer results building an assembly (exclude solution folders and such) - var analyzerTestProjects = mutableProjectsAnalyzerResults.Where(p => p.isTest).SelectMany(p => p.result).Where(p => p.BuildsAnAssembly()); - var mutableProjects = mutableProjectsAnalyzerResults.Where(p => !p.isTest).SelectMany(p => p.result).Where(p => p.BuildsAnAssembly()).ToArray(); + var analyzerTestProjects = mutableProjectsAnalyzerResults.Where(p => p.IsTest && p.BuildsAnAssembly()); + var mutableProjects = mutableProjectsAnalyzerResults.Where(p => !p.IsTest && p.BuildsAnAssembly()).ToArray(); + + var mutableToTestMap = mutableProjects.ToDictionary(p =>p, p => new MutableProjectTree(p, _logger)); + var unusedTestProjects = new List(); - var mutableToTestMap = mutableProjects.ToDictionary(p => p, _ => new List()); - var unusedTestProjects = new List(); // for each test project foreach (var testProject in analyzerTestProjects) { @@ -588,7 +420,7 @@ IAnalyzerResult PickFrameworkVersion() continue; } - _logger.LogInformation("Could not find an assembly reference to a mutable assembly for project {ProjectName}. Will look into project references.", testProject.ProjectFilePath); + _logger.LogInformation("Could not find an assembly reference to a mutable assembly for project {ProjectName}. Will look into project references.", testProject.ProjectFileName); // we try to find a project reference if (!ScanProjectReferences(mutableToTestMap, mutableProjects, testProject)) { @@ -596,95 +428,68 @@ IAnalyzerResult PickFrameworkVersion() } } - return (mutableToTestMap, unusedTestProjects); + return (mutableToTestMap.Values.ToList(), unusedTestProjects); } - private static bool ScanProjectReferences(Dictionary> mutableToTestMap, - IAnalyzerResult[] mutableProjects, IAnalyzerResult testProject) + private static bool ScanAssemblyReferences(Dictionary mutableToTestMap, + ProjectAnalyzerContext[] mutableProjects, ProjectAnalyzerContext testProject) { - var mutableProject = mutableProjects.FirstOrDefault(p => testProject.ProjectReferences.Contains(p.ProjectFilePath)); - if (mutableProject == null) - { - return false; - } - if (!mutableToTestMap.TryGetValue(mutableProject, out var dependencies)) + var foundOneProject = false; + // we do the work for each available target + foreach (var variant in testProject.AnalyzerLastResults) { - mutableToTestMap[mutableProject] = dependencies = []; - } + // we analyze references + foreach (var variantReference in variant.References) + { + foreach (var candidateProject in mutableProjects) + { + if (!candidateProject.FindMatchingVariant(variantReference, out var candidateTarget)) + { + continue; + } + // find the entry + mutableToTestMap[candidateProject][candidateTarget].TestProjects.Add(variant); - dependencies.Add(testProject); - return true; + foundOneProject = true; + } + } + } + return foundOneProject; } - - private static bool ScanAssemblyReferences(Dictionary> mutableToTestMap, - IAnalyzerResult[] mutableProjects, IAnalyzerResult testProject) + private static bool ScanProjectReferences(Dictionary mutableToTestMap, + ProjectAnalyzerContext[] mutableProjects, ProjectAnalyzerContext testProject) { var foundOneProject = false; - // we identify which projects are referenced by it - foreach (var mutableProject in mutableProjects) + foreach (var variant in testProject.AnalyzerLastResults) { - var assemblyPath = mutableProject.GetAssemblyPath(); - var refAssemblyPath = mutableProject.GetReferenceAssemblyPath(); - - if (Array.TrueForAll(testProject.References, r => !r.Equals(assemblyPath, StringComparison.OrdinalIgnoreCase) && - !r.Equals(refAssemblyPath, StringComparison.OrdinalIgnoreCase))) - { - continue; - } - if (!mutableToTestMap.TryGetValue(mutableProject, out var dependencies)) + foreach (var projectReference in variant.ProjectReferences) { - mutableToTestMap[mutableProject] = dependencies = []; + var candidateProject = mutableProjects.FirstOrDefault(p => p.ProjectFileName == projectReference); + var candidateProjectVariants = candidateProject + ?.AnalyzerLastResults; + if (candidateProjectVariants == null) + { + // probably another test project + continue; + } + // we try to find a target with the same target framework or one of the same kind (full or core + var candidateVariant = candidateProjectVariants.FirstOrDefault( v=> v.TargetFramework == variant.TargetFramework) ?? + candidateProjectVariants.FirstOrDefault( v=> v.TargetsFullFramework() == variant.TargetsFullFramework()); + if (candidateVariant == null) + { + continue; + } + mutableToTestMap[candidateProject][candidateVariant].TestProjects.Add(variant); + + foundOneProject = true; } - dependencies.Add(testProject); - foundOneProject = true; } return foundOneProject; } - /// - /// Builds a instance describing a project its associated test project(s) - /// - /// Stryker options - /// - /// project buildalyzer result - /// test project(s) buildalyzer result(s) - /// - private SourceProjectInfo BuildSourceProjectInfo(IStrykerOptions options, - TargetsForMutation solutionInfo, - IAnalyzerResult analyzerResult, - IEnumerable analyzerResults) - { - var targetProjectInfo = new SourceProjectInfo - { - AnalyzerResult = analyzerResult - }; - var language = targetProjectInfo.AnalyzerResult.GetLanguage(); - if (language == Language.Fsharp) - { - _logger.LogError( - targetProjectInfo.LogError( - "Mutation testing of F# projects is not ready yet. No mutants will be generated.")); - } - ProjectComponentsBuilder builder = language == Language.Csharp - ? new CsharpProjectComponentsBuilder(targetProjectInfo, options, _foldersToExclude, _logger, - FileSystem) - : throw new NotSupportedException($"Language not supported: {language}"); - - var inputFiles = builder.Build(); - builder.InjectHelpers(inputFiles); - targetProjectInfo.OnProjectBuilt = builder.PostBuildAction(); - targetProjectInfo.ProjectContents = inputFiles; - targetProjectInfo.TargetsForMutation = solutionInfo; - _logger.LogInformation("Found project {ProjectFileName} to mutate.", analyzerResult.ProjectFilePath); - targetProjectInfo.TestProjectsInfo = new TestProjectsInfo(FileSystem) - { - TestProjects = analyzerResults.Select(testProjectAnalyzerResult => new TestProject(FileSystem, testProjectAnalyzerResult)).ToList() - }; - return targetProjectInfo; - } private string FindProjectFile(string path) { @@ -745,9 +550,6 @@ private static StringBuilder BuildReferenceChoice(IEnumerable projectRef private static string NormalizePath(string path) => path?.Replace('\\', '/'); - private static string NormalizePlatform(string platform) => - string.Equals(platform, "Any CPU", StringComparison.OrdinalIgnoreCase) ? "AnyCPU" : platform; - private sealed class DynamicEnumerableQueue { private readonly ConcurrentQueue _queue; @@ -761,13 +563,16 @@ public DynamicEnumerableQueue(IEnumerable init) public bool Empty => _queue.IsEmpty; - public void Add(T entry) + public void Add(IEnumerable where) { - if (!_cache.TryAdd(entry, true)) + foreach (var entry in where) { - return; + if (!_cache.TryAdd(entry, true)) + { + return; + } + _queue.Enqueue(entry); } - _queue.Enqueue(entry); } public IEnumerable Consume() diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTarget.cs b/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTarget.cs new file mode 100644 index 0000000000..46e8aa3fab --- /dev/null +++ b/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTarget.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Linq; +using Buildalyzer; +using Microsoft.Extensions.Logging; +using Stryker.Abstractions; +using Stryker.Abstractions.Options; +using Stryker.Core.ProjectComponents.SourceProjects; +using Stryker.Core.ProjectComponents.TestProjects; +using Stryker.Utilities.Buildalyzer; + +namespace Stryker.Core.Initialisation; + +/// +/// Keep track of a project and the test projects covering it +/// +/// analyzer result for target project +/// logger used for reporting +internal class MutableProjectTarget(IAnalyzerResult target, ILogger logger) +{ + public IAnalyzerResult ProjectTarget { get; } = target; + + public List TestProjects { get; } = []; + + public bool IsValidTarget => ProjectTarget.IsValid() && TestProjects.Count>0 && TestProjects.Any(tp => tp.IsValid()); + + private static readonly string[] FoldersToExclude = ["obj", "bin", "node_modules", "StrykerOutput"]; + + /// + /// Builds a instance describing a project its associated test project(s) + /// + /// Stryker options + /// + /// filesystem + /// + public SourceProjectInfo BuildSourceProjectInfo(IStrykerOptions options, + TargetsForMutation solutionInfo, IFileSystem fileSystem ) + { + var targetProjectInfo = new SourceProjectInfo + { + AnalyzerResult = ProjectTarget + }; + + var language = targetProjectInfo.AnalyzerResult.GetLanguage(); + + ProjectComponentsBuilder builder = language == Language.Csharp + ? new CsharpProjectComponentsBuilder(targetProjectInfo, options, FoldersToExclude, logger, + fileSystem) + : throw new NotSupportedException($"Language not supported: {language}"); + + var inputFiles = builder.Build(); + builder.InjectHelpers(inputFiles); + targetProjectInfo.OnProjectBuilt = builder.PostBuildAction(); + targetProjectInfo.ProjectContents = inputFiles; + targetProjectInfo.TargetsForMutation = solutionInfo; + logger.LogInformation("Found project {ProjectFileName} to mutate.", ProjectTarget.ProjectFilePath); + targetProjectInfo.TestProjectsInfo = new TestProjectsInfo(fileSystem) + { + TestProjects = TestProjects.Select(testProjectAnalyzerResult => new TestProject(fileSystem, testProjectAnalyzerResult)).ToList() + }; + return targetProjectInfo; + } + + public void DumpForAnalysis() + { + var targetProjectTarget = ProjectTarget; + logger.LogInformation(" target {TargetFramework} analysis {Result}, simulated build {BuildResult}.", + targetProjectTarget.TargetFramework, + targetProjectTarget.IsValid() ? "succeeded" : "failed", + targetProjectTarget.Succeeded ? "succeeded" : "failed" + ); + if (TestProjects.Count == 0) + { + logger.LogWarning( + " can't be mutated because no test project references it. If this is a test project, " + + "ensure it has the property: true in its project file."); + return; + } + + // dump info on test projects + foreach (var testProject in TestProjects) + { + logger.LogInformation(" referenced by test project {ProjectName}, analysis {Result}.", + testProject.ProjectFilePath, + testProject.IsValid() ? "succeeded" : "failed"); + + } + + // dump associated test projects + // provide synthetic status + if (TestProjects.Any(r => r.IsValid())) + { + logger.LogInformation(" can be mutated."); + } + else + { + logger.LogWarning(" can't be mutated because all referencing test projects' analysis failed."); + } + } +} diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTree.cs b/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTree.cs new file mode 100644 index 0000000000..148bbf310c --- /dev/null +++ b/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTree.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Buildalyzer; +using Microsoft.Extensions.Logging; +using Stryker.Utilities.Buildalyzer; + +namespace Stryker.Core.Initialisation; + +/// +/// This class is used to keep track of the project and its targets during the initialization process. +/// It allows us to keep track of the targets that are valid for mutation and to keep track of the project analyzer context. +/// +/// +internal class MutableProjectTree(ProjectAnalyzerContext project, ILogger logger) +{ + private readonly ILogger _logger = logger; + + public ProjectAnalyzerContext Project { get; } = project; + + public List Targets { get; } = []; + + public bool IsValidTarget => Targets.Any(t => t.IsValidTarget); + + public MutableProjectTarget this[IAnalyzerResult target] + { + get + { + var existingTarget = Targets.FirstOrDefault(t => t.ProjectTarget == target); + if (existingTarget != null) + { + return existingTarget; + } + var newTarget = new MutableProjectTarget(target, _logger); + Targets.Add(newTarget); + return newTarget; + } + } + + /// + /// Keep a single target for mutation when analysis gave several results + /// + /// + public void KeepOnlyOneTarget(string optionsTargetFramework) + { + MutableProjectTarget targetToKeep; + // look for a specified target framework if any + if (!string.IsNullOrEmpty(optionsTargetFramework)) + { + // try to find a valid analysis for requested framework + targetToKeep = Targets.FirstOrDefault(t => t.ProjectTarget.TargetFramework == optionsTargetFramework && t.IsValidTarget); + // then try to find a valid test project for the requested framework + targetToKeep ??= Targets.FirstOrDefault(t => t.TestProjects.Any( tp => tp.TargetFramework == optionsTargetFramework && tp.IsValid())); + if (targetToKeep != null) + { + Targets.Clear(); + Targets.Add(targetToKeep); + return; + } + _logger.LogWarning("Failed to find a valid {Framework} target for project {Project}. ", optionsTargetFramework, Project.ProjectFileName); + } + + targetToKeep = Targets.Where(t => t.IsValidTarget).FirstOrDefault( t => OperatingSystem.IsWindows() || !t.ProjectTarget.TargetsFullFramework()); + Targets.Clear(); + if (targetToKeep == null) + { + _logger.LogWarning("Failed to find a valid target for project {Project}. ", Project.ProjectFileName); + return; + } + _logger.LogInformation("Picking {Framework} for project {Project}. ", targetToKeep.ProjectTarget.TargetFramework, Project.ProjectFileName); + Targets.Add(targetToKeep); + } + + public void DumpForAnalysis() + { + _logger.LogInformation("Project {ProjectPath} overall analysis {Result}.", + Project.ProjectFileName, + IsValidTarget ? "succeeded" : "failed hence can't be mutated"); + foreach (var target in Targets) + { + target.DumpForAnalysis(); + } + } +} diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs index 6a36d48be5..4bf0b1b548 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs @@ -1,11 +1,12 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using Buildalyzer; using Buildalyzer.Environment; using Microsoft.Extensions.Logging; using Stryker.Abstractions.Exceptions; -using Stryker.Abstractions.Options; using Stryker.Utilities.Buildalyzer; namespace Stryker.Core.Initialisation; @@ -14,18 +15,19 @@ public class ProjectAnalyzerContext { private readonly IProjectAnalyzer _analyzer; private readonly TargetsForMutation _targetsForMutation; - private readonly IStrykerOptions _options; + private readonly string _msBuildPath; private readonly string _configuration; private readonly string _platform; private readonly string _framework; private readonly ILogger _logger; private readonly StringWriter _buildLogger; - private IAnalyzerResults _analyzerLastResults; + public IAnalyzerResults AnalyzerLastResults { get; private set; } + private IEnumerable _filteredResults; private string[] _targetFrameworks; public ProjectAnalyzerContext(IBuildalyzerProvider buildalyzerProvider, string projectFile, - IStrykerOptions options, + string msBuildPath, string configuration, string platform, string framework, @@ -39,7 +41,7 @@ public ProjectAnalyzerContext(IBuildalyzerProvider buildalyzerProvider, _analyzer = analyzer; ProjectFileName = projectFile; _targetsForMutation = targetsForMutation; - _options = options; + _msBuildPath = msBuildPath; _configuration = configuration; _platform = platform; _framework = framework; @@ -52,19 +54,19 @@ public ProjectAnalyzerContext(IBuildalyzerProvider buildalyzerProvider, public IAnalyzerResults Analyze(bool withRestore = false, bool forceFramework = false) { - if (withRestore && _analyzerLastResults?.Any(ar => ar.TargetsFullFramework()) == true) + if (withRestore && AnalyzerLastResults?.Any(ar => ar.TargetsFullFramework()) == true) { - _targetsForMutation.RestoreSolution(_analyzerLastResults); + _targetsForMutation.RestoreSolution(AnalyzerLastResults); } _buildLogger.GetStringBuilder().Clear(); var env = new EnvironmentOptions { Restore = withRestore }; - if (!string.IsNullOrEmpty(_options.MsBuildPath)) + if (!string.IsNullOrEmpty(_msBuildPath)) { // we need to forward this path to buildalyzer - env.EnvironmentVariables[EnvironmentVariables.MSBUILD_EXE_PATH] = _options.MsBuildPath; + env.EnvironmentVariables[EnvironmentVariables.MSBUILD_EXE_PATH] = _msBuildPath; } if (!string.IsNullOrEmpty(_configuration)) { @@ -75,9 +77,9 @@ public IAnalyzerResults Analyze(bool withRestore = false, bool forceFramework = env.GlobalProperties["Platform"] = _platform; } - _analyzerLastResults = forceFramework ? _analyzer.Build(_options.TargetFramework, env) : _analyzer.Build(env); + AnalyzerLastResults = forceFramework ? _analyzer.Build(_framework, env) : _analyzer.Build(env); InitializeTargetFrameworks(); - return _analyzerLastResults; + return AnalyzerLastResults; } private void InitializeTargetFrameworks() @@ -89,14 +91,14 @@ private void InitializeTargetFrameworks() } else { - if (!string.IsNullOrEmpty(_options.TargetFramework)) + if (!string.IsNullOrEmpty(_framework)) { - projectFileTargetFrameworks=[_options.TargetFramework]; - _logger.LogWarning("Failed to identify target frameworks for project {ProjectFilePath}. Assuming selected framework ({framework}) is present.", ProjectFileName, _options.TargetFramework); + projectFileTargetFrameworks=[_framework]; + _logger.LogWarning("Failed to identify target frameworks for project {ProjectFilePath}. Assuming selected framework ({framework}) is present.", ProjectFileName, _framework); } else { - projectFileTargetFrameworks = _analyzerLastResults.Select(br => br.TargetFramework).ToArray(); + projectFileTargetFrameworks = AnalyzerLastResults.Select(br => br.TargetFramework).ToArray(); _logger.LogWarning("Failed to identify target frameworks for project {ProjectFilePath}. Using analysis results: {frameworks}", ProjectFileName, string.Join(',', projectFileTargetFrameworks)); } } @@ -105,25 +107,31 @@ private void InitializeTargetFrameworks() } public IEnumerable FailedFrameworks => _targetFrameworks.Where(tf => - !_analyzerLastResults.Any( ar => ar.TargetFramework == tf && ar.IsValid())); + !AnalyzerLastResults.Any( ar => ar.TargetFramework == tf && ar.IsValid())); - public bool HasValidResults() => _analyzerLastResults != null && _analyzerLastResults.IsValidFor(_targetFrameworks); + public bool IsTest => AnalyzerLastResults?.IsTestProject() == true; - private IAnalyzerResult SelectAnalyzerResult(IEnumerable analyzerResults, string targetFramework) + public bool HasValidResults() => AnalyzerLastResults != null && AnalyzerLastResults.IsValidFor(_targetFrameworks); + + public bool IsTestProject() => AnalyzerLastResults != null && AnalyzerLastResults.IsTestProject(); + + public IEnumerable GetProjectReferences() => AnalyzerLastResults?.SelectMany(r => r.ProjectReferences).Distinct(); + + public IAnalyzerResult SelectAnalyzerResult() { - var validResults = analyzerResults.ToList(); + var validResults = AnalyzerLastResults.ToList(); if (validResults.Count == 0) { throw new InputException($"No valid project analysis results could be found for '{ProjectFileName}'."); } - if (targetFramework is null) + if (_framework is null) { // we try to avoid desktop versions return PickFrameworkVersion(); } - var resultForRequestedFramework = validResults.Find(a => a.TargetFramework == targetFramework); + var resultForRequestedFramework = validResults.Find(a => a.TargetFramework == _framework); if (resultForRequestedFramework is not null) { return resultForRequestedFramework; @@ -134,7 +142,7 @@ private IAnalyzerResult SelectAnalyzerResult(IEnumerable analyz var singleAnalyzerResult = validResults[0]; _logger.LogInformation( "Could not find a valid analysis for target {0} for project '{1}'. Selected version is {2}.", - targetFramework, ProjectFileName, singleAnalyzerResult.TargetFramework); + _framework, ProjectFileName, singleAnalyzerResult.TargetFramework); return singleAnalyzerResult; } @@ -145,7 +153,7 @@ private IAnalyzerResult SelectAnalyzerResult(IEnumerable analyz Could not find a valid analysis for target {0} for project '{1}'. The available target frameworks are: {2}. selected version is {3}. - """, targetFramework, ProjectFileName, string.Join(',', availableFrameworks), firstAnalyzerResult.TargetFramework); + """, _framework, ProjectFileName, string.Join(',', availableFrameworks), firstAnalyzerResult.TargetFramework); return firstAnalyzerResult; @@ -155,4 +163,80 @@ IAnalyzerResult PickFrameworkVersion() } } + private static readonly HashSet ImportantProperties = + ["Configuration", "Platform", "AssemblyName", "Configurations", "TargetPath", "OS"]; + + public void LogAnalyzerResult() + { + var log = new StringBuilder(); + try + { + log.AppendLine("**** Buildalyzer result ****"); + log.AppendLine($"Project: {ProjectFileName}"); + if (AnalyzerLastResults.Count == 0) + { + _logger.LogTrace("No analyzer results to log. This indicates an early failure in analysis, check build log for details."); + return; + } + // dump all properties as it can help diagnosing build issues for user project. + foreach (var analyzerResult in AnalyzerLastResults) + { + log.AppendLine($"TargetFramework: {analyzerResult.TargetFramework}"); + log.AppendLine($"Succeeded: {analyzerResult.Succeeded}"); + log.AppendLine($"Compiler command: {analyzerResult.Command}"); + + var properties = analyzerResult.Properties; + foreach (var property in ImportantProperties) + { + log.AppendLine($"Property {property}={properties.GetValueOrDefault(property) ?? "\"'undefined'\""}"); + } + if (analyzerResult.SourceFiles.Length == 0) + { + log.AppendLine("** No source files identified **"); + } + else + { + log.AppendLine( + $"{analyzerResult.SourceFiles.Length} source files: {string.Join(',', analyzerResult.SourceFiles)}"); + } + if (analyzerResult.References.Length == 0) + { + log.AppendLine("** No references Identified **"); + } + else + { + foreach (var reference in analyzerResult.References) + { + log.AppendLine($"References: {Path.GetFileName(reference)} (in {Path.GetDirectoryName(reference)})"); + } + } + + if (_logger.IsEnabled(LogLevel.Trace)) + { + // dumps all other properties as well, as they can be useful for diagnosing build issues + foreach (var property in properties.Where( p => !ImportantProperties.Contains(p.Key) )) + { + log.AppendLine($"Property {property.Key}={property.Value.Replace(Environment.NewLine, "\\n")}"); + } + } + + log.AppendLine(); + } + } + finally + { + log.AppendLine("**** End Buildalyzer result ****"); + _logger.LogDebug(log.ToString()); + } + } + + public bool BuildsAnAssembly() => AnalyzerLastResults?.Any(p => p.BuildsAnAssembly()) == true; + + public bool FindMatchingVariant(string assemblyPath, out IAnalyzerResult analyzerResult) + { + analyzerResult= AnalyzerLastResults.FirstOrDefault( r=> + string.Compare(assemblyPath, r.GetAssemblyFileName(), StringComparison.OrdinalIgnoreCase) == 0 + || string.Compare(assemblyPath, r.GetReferenceAssemblyPath(), StringComparison.OrdinalIgnoreCase) == 0); + return analyzerResult != null; + } } diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs b/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs index 545274dfe5..1246f70eb2 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs @@ -29,6 +29,7 @@ public TargetsForMutation(SolutionFile file, IStrykerOptions options, IBuildalyz SelectConfiguration(); } + private SolutionFile Solution { get; init; } public string SolutionFilePath => Solution?.FileName; @@ -96,6 +97,11 @@ internal void RestoreSolution(IAnalyzerResults results) _solutionRestored = true; } + /// + /// Gets a project analysis context for the given project file, using the configuration and platform from the solution if available, otherwise using the configuration and platform from the options. + /// + /// target project file + /// a instance for . public ProjectAnalyzerContext GetProjectAnalysisContext(string projectFile) { string configuration; @@ -109,6 +115,7 @@ public ProjectAnalyzerContext GetProjectAnalysisContext(string projectFile) { (configuration, platform) = (Configuration, Platform); } - return new ProjectAnalyzerContext(_buildalyzerProvider, projectFile, _options, configuration, platform, _options.TargetFramework, _logger, this); + return new ProjectAnalyzerContext(_buildalyzerProvider, projectFile, _options.MsBuildPath, configuration, + platform, _options.TargetFramework, _logger, this); } } From 908e031debe7059126060c16ee997430f3de5004 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Tue, 31 Mar 2026 09:10:25 +0200 Subject: [PATCH 05/41] chore: address copilot requests --- .../Initialisation/InputFileResolver.cs | 6 +++--- .../Initialisation/MutableProjectTree.cs | 2 +- .../Initialisation/ProjectAnalyzerContext.cs | 17 +++++++++------ .../Initialisation/TargetsForMutation.cs | 2 +- src/Stryker.Solutions/SolutionFile.cs | 21 ++++++++++++------- .../MicrosoftTestPlatformRunnerPoolTests.cs | 6 +++--- 6 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index cae74d622b..7109033940 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -48,7 +48,7 @@ public class InputFileResolver( /// Identifies the project(s) to mutate and their associated test project(s) according to provided options, and returns a collection of describing them. /// /// Stryker options - /// a collection of describing mutable project + /// a collection of describing mutable project /// Thrown if the method fails during analysis. public IReadOnlyCollection ResolveSourceProjectInfos(IStrykerOptions options) { @@ -172,7 +172,7 @@ private List SelectSingleProject(string normalizedProjectUnde } else { - _logger.LogError("No project could be found as a project referenced by the provi ded test projects."); + _logger.LogError("No project could be found as a project referenced by the provided test projects."); } return result; @@ -569,7 +569,7 @@ public void Add(IEnumerable where) { if (!_cache.TryAdd(entry, true)) { - return; + continue; } _queue.Enqueue(entry); } diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTree.cs b/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTree.cs index 148bbf310c..69e2df583b 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTree.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTree.cs @@ -75,7 +75,7 @@ public void DumpForAnalysis() { _logger.LogInformation("Project {ProjectPath} overall analysis {Result}.", Project.ProjectFileName, - IsValidTarget ? "succeeded" : "failed hence can't be mutated"); + IsValidTarget ? "succeeded" : "failed hence can't be mutated"); foreach (var target in Targets) { target.DumpForAnalysis(); diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs index 4bf0b1b548..51ca365ad4 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs @@ -18,19 +18,18 @@ public class ProjectAnalyzerContext private readonly string _msBuildPath; private readonly string _configuration; private readonly string _platform; - private readonly string _framework; + private readonly string? _framework; private readonly ILogger _logger; private readonly StringWriter _buildLogger; - public IAnalyzerResults AnalyzerLastResults { get; private set; } - private IEnumerable _filteredResults; - private string[] _targetFrameworks; + public IAnalyzerResults? AnalyzerLastResults { get; private set; } + private string[] _targetFrameworks=[]; public ProjectAnalyzerContext(IBuildalyzerProvider buildalyzerProvider, string projectFile, string msBuildPath, string configuration, string platform, - string framework, + string? framework, ILogger logger, TargetsForMutation targetsForMutation) { @@ -234,8 +233,14 @@ public void LogAnalyzerResult() public bool FindMatchingVariant(string assemblyPath, out IAnalyzerResult analyzerResult) { + if (AnalyzerLastResults == null) + { + analyzerResult = null; + return false; + } + analyzerResult= AnalyzerLastResults.FirstOrDefault( r=> - string.Compare(assemblyPath, r.GetAssemblyFileName(), StringComparison.OrdinalIgnoreCase) == 0 + string.Compare(assemblyPath, r.GetAssemblyPath(), StringComparison.OrdinalIgnoreCase) == 0 || string.Compare(assemblyPath, r.GetReferenceAssemblyPath(), StringComparison.OrdinalIgnoreCase) == 0); return analyzerResult != null; } diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs b/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs index 1246f70eb2..97802ca4a2 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs @@ -19,7 +19,7 @@ public class TargetsForMutation private readonly IBuildalyzerProvider _buildalyzerProvider; private readonly ILogger _logger; - public TargetsForMutation(SolutionFile file, IStrykerOptions options, IBuildalyzerProvider buildalyzerProvider ,ILogger logger, INugetRestoreProcess nugetRestoreProcess) + public TargetsForMutation(SolutionFile? file, IStrykerOptions options, IBuildalyzerProvider buildalyzerProvider ,ILogger logger, INugetRestoreProcess nugetRestoreProcess) { _options = options; _buildalyzerProvider = buildalyzerProvider; diff --git a/src/Stryker.Solutions/SolutionFile.cs b/src/Stryker.Solutions/SolutionFile.cs index 066e73b738..c9d413d4d0 100644 --- a/src/Stryker.Solutions/SolutionFile.cs +++ b/src/Stryker.Solutions/SolutionFile.cs @@ -135,6 +135,7 @@ public IReadOnlyCollection GetProjects(string buildType, string? platfor .ToImmutableList(); } + // returns the appropriate default build type if the provided one is null or whitespace, otherwise returns the provided one private string GetEffectiveBuildType(string? buildType) { if (!string.IsNullOrWhiteSpace(buildType)) @@ -154,23 +155,27 @@ private string GetEffectiveBuildType(string? buildType) { buildType = GetEffectiveBuildType(buildType); platform ??= DefaultPlatform; - return _configurations - .Where(entry => entry.Key.buildType == buildType && entry.Key.platform == platform) - .SelectMany(entry => entry.Value).Select(p => (p.Key, p.Value.buildType, p.Value.platform)) + return _configurations[(buildType, platform)] + .Select(p => (p.Key, p.Value.buildType, p.Value.platform)) .Distinct() .ToImmutableList(); } + /// + /// Gets the appropriate configuration and platform for a given project based on the solution configuration and platform provided as parameters. + /// If the project is not included in the solution configuration, it will return the provided configuration and platform or the defaults if they are not provided. + /// + /// project file name + /// requested configuration + /// requested platform + /// public (string configuration, string platform) GetProjectConfiguration(string filename, string configuration, string platform) { configuration = GetEffectiveBuildType(configuration); platform ??= DefaultPlatform; - foreach (var entry in _configurations) + if (_configurations.TryGetValue((configuration, platform), out var projects) && projects.TryGetValue(filename, out var projectConfig)) { - if (entry.Value.TryGetValue(filename, out var projectConfig)) - { - return (projectConfig.buildType, projectConfig.platform); - } + return (projectConfig.buildType, projectConfig.platform); } return (configuration, platform); } diff --git a/src/Stryker.TestRunner.MicrosoftTestPlatform.UnitTest/MicrosoftTestPlatformRunnerPoolTests.cs b/src/Stryker.TestRunner.MicrosoftTestPlatform.UnitTest/MicrosoftTestPlatformRunnerPoolTests.cs index 470e94c6e9..3025a86781 100644 --- a/src/Stryker.TestRunner.MicrosoftTestPlatform.UnitTest/MicrosoftTestPlatformRunnerPoolTests.cs +++ b/src/Stryker.TestRunner.MicrosoftTestPlatform.UnitTest/MicrosoftTestPlatformRunnerPoolTests.cs @@ -161,7 +161,7 @@ public void Dispose_ShouldDisposeAllRunnersInPool() // Arrange var options = new Mock(); options.Setup(x => x.Concurrency).Returns(3); - + var createdRunners = new List(); var disposedRunners = new System.Collections.Concurrent.ConcurrentBag(); var runnerFactory = new Mock(); @@ -189,7 +189,7 @@ public void Dispose_ShouldDisposeAllRunnersInPool() // The pool uses Parallel.For to create runners, which should complete before constructor returns // However, to be defensive against timing issues, verify by checking the actual runners in the pool - + var timeout = new Stopwatch(); timeout.Start(); var start = timeout.ElapsedMilliseconds; @@ -296,7 +296,7 @@ public void CaptureCoverage_ShouldReturnNormalConfidenceWithCoverageData() coverage.ShouldBeEmpty(); } - // Note: Testing CaptureCoverage with multiple tests that cover different mutants is complex + // Note: Testing CaptureCoverage with multiple tests that cover different mutants is complex // because SingleMicrosoftTestPlatformRunner methods are not virtual/overridable for mocking. // The coverage model is cumulative: all tests receive the aggregated coverage from all runners. // Example: If Test1 covers mutant 1 and Test2 covers mutant 2, both tests will be reported From 56821b2142cba0f6bf32a908ea5999f1a2d93796 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Tue, 31 Mar 2026 11:51:51 +0200 Subject: [PATCH 06/41] dix: failing test on Windows --- .../Initialisation/BuildAnalyzerTestsBase.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/BuildAnalyzerTestsBase.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/BuildAnalyzerTestsBase.cs index 7953724937..5a2ebf09ea 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/BuildAnalyzerTestsBase.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/BuildAnalyzerTestsBase.cs @@ -266,6 +266,7 @@ internal Mock BuildProjectAnalyzerMock(string csprojPathName, projectAnalyzerResultMock.Setup(x => x.ProjectFilePath).Returns(csprojPathName); projectAnalyzerResultMock.Setup(x => x.TargetFramework).Returns(framework); projectAnalyzerResultMock.Setup(x => x.Succeeded).Returns(success); + projectAnalyzerResultMock.Setup(x => x.Command).Returns($"build command for {csprojPathName} with {framework}"); projectAnalyzerResultMock.Setup(x => x.Analyzer).Returns(null); projectAnalyzerResults[framework] = projectAnalyzerResultMock.Object; From 0a5b6aea78f15411278bc17fa9885028424f7ef3 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Tue, 31 Mar 2026 14:10:09 +0200 Subject: [PATCH 07/41] fox: slightly change sse test --- .../Stryker.CLI/Logging/LoggingInitializer.cs | 2 +- .../Reporters/Html/RealTime/SseServerTest.cs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Stryker.CLI/Stryker.CLI/Logging/LoggingInitializer.cs b/src/Stryker.CLI/Stryker.CLI/Logging/LoggingInitializer.cs index ed940c9eba..f3031399c4 100644 --- a/src/Stryker.CLI/Stryker.CLI/Logging/LoggingInitializer.cs +++ b/src/Stryker.CLI/Stryker.CLI/Logging/LoggingInitializer.cs @@ -38,7 +38,7 @@ public void SetupLogOptions(IStrykerInputs inputs, IFileSystem fileSystem = null ApplicationLogging.ConfigureLogger(logLevel, logToFile, diagnoseMode, outputPath); } - private string CreateOutputPath(IStrykerInputs inputs, IFileSystem fileSystem) + private static string CreateOutputPath(IStrykerInputs inputs, IFileSystem fileSystem) { var outputPath = inputs.OutputPathInput.SuppliedInput ?? Path.Combine("StrykerOutput", DateTime.Now.ToString("yyyy-MM-dd.HH-mm-ss")); diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Reporters/Html/RealTime/SseServerTest.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Reporters/Html/RealTime/SseServerTest.cs index 668b5f96f9..ee2ab15694 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Reporters/Html/RealTime/SseServerTest.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Reporters/Html/RealTime/SseServerTest.cs @@ -34,10 +34,10 @@ private void ClientConnected(object sender, EventArgs e) private bool WaitForConnection(int timeout) { - var watch = new Stopwatch(); - watch.Start(); lock (_lock) { + var watch = new Stopwatch(); + watch.Start(); while (!_connected && watch.ElapsedMilliseconds < timeout) { Monitor.Wait(_lock, Math.Max(timeout - (int)watch.ElapsedMilliseconds, 1)); @@ -49,10 +49,10 @@ private bool WaitForConnection(int timeout) private bool WaitForDisConnection(int timeout) { - var watch = new Stopwatch(); - watch.Start(); lock (_lock) { + var watch = new Stopwatch(); + watch.Start(); while (_sut.HasConnectedClients && watch.ElapsedMilliseconds < timeout) { Monitor.Wait(_lock, Math.Max(Math.Min( timeout - (int)watch.ElapsedMilliseconds, 100), 1)); @@ -129,7 +129,7 @@ public void ShouldHandleDroppedConnection() var @object = new { Id = "1", Status = "Survived" }; var sseClient = new EventSource(new Uri($"http://localhost:{_sut.Port}/")); - + Task.Run(() => sseClient.StartAsync()); WaitForConnection(500).ShouldBeTrue(); Task.Run( ()=> {sseClient.Close(); sseClient.Dispose();}).Wait(); From de7d470ae0688b9d33fe6b7a51f72e07f6bb650a Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Tue, 31 Mar 2026 14:54:28 +0200 Subject: [PATCH 08/41] chore: address sonar errors --- .../Initialisation/InputFileResolver.cs | 8 +- .../Initialisation/ProjectAnalyzerContext.cs | 99 ++++++++++--------- .../Initialisation/TargetsForMutation.cs | 4 +- .../Buildalyzer/IAnalyzerResultExtensions.cs | 9 +- 4 files changed, 59 insertions(+), 61 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index 7109033940..2f82f4316c 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -93,12 +93,12 @@ public IReadOnlyCollection ResolveSourceProjectInfos(IStryker private IReadOnlyCollection FindProjectsInSolutionMode(IStrykerOptions options, TargetsForMutation solutionInfo, string normalizedProjectUnderTestNameFilter) { - _logger.LogInformation("Identifying projects to mutate in {solution}. This can take a while.", + _logger.LogInformation("Identifying projects to mutate in {Solution}. This can take a while.", FileSystem.Path.GetFileNameWithoutExtension(solutionInfo.SolutionFilePath)); // analyze all projects solutionInfo.SelectAllProjects(); - _logger.LogDebug("Analyzing {numProjects} projects.", solutionInfo.ProjectCount); + _logger.LogDebug("Analyzing {ProjectsCount} projects.", solutionInfo.ProjectCount); // we analyze every project in the solution var mutableProjectsAnalyzerResults = AnalyzeAllNeededProjects(solutionInfo, normalizedProjectUnderTestNameFilter, @@ -207,7 +207,7 @@ .. result.Where(p => public string FindTestProject(string path) { var projectFile = FindProjectFile(path); - _logger.LogDebug("Using {filename} as test project", projectFile); + _logger.LogDebug("Using {Filename} as test project", projectFile); return projectFile; } @@ -246,7 +246,7 @@ private List AnalyzeAndIdentifyProjects(IStrykerOptions optio // keep only projects with one or more test projects // we must select projects according to framework settings if any var projectInfos = suitableCandidates.Where(p => p.Targets.Count > 0) - .Select(analyzerResult => analyzerResult.Targets.First().BuildSourceProjectInfo(options, solutionInfo, FileSystem)) + .Select(analyzerResult => analyzerResult.Targets[0].BuildSourceProjectInfo(options, solutionInfo, FileSystem)) .ToList(); if (projectInfos.Count != 0) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs index 51ca365ad4..c6229213a3 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs @@ -27,9 +27,9 @@ public class ProjectAnalyzerContext public ProjectAnalyzerContext(IBuildalyzerProvider buildalyzerProvider, string projectFile, string msBuildPath, - string configuration, + (string configuration, string platform, - string? framework, + string? framework) target, ILogger logger, TargetsForMutation targetsForMutation) { @@ -41,9 +41,9 @@ public ProjectAnalyzerContext(IBuildalyzerProvider buildalyzerProvider, ProjectFileName = projectFile; _targetsForMutation = targetsForMutation; _msBuildPath = msBuildPath; - _configuration = configuration; - _platform = platform; - _framework = framework; + _configuration = target.configuration; + _platform = target.platform; + _framework = target.framework; _logger = logger; } @@ -93,12 +93,12 @@ private void InitializeTargetFrameworks() if (!string.IsNullOrEmpty(_framework)) { projectFileTargetFrameworks=[_framework]; - _logger.LogWarning("Failed to identify target frameworks for project {ProjectFilePath}. Assuming selected framework ({framework}) is present.", ProjectFileName, _framework); + _logger.LogWarning("Failed to identify target frameworks for project {ProjectFilePath}. Assuming selected framework ({Framework}) is present.", ProjectFileName, _framework); } else { projectFileTargetFrameworks = AnalyzerLastResults.Select(br => br.TargetFramework).ToArray(); - _logger.LogWarning("Failed to identify target frameworks for project {ProjectFilePath}. Using analysis results: {frameworks}", ProjectFileName, string.Join(',', projectFileTargetFrameworks)); + _logger.LogWarning("Failed to identify target frameworks for project {ProjectFilePath}. Using analysis results: {Frameworks}", ProjectFileName, string.Join(',', projectFileTargetFrameworks)); } } @@ -180,46 +180,7 @@ public void LogAnalyzerResult() // dump all properties as it can help diagnosing build issues for user project. foreach (var analyzerResult in AnalyzerLastResults) { - log.AppendLine($"TargetFramework: {analyzerResult.TargetFramework}"); - log.AppendLine($"Succeeded: {analyzerResult.Succeeded}"); - log.AppendLine($"Compiler command: {analyzerResult.Command}"); - - var properties = analyzerResult.Properties; - foreach (var property in ImportantProperties) - { - log.AppendLine($"Property {property}={properties.GetValueOrDefault(property) ?? "\"'undefined'\""}"); - } - if (analyzerResult.SourceFiles.Length == 0) - { - log.AppendLine("** No source files identified **"); - } - else - { - log.AppendLine( - $"{analyzerResult.SourceFiles.Length} source files: {string.Join(',', analyzerResult.SourceFiles)}"); - } - if (analyzerResult.References.Length == 0) - { - log.AppendLine("** No references Identified **"); - } - else - { - foreach (var reference in analyzerResult.References) - { - log.AppendLine($"References: {Path.GetFileName(reference)} (in {Path.GetDirectoryName(reference)})"); - } - } - - if (_logger.IsEnabled(LogLevel.Trace)) - { - // dumps all other properties as well, as they can be useful for diagnosing build issues - foreach (var property in properties.Where( p => !ImportantProperties.Contains(p.Key) )) - { - log.AppendLine($"Property {property.Key}={property.Value.Replace(Environment.NewLine, "\\n")}"); - } - } - - log.AppendLine(); + DumpTestAnalyzerResult(log, analyzerResult); } } finally @@ -229,6 +190,50 @@ public void LogAnalyzerResult() } } + private void DumpTestAnalyzerResult(StringBuilder log, IAnalyzerResult analyzerResult) + { + log.AppendLine($"TargetFramework: {analyzerResult.TargetFramework}"); + log.AppendLine($"Succeeded: {analyzerResult.Succeeded}"); + log.AppendLine($"Compiler command: {analyzerResult.Command}"); + + var properties = analyzerResult.Properties; + foreach (var property in ImportantProperties) + { + log.AppendLine($"Property {property}={properties.GetValueOrDefault(property) ?? "\"'undefined'\""}"); + } + if (analyzerResult.SourceFiles.Length == 0) + { + log.AppendLine("** No source files identified **"); + } + else + { + log.AppendLine( + $"{analyzerResult.SourceFiles.Length} source files: {string.Join(',', analyzerResult.SourceFiles)}"); + } + if (analyzerResult.References.Length == 0) + { + log.AppendLine("** No references Identified **"); + } + else + { + foreach (var reference in analyzerResult.References) + { + log.AppendLine($"References: {Path.GetFileName(reference)} (in {Path.GetDirectoryName(reference)})"); + } + } + + if (_logger.IsEnabled(LogLevel.Trace)) + { + // dumps all other properties as well, as they can be useful for diagnosing build issues + foreach (var property in properties.Where( p => !ImportantProperties.Contains(p.Key) )) + { + log.AppendLine($"Property {property.Key}={property.Value.Replace(Environment.NewLine, "\\n")}"); + } + } + + log.AppendLine(); + } + public bool BuildsAnAssembly() => AnalyzerLastResults?.Any(p => p.BuildsAnAssembly()) == true; public bool FindMatchingVariant(string assemblyPath, out IAnalyzerResult analyzerResult) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs b/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs index 97802ca4a2..28e77996ca 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs @@ -115,7 +115,7 @@ public ProjectAnalyzerContext GetProjectAnalysisContext(string projectFile) { (configuration, platform) = (Configuration, Platform); } - return new ProjectAnalyzerContext(_buildalyzerProvider, projectFile, _options.MsBuildPath, configuration, - platform, _options.TargetFramework, _logger, this); + return new ProjectAnalyzerContext(_buildalyzerProvider, projectFile, _options.MsBuildPath, (configuration, + platform, _options.TargetFramework), _logger, this); } } diff --git a/src/Stryker.Utilities/Buildalyzer/IAnalyzerResultExtensions.cs b/src/Stryker.Utilities/Buildalyzer/IAnalyzerResultExtensions.cs index 08dad218b1..d4e6da272c 100644 --- a/src/Stryker.Utilities/Buildalyzer/IAnalyzerResultExtensions.cs +++ b/src/Stryker.Utilities/Buildalyzer/IAnalyzerResultExtensions.cs @@ -166,14 +166,7 @@ public static bool IsValid(this IAnalyzerResult br) => br.Succeeded || (br.Sourc /// 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; - - /// - /// Checks if a project analysis is at least partially successful - /// - /// Analysis results - /// true if analysis was successful - public static bool HasValidResult(this IAnalyzerResults br) => br.OverallSuccess || br.Results.Any(IsValid); + private static bool IsValidFor(this IAnalyzerResult br, string framework) => br.IsValid() && br.TargetFramework == framework; /// /// Checks is a project analysis is valid for all given target frameworks. If no target frameworks are given, it checks if the overall analysis was successful. From 5eb56ecfe9a13756431248a810bb5951928e20ba Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Tue, 31 Mar 2026 15:33:35 +0200 Subject: [PATCH 09/41] chore: Copilot complaints --- .../Stryker.Core/Initialisation/InputFileResolver.cs | 8 ++------ src/Stryker.Solutions/SolutionFile.cs | 6 +++++- .../Buildalyzer/IAnalyzerResultExtensions.cs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index 2f82f4316c..2d4c02c23c 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -40,7 +40,6 @@ public class InputFileResolver( private readonly ISolutionProvider _solutionProvider = solutionProvider ?? throw new ArgumentNullException(nameof(solutionProvider)); private readonly INugetRestoreProcess _nugetRestoreProcess = nugetRestoreProcess ?? throw new ArgumentNullException(nameof(nugetRestoreProcess)); - private readonly Dictionary _buildLogs = []; public IFileSystem FileSystem { get; } = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); @@ -350,7 +349,6 @@ private IAnalyzerResults AnalyzeSingleProject(ProjectAnalyzerContext project, IS _logger.LogDebug("Analyzing {ProjectFilePath}", projectLogName); var buildResult = project.Analyze(); - _buildLogs[projectLogName] = project.LastBuildLog; var buildResultOverallSuccess = project.HasValidResults(); // retry if it failed @@ -358,7 +356,7 @@ private IAnalyzerResults AnalyzeSingleProject(ProjectAnalyzerContext project, IS { if (options.DiagMode) { - _logger.LogWarning("Project {ProjectFilePath} analysis failed. The MsBuild log is: {Log}", projectLogName, _buildLogs[projectLogName]); + _logger.LogWarning("Project {ProjectFilePath} analysis failed. The MsBuild log is: {Log}", projectLogName, project.LastBuildLog); } // if this is a full framework project, we can retry after a nuget restore @@ -374,8 +372,6 @@ private IAnalyzerResults AnalyzeSingleProject(ProjectAnalyzerContext project, IS buildResultOverallSuccess = project.HasValidResults(); } - // store the build log - _buildLogs[projectLogName] = project.LastBuildLog; } if (options.DiagMode || _logger.IsEnabled(LogLevel.Debug)) @@ -396,7 +392,7 @@ private IAnalyzerResults AnalyzeSingleProject(ProjectAnalyzerContext project, IS if (options.DiagMode) { - _logger.LogWarning("Project {ProjectFilePath} analysis failed. The MsBuild log is: {Log}", projectLogName, _buildLogs[projectLogName]); + _logger.LogWarning("Project {ProjectFilePath} analysis failed. The MsBuild log is: {Log}", projectLogName, project.LastBuildLog); } return buildResult; diff --git a/src/Stryker.Solutions/SolutionFile.cs b/src/Stryker.Solutions/SolutionFile.cs index c9d413d4d0..d2b305f07c 100644 --- a/src/Stryker.Solutions/SolutionFile.cs +++ b/src/Stryker.Solutions/SolutionFile.cs @@ -155,7 +155,11 @@ private string GetEffectiveBuildType(string? buildType) { buildType = GetEffectiveBuildType(buildType); platform ??= DefaultPlatform; - return _configurations[(buildType, platform)] + if (!_configurations.TryGetValue((buildType, platform), out var projects)) + { + return []; + } + return projects .Select(p => (p.Key, p.Value.buildType, p.Value.platform)) .Distinct() .ToImmutableList(); diff --git a/src/Stryker.Utilities/Buildalyzer/IAnalyzerResultExtensions.cs b/src/Stryker.Utilities/Buildalyzer/IAnalyzerResultExtensions.cs index d4e6da272c..c88c5b27bf 100644 --- a/src/Stryker.Utilities/Buildalyzer/IAnalyzerResultExtensions.cs +++ b/src/Stryker.Utilities/Buildalyzer/IAnalyzerResultExtensions.cs @@ -169,7 +169,7 @@ public static bool IsValid(this IAnalyzerResult br) => br.Succeeded || (br.Sourc private static bool IsValidFor(this IAnalyzerResult br, string framework) => br.IsValid() && br.TargetFramework == framework; /// - /// Checks is a project analysis is valid for all given target frameworks. If no target frameworks are given, it checks if the overall analysis was successful. + /// Checks if a project analysis is valid for all given target frameworks. If no target frameworks are given, it checks if the overall analysis was successful. /// /// Analysis results. /// list of frameworks to check for From bd83103ed6cb13b8a6aab481502489cd7e322e47 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Tue, 31 Mar 2026 23:05:24 +0200 Subject: [PATCH 10/41] fix: address copilot --- .../Initialisation/InputFileResolver.cs | 1 - .../Initialisation/ProjectAnalyzerContext.cs | 49 +------------------ .../SolutionFileShould.cs | 2 +- src/Stryker.Solutions/SolutionFile.cs | 39 +++++++++++++-- 4 files changed, 36 insertions(+), 55 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index 2d4c02c23c..3b9972b5be 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -371,7 +371,6 @@ private IAnalyzerResults AnalyzeSingleProject(ProjectAnalyzerContext project, IS buildResult = project.Analyze(forceFramework: true); buildResultOverallSuccess = project.HasValidResults(); } - } if (options.DiagMode || _logger.IsEnabled(LogLevel.Debug)) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs index c6229213a3..02fb73e26c 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs @@ -6,7 +6,6 @@ using Buildalyzer; using Buildalyzer.Environment; using Microsoft.Extensions.Logging; -using Stryker.Abstractions.Exceptions; using Stryker.Utilities.Buildalyzer; namespace Stryker.Core.Initialisation; @@ -116,52 +115,6 @@ private void InitializeTargetFrameworks() public IEnumerable GetProjectReferences() => AnalyzerLastResults?.SelectMany(r => r.ProjectReferences).Distinct(); - public IAnalyzerResult SelectAnalyzerResult() - { - var validResults = AnalyzerLastResults.ToList(); - if (validResults.Count == 0) - { - throw new InputException($"No valid project analysis results could be found for '{ProjectFileName}'."); - } - - if (_framework is null) - { - // we try to avoid desktop versions - return PickFrameworkVersion(); - } - - var resultForRequestedFramework = validResults.Find(a => a.TargetFramework == _framework); - if (resultForRequestedFramework is not null) - { - return resultForRequestedFramework; - } - // if there is only one available framework version, we log an info - if (validResults.Count == 1) - { - var singleAnalyzerResult = validResults[0]; - _logger.LogInformation( - "Could not find a valid analysis for target {0} for project '{1}'. Selected version is {2}.", - _framework, ProjectFileName, singleAnalyzerResult.TargetFramework); - return singleAnalyzerResult; - } - - var firstAnalyzerResult = PickFrameworkVersion(); - var availableFrameworks = validResults.Select(a => a.TargetFramework).Distinct(); - _logger.LogWarning( - """ - Could not find a valid analysis for target {0} for project '{1}'. - The available target frameworks are: {2}. - selected version is {3}. - """, _framework, ProjectFileName, string.Join(',', availableFrameworks), firstAnalyzerResult.TargetFramework); - - return firstAnalyzerResult; - - IAnalyzerResult PickFrameworkVersion() - { - return validResults.Find(a => a.IsValid() && !a.TargetsFullFramework()) ?? validResults[0]; - } - } - private static readonly HashSet ImportantProperties = ["Configuration", "Platform", "AssemblyName", "Configurations", "TargetPath", "OS"]; @@ -236,7 +189,7 @@ private void DumpTestAnalyzerResult(StringBuilder log, IAnalyzerResult analyzerR public bool BuildsAnAssembly() => AnalyzerLastResults?.Any(p => p.BuildsAnAssembly()) == true; - public bool FindMatchingVariant(string assemblyPath, out IAnalyzerResult analyzerResult) + public bool FindMatchingVariant(string assemblyPath, out IAnalyzerResult? analyzerResult) { if (AnalyzerLastResults == null) { diff --git a/src/Stryker.Solutions.Test/SolutionFileShould.cs b/src/Stryker.Solutions.Test/SolutionFileShould.cs index 244398ae61..d04cdd9872 100644 --- a/src/Stryker.Solutions.Test/SolutionFileShould.cs +++ b/src/Stryker.Solutions.Test/SolutionFileShould.cs @@ -45,7 +45,7 @@ public void DetectPlatformIfNotSpecified(string platform) // Arrange List projects = ["Project.csproj", "Test.csproj"]; // Act - var solution = SolutionFile.BuildFromProjectList( projects, [platform]); + var solution = SolutionFile.BuildFromProjectList( projects, [platform], [platform]); // Assert solution.GetProjectsWithDetails("Debug").ShouldBe(projects.Select(p => (p, "Debug", platform))); diff --git a/src/Stryker.Solutions/SolutionFile.cs b/src/Stryker.Solutions/SolutionFile.cs index d2b305f07c..8c4b6668e3 100644 --- a/src/Stryker.Solutions/SolutionFile.cs +++ b/src/Stryker.Solutions/SolutionFile.cs @@ -173,7 +173,7 @@ private string GetEffectiveBuildType(string? buildType) /// requested configuration /// requested platform /// - public (string configuration, string platform) GetProjectConfiguration(string filename, string configuration, string platform) + public (string configuration, string platform) GetProjectConfiguration(string filename, string? configuration, string? platform) { configuration = GetEffectiveBuildType(configuration); platform ??= DefaultPlatform; @@ -189,30 +189,59 @@ private string GetEffectiveBuildType(string? buildType) /// /// list of csproj filenames /// list of declared platforms. Default to AnyCpu and x86 + /// list of solution level platforms /// a solution instance /// This method is used for testing purposes. It is mandatory as the underlying solution parser does not support any form of mocking - public static SolutionFile BuildFromProjectList(List projects, string[]? platforms = null) + public static SolutionFile BuildFromProjectList(List projects, string[]? platforms = null + , string[]? solutionPlatforms = null) { var result = new SolutionFile(); + solutionPlatforms = DefineSolutionPlatforms(platforms, solutionPlatforms); platforms ??= [ "AnyCPU", "x86" ]; + if (platforms.Length != solutionPlatforms.Length) + { + throw new ArgumentException("The number of platforms should be the same as the number of solution platforms, if specified."); + } // default to Debug|Any CPU string[] buildTypes = ["Debug", "Release"]; foreach (var buildType in buildTypes) { - foreach (var platform in platforms) + for (var index = 0; index < platforms.Length; index++) { + var platform = platforms[index]; var projectDict = new Dictionary(); foreach (var project in projects) { projectDict[project] = (buildType, platform); } - var solutionPlatform = platform == "AnyCPU" ? "Any CPU" : platform; - result._configurations.Add((buildType, solutionPlatform), projectDict); + + result._configurations.Add((buildType, solutionPlatforms[index]), projectDict); } } return result; } + private static string[] DefineSolutionPlatforms(string[]? platforms, string[]? solutionPlatforms) + { + if (solutionPlatforms == null) + { + if (platforms == null) + { + solutionPlatforms = [ "Any CPU", "x86" ]; + } + else + { + solutionPlatforms = new string[platforms.Length]; + for (var i = 0; i < platforms.Length; i++) + { + solutionPlatforms[i] = platforms[i] == DefaultProjectBuildType ? DefaultSolutionType : platforms[i]; + } + } + } + + return solutionPlatforms; + } + private static SolutionFile AnalyzeSolution(string solutionPath, SolutionModel solution) { // extract needed information From 4340ca611e0cbe568fa5da92ac82d6b808e34fbf Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Wed, 1 Apr 2026 07:39:12 +0200 Subject: [PATCH 11/41] misc: dump source code on fatal error during build --- .../Stryker.Core/Compiling/CSharpRollbackProcess.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Stryker.Core/Stryker.Core/Compiling/CSharpRollbackProcess.cs b/src/Stryker.Core/Stryker.Core/Compiling/CSharpRollbackProcess.cs index 7da7852bad..7d76409d21 100644 --- a/src/Stryker.Core/Stryker.Core/Compiling/CSharpRollbackProcess.cs +++ b/src/Stryker.Core/Stryker.Core/Compiling/CSharpRollbackProcess.cs @@ -70,6 +70,12 @@ public CSharpRollbackProcessResult Start(CSharpCompilation compiler, ImmutableAr if (updatedSyntaxTree == originalTree || lastAttempt) { + Logger.LogError("Failed to restore file {Filename} to be compilable. Aborting.", originalTree.FilePath); + if (devMode) + { + DumpBuildErrors(syntaxTreeMap); + Logger.LogDebug("Current code: {OriginalTree}", originalTree); + } Logger.LogCritical( "Stryker.NET could not compile the project after mutation. This is probably an error for Stryker.NET and not your project. Please report this issue on github with the previous error message."); throw new CompilationException("Internal error due to compile error."); From bf150b7447b5df6ed31ebd6099bea597d325c60d Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Wed, 1 Apr 2026 07:49:20 +0200 Subject: [PATCH 12/41] misc: Fix Sonar complaint --- .../Stryker.Core/Compiling/CSharpRollbackProcess.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core/Compiling/CSharpRollbackProcess.cs b/src/Stryker.Core/Stryker.Core/Compiling/CSharpRollbackProcess.cs index 7d76409d21..ba1713aea4 100644 --- a/src/Stryker.Core/Stryker.Core/Compiling/CSharpRollbackProcess.cs +++ b/src/Stryker.Core/Stryker.Core/Compiling/CSharpRollbackProcess.cs @@ -76,9 +76,7 @@ public CSharpRollbackProcessResult Start(CSharpCompilation compiler, ImmutableAr DumpBuildErrors(syntaxTreeMap); Logger.LogDebug("Current code: {OriginalTree}", originalTree); } - Logger.LogCritical( - "Stryker.NET could not compile the project after mutation. This is probably an error for Stryker.NET and not your project. Please report this issue on github with the previous error message."); - throw new CompilationException("Internal error due to compile error."); + throw new CompilationException("Stryker.NET could not compile the project after mutation. This is probably an error for Stryker.NET and not your project. Please report this issue on github with the previous error message."); } Logger.LogTrace("RolledBack to {UpdatedSyntaxTree}", updatedSyntaxTree.ToString()); From 5fa8dd5e2359c52a8ca82e8e745996b5c88dd0a8 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Wed, 1 Apr 2026 08:44:36 +0200 Subject: [PATCH 13/41] chore: Copilot --- .../Stryker.Core/Initialisation/InputFileResolver.cs | 1 + .../Initialisation/MutableProjectTarget.cs | 7 +++---- .../Initialisation/ProjectAnalyzerContext.cs | 10 +++++++++- .../Stryker.Core/Initialisation/TargetsForMutation.cs | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index 3b9972b5be..8005758ec8 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -320,6 +320,7 @@ private ConcurrentBag AnalyzeAllNeededProjects( } mutableProjectsAnalyzerResults.Add(projectAnalysisContext); + // recursively scan dependencies only if enabled and current project is a test project if (mode == ScanMode.NoScan || (mode == ScanMode.ScanTestProjectReferences && !projectAnalysisContext.IsTestProject())) { diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTarget.cs b/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTarget.cs index 46e8aa3fab..3479cd973a 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTarget.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTarget.cs @@ -64,11 +64,10 @@ public SourceProjectInfo BuildSourceProjectInfo(IStrykerOptions options, public void DumpForAnalysis() { - var targetProjectTarget = ProjectTarget; logger.LogInformation(" target {TargetFramework} analysis {Result}, simulated build {BuildResult}.", - targetProjectTarget.TargetFramework, - targetProjectTarget.IsValid() ? "succeeded" : "failed", - targetProjectTarget.Succeeded ? "succeeded" : "failed" + ProjectTarget.TargetFramework, + ProjectTarget.IsValid() ? "succeeded" : "failed", + ProjectTarget.Succeeded ? "succeeded" : "failed" ); if (TestProjects.Count == 0) { diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs index 02fb73e26c..00a765d9fb 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs @@ -146,7 +146,8 @@ public void LogAnalyzerResult() private void DumpTestAnalyzerResult(StringBuilder log, IAnalyzerResult analyzerResult) { log.AppendLine($"TargetFramework: {analyzerResult.TargetFramework}"); - log.AppendLine($"Succeeded: {analyzerResult.Succeeded}"); + log.AppendLine($"Simulated build: {(analyzerResult.Succeeded ? "succeeded": "failed")}"); + log.AppendLine($"Stryker analysis: {(analyzerResult.IsValid() ? "succeeded": "failed")}"); log.AppendLine($"Compiler command: {analyzerResult.Command}"); var properties = analyzerResult.Properties; @@ -175,6 +176,13 @@ private void DumpTestAnalyzerResult(StringBuilder log, IAnalyzerResult analyzerR } } + if (analyzerResult.ReferenceAliases.Count > 0) + { + foreach (var (alias, assemblies) in analyzerResult.ReferenceAliases) + { + log.AppendLine($"Aliases: {alias} => {string.Join(", ", assemblies)}"); + } + } if (_logger.IsEnabled(LogLevel.Trace)) { // dumps all other properties as well, as they can be useful for diagnosing build issues diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs b/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs index 28e77996ca..eb696ab12c 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs @@ -32,7 +32,7 @@ public TargetsForMutation(SolutionFile? file, IStrykerOptions options, IBuildaly private SolutionFile Solution { get; init; } - public string SolutionFilePath => Solution?.FileName; + public string? SolutionFilePath => Solution?.FileName; public string Configuration { get; private set; } From 0ed49dc314da1492f217b568fd75ef3354011dd7 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Wed, 1 Apr 2026 09:01:19 +0200 Subject: [PATCH 14/41] chore: add missing mock entry --- .../Initialisation/BuildAnalyzerTestsBase.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/BuildAnalyzerTestsBase.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/BuildAnalyzerTestsBase.cs index 5a2ebf09ea..4c4f665ce7 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/BuildAnalyzerTestsBase.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/BuildAnalyzerTestsBase.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Reflection; @@ -254,6 +255,7 @@ internal Mock BuildProjectAnalyzerMock(string csprojPathName, Select( iar => GetProjectResult(iar, framework).GetAssemblyPath()).Union(rawReferences).ToArray()); } + projectAnalyzerResultMock.Setup(x => x.ReferenceAliases).Returns(new Dictionary>().ToImmutableDictionary()); projectAnalyzerResultMock.Setup(x => x.SourceFiles).Returns(sourceFiles); projectAnalyzerResultMock.Setup(x => x.PackageReferences).Returns(new Dictionary>()); projectAnalyzerResultMock.Setup(x => x.PreprocessorSymbols).Returns(["NET"]); From 9cd9c8b4bb61b2cce627fbde15c61e741fdda904 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Wed, 1 Apr 2026 15:00:26 +0200 Subject: [PATCH 15/41] fix: fix issues with Microsot solution --- .../Initialisation/ProjectAnalyzerContext.cs | 19 +++++++++---------- src/Stryker.Solutions/SolutionFile.cs | 4 ++++ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs index 00a765d9fb..dae53a27d1 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs @@ -148,7 +148,6 @@ private void DumpTestAnalyzerResult(StringBuilder log, IAnalyzerResult analyzerR log.AppendLine($"TargetFramework: {analyzerResult.TargetFramework}"); log.AppendLine($"Simulated build: {(analyzerResult.Succeeded ? "succeeded": "failed")}"); log.AppendLine($"Stryker analysis: {(analyzerResult.IsValid() ? "succeeded": "failed")}"); - log.AppendLine($"Compiler command: {analyzerResult.Command}"); var properties = analyzerResult.Properties; foreach (var property in ImportantProperties) @@ -172,18 +171,18 @@ private void DumpTestAnalyzerResult(StringBuilder log, IAnalyzerResult analyzerR { foreach (var reference in analyzerResult.References) { - log.AppendLine($"References: {Path.GetFileName(reference)} (in {Path.GetDirectoryName(reference)})"); + var aliasText = string.Empty; + if (analyzerResult.ReferenceAliases.TryGetValue(reference, out var aliases) && aliases.Length > 0) + { + aliasText = $" aliases: {string.Join(", ", aliases)}"; + } + log.AppendLine($"References: {Path.GetFileName(reference)}{aliasText} (in {Path.GetDirectoryName(reference)})"); } } - if (analyzerResult.ReferenceAliases.Count > 0) - { - foreach (var (alias, assemblies) in analyzerResult.ReferenceAliases) - { - log.AppendLine($"Aliases: {alias} => {string.Join(", ", assemblies)}"); - } - } - if (_logger.IsEnabled(LogLevel.Trace)) + log.AppendLine($"Compiler command: {analyzerResult.Command}"); + + if (_logger.IsEnabled(LogLevel.Trace) && false) { // dumps all other properties as well, as they can be useful for diagnosing build issues foreach (var property in properties.Where( p => !ImportantProperties.Contains(p.Key) )) diff --git a/src/Stryker.Solutions/SolutionFile.cs b/src/Stryker.Solutions/SolutionFile.cs index 8c4b6668e3..20380be183 100644 --- a/src/Stryker.Solutions/SolutionFile.cs +++ b/src/Stryker.Solutions/SolutionFile.cs @@ -260,6 +260,10 @@ private static SolutionFile AnalyzeSolution(string solutionPath, SolutionModel s continue; } + if (projectPlatform == DefaultSolutionType) + { + projectPlatform = DefaultProjectBuildType; + } projects[solutionProject.FilePath] = (projectBuildType, projectPlatform); } if (projects.Count == 0) From 1a0a0ac33530f9cd59371bdcb9d95eb2e1d7ae80 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Wed, 1 Apr 2026 23:34:01 +0200 Subject: [PATCH 16/41] misc: dump msbuild log when simulated build failed for debug purposes --- .../Stryker.Core/Initialisation/InputFileResolver.cs | 5 +++++ src/Stryker.Solutions/SolutionFile.cs | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index 8005758ec8..756a4361d8 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -373,6 +373,11 @@ private IAnalyzerResults AnalyzeSingleProject(ProjectAnalyzerContext project, IS buildResultOverallSuccess = project.HasValidResults(); } } + else if (!buildResult.OverallSuccess) + { + _logger.LogInformation("Project {ProjectFilePath} simulated build failed. The MsBuild log is: {Log}", projectLogName, project.LastBuildLog); + } + if (options.DiagMode || _logger.IsEnabled(LogLevel.Debug)) { diff --git a/src/Stryker.Solutions/SolutionFile.cs b/src/Stryker.Solutions/SolutionFile.cs index 20380be183..927eaf8be6 100644 --- a/src/Stryker.Solutions/SolutionFile.cs +++ b/src/Stryker.Solutions/SolutionFile.cs @@ -85,7 +85,7 @@ public bool ConfigurationExists(string buildType, string? platform = null) return (currentBuildType, currentPlatform); } - var thisScore = -1; + int thisScore; // evaluate the buildType match if (buildType != null && buildType == currentBuildType) { @@ -259,7 +259,7 @@ private static SolutionFile AnalyzeSolution(string solutionPath, SolutionModel s { continue; } - + // workaround for a bug in SolutionPersistence which does not properly handle default platform naming. if (projectPlatform == DefaultSolutionType) { projectPlatform = DefaultProjectBuildType; From 00b9739f8220117c909ca981df4e79eba0bead51 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Thu, 2 Apr 2026 08:34:10 +0200 Subject: [PATCH 17/41] chore: sonar --- .../Initialisation/InputFileResolver.cs | 5 +-- .../Initialisation/ProjectAnalyzerContext.cs | 36 +++++++++++------ src/Stryker.Solutions/SolutionFile.cs | 40 +++++++++++-------- 3 files changed, 48 insertions(+), 33 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index 756a4361d8..8c0258feba 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -378,7 +378,6 @@ private IAnalyzerResults AnalyzeSingleProject(ProjectAnalyzerContext project, IS _logger.LogInformation("Project {ProjectFilePath} simulated build failed. The MsBuild log is: {Log}", projectLogName, project.LastBuildLog); } - if (options.DiagMode || _logger.IsEnabled(LogLevel.Debug)) { project.LogAnalyzerResult(); @@ -457,6 +456,7 @@ private static bool ScanAssemblyReferences(Dictionary mutableToTestMap, ProjectAnalyzerContext[] mutableProjects, ProjectAnalyzerContext testProject) { @@ -489,9 +489,6 @@ private static bool ScanProjectReferences(Dictionary !ImportantProperties.Contains(p.Key) )) + { + log.AppendLine($"Property {property.Key}={property.Value.Replace(Environment.NewLine, "\\n")}"); + } + } + + log.AppendLine(); + } + + private static void DumpSourceFiles(StringBuilder log, IAnalyzerResult analyzerResult) + { if (analyzerResult.SourceFiles.Length == 0) { log.AppendLine("** No source files identified **"); @@ -163,6 +182,10 @@ private void DumpTestAnalyzerResult(StringBuilder log, IAnalyzerResult analyzerR log.AppendLine( $"{analyzerResult.SourceFiles.Length} source files: {string.Join(',', analyzerResult.SourceFiles)}"); } + } + + private static void DumpReferences(StringBuilder log, IAnalyzerResult analyzerResult) + { if (analyzerResult.References.Length == 0) { log.AppendLine("** No references Identified **"); @@ -179,19 +202,6 @@ private void DumpTestAnalyzerResult(StringBuilder log, IAnalyzerResult analyzerR log.AppendLine($"References: {Path.GetFileName(reference)}{aliasText} (in {Path.GetDirectoryName(reference)})"); } } - - log.AppendLine($"Compiler command: {analyzerResult.Command}"); - - if (_logger.IsEnabled(LogLevel.Trace) && false) - { - // dumps all other properties as well, as they can be useful for diagnosing build issues - foreach (var property in properties.Where( p => !ImportantProperties.Contains(p.Key) )) - { - log.AppendLine($"Property {property.Key}={property.Value.Replace(Environment.NewLine, "\\n")}"); - } - } - - log.AppendLine(); } public bool BuildsAnAssembly() => AnalyzerLastResults?.Any(p => p.BuildsAnAssembly()) == true; diff --git a/src/Stryker.Solutions/SolutionFile.cs b/src/Stryker.Solutions/SolutionFile.cs index 927eaf8be6..e38e0e0a2f 100644 --- a/src/Stryker.Solutions/SolutionFile.cs +++ b/src/Stryker.Solutions/SolutionFile.cs @@ -250,22 +250,7 @@ private static SolutionFile AnalyzeSolution(string solutionPath, SolutionModel s { 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; - } - // workaround for a bug in SolutionPersistence which does not properly handle default platform naming. - if (projectPlatform == DefaultSolutionType) - { - projectPlatform = DefaultProjectBuildType; - } - projects[solutionProject.FilePath] = (projectBuildType, projectPlatform); - } + var projects = ExtractProjectForGivenConfiguration(solution, buildType, solutionPlatform); if (projects.Count == 0) { continue; @@ -277,6 +262,29 @@ private static SolutionFile AnalyzeSolution(string solutionPath, SolutionModel s return result; } + private static Dictionary ExtractProjectForGivenConfiguration(SolutionModel solution, string buildType, + string solutionPlatform) + { + 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; + } + // workaround for a bug in SolutionPersistence which does not properly handle default platform naming. + if (projectPlatform == DefaultSolutionType) + { + projectPlatform = DefaultProjectBuildType; + } + projects[solutionProject.FilePath] = (projectBuildType, projectPlatform); + } + + return projects; + } + /// /// Loads a solution file from disk /// From 0742f65ef6f45479ad5a06ce68a201d3163ef63b Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Thu, 2 Apr 2026 12:45:39 +0200 Subject: [PATCH 18/41] fix: incorrect configuration extration from solution info --- .../Initialisation/TargetsForMutation.cs | 3 +- src/Stryker.Solutions/SolutionFile.cs | 34 +++++++++++-------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs b/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs index eb696ab12c..195dd6049d 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs @@ -54,7 +54,6 @@ private void SelectConfiguration() { // we use the solution to determine the configuration and platform to use, as the project files may not contain all configurations and platforms that are defined in the solution (Configuration, Platform) = Solution.GetMatching(configuration, platform); - _logger.LogDebug("Using solution configuration/platform '{Configuration}|{Platform}'.", Configuration, Platform); if ((!string.IsNullOrEmpty(configuration) && configuration != Configuration) || (!string.IsNullOrEmpty(platform) && platform != Platform)) { @@ -71,7 +70,7 @@ private void SelectConfiguration() Configuration = configuration; // "Any CPU" is default platform at solution level, but in project files it is "AnyCPU", so we need to convert it to match the project files Platform = platform == "Any CPU" ? "AnyCPU" : platform; - _logger.LogDebug("Using project configuration/platform '{Configuration}|{Platform}'.", Configuration, Platform); + _logger.LogInformation("Using project configuration/platform '{Configuration}|{Platform}'.", Configuration??"`default`", Platform ?? "`default`"); } } diff --git a/src/Stryker.Solutions/SolutionFile.cs b/src/Stryker.Solutions/SolutionFile.cs index e38e0e0a2f..54be267c6c 100644 --- a/src/Stryker.Solutions/SolutionFile.cs +++ b/src/Stryker.Solutions/SolutionFile.cs @@ -223,19 +223,21 @@ public static SolutionFile BuildFromProjectList(List projects, string[]? private static string[] DefineSolutionPlatforms(string[]? platforms, string[]? solutionPlatforms) { - if (solutionPlatforms == null) + if (solutionPlatforms != null) { - if (platforms == null) - { - solutionPlatforms = [ "Any CPU", "x86" ]; - } - else + return solutionPlatforms; + } + + if (platforms == null) + { + solutionPlatforms = [ "Any CPU", "x86" ]; + } + else + { + solutionPlatforms = new string[platforms.Length]; + for (var i = 0; i < platforms.Length; i++) { - solutionPlatforms = new string[platforms.Length]; - for (var i = 0; i < platforms.Length; i++) - { - solutionPlatforms[i] = platforms[i] == DefaultProjectBuildType ? DefaultSolutionType : platforms[i]; - } + solutionPlatforms[i] = platforms[i] == DefaultProjectBuildType ? DefaultSolutionType : platforms[i]; } } @@ -246,11 +248,12 @@ private static SolutionFile AnalyzeSolution(string solutionPath, SolutionModel s { // extract needed information var result = new SolutionFile{ FileName = solutionPath }; + var referencePath = Path.GetDirectoryName(Path.GetFullPath(solutionPath)); foreach (var buildType in solution.BuildTypes) { foreach (var solutionPlatform in solution.Platforms) { - var projects = ExtractProjectForGivenConfiguration(solution, buildType, solutionPlatform); + var projects = ExtractProjectForGivenConfiguration(solution, referencePath, buildType, solutionPlatform); if (projects.Count == 0) { continue; @@ -262,13 +265,16 @@ private static SolutionFile AnalyzeSolution(string solutionPath, SolutionModel s return result; } - private static Dictionary ExtractProjectForGivenConfiguration(SolutionModel solution, string buildType, + private static Dictionary ExtractProjectForGivenConfiguration(SolutionModel solution, + string path, + string buildType, string solutionPlatform) { var projects = new Dictionary(); // add all projects that are built with this configuration foreach (var solutionProject in solution.SolutionProjects) { + var fullPath = Path.Combine(path, solutionProject.FilePath); var (projectBuildType, projectPlatform, isBuilt, _) = solutionProject.GetProjectConfiguration(buildType, solutionPlatform); if (!isBuilt || projectBuildType == null || projectPlatform == null) { @@ -279,7 +285,7 @@ private static SolutionFile AnalyzeSolution(string solutionPath, SolutionModel s { projectPlatform = DefaultProjectBuildType; } - projects[solutionProject.FilePath] = (projectBuildType, projectPlatform); + projects[fullPath] = (projectBuildType, projectPlatform); } return projects; From 963ff2244e5c5b15aea54b422cd0b4fe413d0696 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Thu, 2 Apr 2026 13:33:06 +0200 Subject: [PATCH 19/41] fix: failing unit tests --- .../SolutionFileShould.cs | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/src/Stryker.Solutions.Test/SolutionFileShould.cs b/src/Stryker.Solutions.Test/SolutionFileShould.cs index d04cdd9872..e355539f7a 100644 --- a/src/Stryker.Solutions.Test/SolutionFileShould.cs +++ b/src/Stryker.Solutions.Test/SolutionFileShould.cs @@ -80,30 +80,30 @@ public void ProvideProjectListForGivenConfiguration() { // Arrange // Act - var solution = SolutionFile.GetSolution(Path.Combine("..","..","..","..","Stryker.slnx")); + var solution = SolutionFile.GetSolution(Path.GetFullPath(Path.Combine("..","..","..","..","Stryker.slnx"))); // Assert solution.ConfigurationExists("Debug", "Any CPU").ShouldBeTrue(); - + var solutionPath = Path.GetDirectoryName(solution.FileName); // 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.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.Configuration", "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"), - Path.Combine("Stryker.TestRunner.MicrosoftTestPlatform", "Stryker.TestRunner.MicrosoftTestPlatform.csproj"), - Path.Combine("Stryker.TestRunner.MicrosoftTestPlatform.UnitTest", "Stryker.TestRunner.MicrosoftTestPlatform.UnitTest.csproj"), + Path.Combine(solutionPath, "Stryker.CLI", "Stryker.CLI", "Stryker.CLI.csproj"), + Path.Combine(solutionPath, "Stryker.CLI", "Stryker.CLI.UnitTest", "Stryker.CLI.UnitTest.csproj"), + Path.Combine(solutionPath, "Stryker.Core", "Stryker.Core", "Stryker.Core.csproj"), + Path.Combine(solutionPath, "Stryker.Core", "Stryker.Core.UnitTest", "Stryker.Core.UnitTest.csproj"), + Path.Combine(solutionPath, "Stryker.DataCollector", "Stryker.DataCollector.csproj"), + Path.Combine(solutionPath, "Stryker.RegexMutators", "Stryker.RegexMutators", "Stryker.RegexMutators.csproj"), + Path.Combine(solutionPath, "Stryker.RegexMutators", "Stryker.RegexMutators.UnitTest", "Stryker.RegexMutators.UnitTest.csproj"), + Path.Combine(solutionPath, "Stryker.Abstractions", "Stryker.Abstractions.csproj"), + Path.Combine(solutionPath, "Stryker.Configuration", "Stryker.Configuration.csproj"), + Path.Combine(solutionPath, "Stryker.Utilities", "Stryker.Utilities.csproj"), + Path.Combine(solutionPath, "Stryker.TestRunner", "Stryker.TestRunner.csproj"), + Path.Combine(solutionPath, "Stryker.TestRunner.VsTest", "Stryker.TestRunner.VsTest.csproj"), + Path.Combine(solutionPath, "Stryker.TestRunner.VsTest.UnitTest", "Stryker.TestRunner.VsTest.UnitTest.csproj"), + Path.Combine(solutionPath, "Stryker.Solutions", "Stryker.Solutions.csproj"), + Path.Combine(solutionPath, "Stryker.Solutions.Test", "Stryker.Solutions.Test.csproj"), + Path.Combine(solutionPath, "Stryker.TestRunner.MicrosoftTestPlatform", "Stryker.TestRunner.MicrosoftTestPlatform.csproj"), + Path.Combine(solutionPath, "Stryker.TestRunner.MicrosoftTestPlatform.UnitTest", "Stryker.TestRunner.MicrosoftTestPlatform.UnitTest.csproj"), }; solution.GetProjects("Debug").ShouldBe(expectedProjects, ignoreOrder: true); } @@ -115,17 +115,18 @@ public void ProvideProjectListForGivenConfigurationOnSolutionWithMultiplePlatfor { // Arrange // Act - var solution = SolutionFile.GetSolution(Path.Combine("..","..","..","..","..","integrationtest","TargetProjects",solutionFile)); + var solution = SolutionFile.GetSolution( Path.GetFullPath(Path.Combine("..","..","..","..","..","integrationtest","TargetProjects",solutionFile))); + var solutionPath = Path.GetDirectoryName(solution.FileName); // Assert var expectedProjects = new List { - Path.Combine("NetCore", "TargetProject", "TargetProject.csproj"), - Path.Combine("NetCore", "Library", "Library.csproj"), - Path.Combine("MicrosoftTestPlatform", "UnitTests.MSTest", "UnitTests.MSTest.csproj"), - Path.Combine("MicrosoftTestPlatform", "UnitTests.XUnit", "UnitTests.XUnit.csproj"), - Path.Combine("MicrosoftTestPlatform", "UnitTests.NUnit", "UnitTests.NUnit.csproj"), - Path.Combine("MicrosoftTestPlatform", "UnitTests.TUnit", "UnitTests.TUnit.csproj"), + Path.Combine(solutionPath, "NetCore", "TargetProject", "TargetProject.csproj"), + Path.Combine(solutionPath, "NetCore", "Library", "Library.csproj"), + Path.Combine(solutionPath, "MicrosoftTestPlatform", "UnitTests.MSTest", "UnitTests.MSTest.csproj"), + Path.Combine(solutionPath, "MicrosoftTestPlatform", "UnitTests.XUnit", "UnitTests.XUnit.csproj"), + Path.Combine(solutionPath, "MicrosoftTestPlatform", "UnitTests.NUnit", "UnitTests.NUnit.csproj"), + Path.Combine(solutionPath, "MicrosoftTestPlatform", "UnitTests.TUnit", "UnitTests.TUnit.csproj"), }; solution.GetProjects("Debug").ShouldBe(expectedProjects, ignoreOrder: true); From 345f608acedabaf2838ad97ae7e6d09e4d16ddd9 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Thu, 2 Apr 2026 20:40:23 +0200 Subject: [PATCH 20/41] Update src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Initialisation/TargetsForMutation.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs b/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs index 195dd6049d..d04cc25613 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs @@ -90,7 +90,21 @@ internal void RestoreSolution(IAnalyzerResults results) if (Environment.OSVersion.Platform == PlatformID.Win32NT) { _logger.LogWarning("Project analysis failed. Stryker will retry after a solution level nuget restore"); - _nugetRestoreProcess.RestorePackages(_options.SolutionPath, _options.MsBuildPath ?? results.First().MsBuildPath()); + + var msBuildPath = !string.IsNullOrWhiteSpace(_options.MsBuildPath) + ? _options.MsBuildPath + : results + .Select(result => result.MsBuildPath()) + .FirstOrDefault(path => !string.IsNullOrWhiteSpace(path)); + + if (string.IsNullOrWhiteSpace(msBuildPath)) + { + throw new InvalidOperationException( + "Failed to determine a valid MSBuild path required to restore NuGet packages. " + + "Specify a valid MSBuild path in the Stryker options or ensure the solution can be analyzed to infer it."); + } + + _nugetRestoreProcess.RestorePackages(_options.SolutionPath, msBuildPath); } _solutionRestored = true; From 171da10c5510806d64cd93de2754192e089f72d3 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Thu, 2 Apr 2026 20:41:00 +0200 Subject: [PATCH 21/41] wip --- .../Stryker.Core/Initialisation/InputFileResolver.cs | 8 +++----- .../Stryker.Core/Initialisation/MutableProjectTarget.cs | 2 +- .../Stryker.Core/Initialisation/TargetsForMutation.cs | 7 ++++++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index 8c0258feba..43e3f26221 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -355,10 +355,7 @@ private IAnalyzerResults AnalyzeSingleProject(ProjectAnalyzerContext project, IS // retry if it failed if (!buildResultOverallSuccess) { - if (options.DiagMode) - { - _logger.LogWarning("Project {ProjectFilePath} analysis failed. The MsBuild log is: {Log}", projectLogName, project.LastBuildLog); - } + _logger.LogWarning("Project {ProjectFilePath} analysis failed. Trying again with a nugget restore.", projectLogName); // if this is a full framework project, we can retry after a nuget restore buildResult = project.Analyze(withRestore: true); @@ -369,11 +366,12 @@ private IAnalyzerResults AnalyzeSingleProject(ProjectAnalyzerContext project, IS if (!buildResultOverallSuccess && !string.IsNullOrEmpty(options.TargetFramework)) { // still failed, we can try using target framework option + _logger.LogWarning("Project {ProjectFilePath} analysis failed. Last attempt, forcing the target framework.", projectLogName); buildResult = project.Analyze(forceFramework: true); buildResultOverallSuccess = project.HasValidResults(); } } - else if (!buildResult.OverallSuccess) + if (!buildResult.OverallSuccess) { _logger.LogInformation("Project {ProjectFilePath} simulated build failed. The MsBuild log is: {Log}", projectLogName, project.LastBuildLog); } diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTarget.cs b/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTarget.cs index 3479cd973a..a91d33f60f 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTarget.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTarget.cs @@ -21,7 +21,7 @@ internal class MutableProjectTarget(IAnalyzerResult target, ILogger logger) { public IAnalyzerResult ProjectTarget { get; } = target; - public List TestProjects { get; } = []; + public HashSet TestProjects { get; } = []; public bool IsValidTarget => ProjectTarget.IsValid() && TestProjects.Count>0 && TestProjects.Any(tp => tp.IsValid()); diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs b/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs index 195dd6049d..dce0fd5c4a 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs @@ -90,7 +90,12 @@ internal void RestoreSolution(IAnalyzerResults results) if (Environment.OSVersion.Platform == PlatformID.Win32NT) { _logger.LogWarning("Project analysis failed. Stryker will retry after a solution level nuget restore"); - _nugetRestoreProcess.RestorePackages(_options.SolutionPath, _options.MsBuildPath ?? results.First().MsBuildPath()); + var optionsMsBuildPath = _options.MsBuildPath ?? results.First().MsBuildPath(); + if (string.IsNullOrEmpty(optionsMsBuildPath)) + { + _logger.LogWarning("Failed to find MSBuild path from analysis results. Nuget restore may fail if MSBuild is not in PATH."); + } + _nugetRestoreProcess.RestorePackages(_options.SolutionPath, optionsMsBuildPath); } _solutionRestored = true; From 109ef99f5b029bbd1d7be4219cf5c364cffe2b60 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Thu, 2 Apr 2026 20:46:56 +0200 Subject: [PATCH 22/41] wip: improve build logging --- .../Initialisation/InputFileResolver.cs | 8 +++++ .../Initialisation/TargetsForMutation.cs | 32 +++++++++++-------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index 43e3f26221..9359a469fe 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -432,6 +432,10 @@ private IAnalyzerResults AnalyzeSingleProject(ProjectAnalyzerContext project, IS private static bool ScanAssemblyReferences(Dictionary mutableToTestMap, ProjectAnalyzerContext[] mutableProjects, ProjectAnalyzerContext testProject) { + if (testProject.AnalyzerLastResults == null) + { + throw new InvalidOperationException($"You must analyze the test project {testProject.ProjectFileName} before trying to find its references."); + } var foundOneProject = false; // we do the work for each available target foreach (var variant in testProject.AnalyzerLastResults) @@ -458,6 +462,10 @@ private static bool ScanAssemblyReferences(Dictionary mutableToTestMap, ProjectAnalyzerContext[] mutableProjects, ProjectAnalyzerContext testProject) { + if (testProject.AnalyzerLastResults == null) + { + throw new InvalidOperationException($"You must analyze the test project {testProject.ProjectFileName} before trying to find its references."); + } var foundOneProject = false; foreach (var variant in testProject.AnalyzerLastResults) { diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs b/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs index dce0fd5c4a..6a52d3c1b1 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs @@ -30,7 +30,7 @@ public TargetsForMutation(SolutionFile? file, IStrykerOptions options, IBuildaly } - private SolutionFile Solution { get; init; } + private SolutionFile? Solution { get; } public string? SolutionFilePath => Solution?.FileName; @@ -38,7 +38,7 @@ public TargetsForMutation(SolutionFile? file, IStrykerOptions options, IBuildaly public string Platform { get; private set; } - public string TargetFramework { get; set; } + public string? TargetFramework { get; set; } public int ProjectCount => _selectedProjects.Count; @@ -82,23 +82,27 @@ private void SelectConfiguration() // the method is at solution level because it needs only be called once internal void RestoreSolution(IAnalyzerResults results) { - if (_solutionRestored) + lock (_nugetRestoreProcess) { - return; - } + if (_solutionRestored) + { + return; + } - if (Environment.OSVersion.Platform == PlatformID.Win32NT) - { - _logger.LogWarning("Project analysis failed. Stryker will retry after a solution level nuget restore"); - var optionsMsBuildPath = _options.MsBuildPath ?? results.First().MsBuildPath(); - if (string.IsNullOrEmpty(optionsMsBuildPath)) + if (Environment.OSVersion.Platform == PlatformID.Win32NT) { - _logger.LogWarning("Failed to find MSBuild path from analysis results. Nuget restore may fail if MSBuild is not in PATH."); + _logger.LogWarning("Project analysis failed. Stryker will retry after a solution level nuget restore"); + var optionsMsBuildPath = _options.MsBuildPath ?? results.First().MsBuildPath(); + if (string.IsNullOrEmpty(optionsMsBuildPath)) + { + _logger.LogWarning("Failed to find MSBuild path from analysis results. Nuget restore may fail if MSBuild is not in PATH."); + } + _nugetRestoreProcess.RestorePackages(_options.SolutionPath, optionsMsBuildPath); } - _nugetRestoreProcess.RestorePackages(_options.SolutionPath, optionsMsBuildPath); - } - _solutionRestored = true; + _solutionRestored = true; + + } } /// From ee4d9c8996ee643c12893e6c48f865cf38ebc7d7 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Thu, 2 Apr 2026 22:54:45 +0200 Subject: [PATCH 23/41] chore: change retry logic --- .../Stryker.Core/Initialisation/InputFileResolver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index 9359a469fe..9416c14fb5 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -353,7 +353,7 @@ private IAnalyzerResults AnalyzeSingleProject(ProjectAnalyzerContext project, IS var buildResultOverallSuccess = project.HasValidResults(); // retry if it failed - if (!buildResultOverallSuccess) + if (!buildResult.OverallSuccess) { _logger.LogWarning("Project {ProjectFilePath} analysis failed. Trying again with a nugget restore.", projectLogName); From 190377ab6d88ba653bf24ed4537a0eae7b823396 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Thu, 2 Apr 2026 23:31:57 +0200 Subject: [PATCH 24/41] chore: refine error reporting for analysis --- .../Stryker.Core/Compiling/CSharpRollbackProcess.cs | 2 +- .../Stryker.Core/Initialisation/InputFileResolver.cs | 8 +++++--- .../Stryker.Core/Initialisation/MutableProjectTarget.cs | 6 +++--- .../Stryker.Core/Initialisation/ProjectAnalyzerContext.cs | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core/Compiling/CSharpRollbackProcess.cs b/src/Stryker.Core/Stryker.Core/Compiling/CSharpRollbackProcess.cs index ba1713aea4..ebf5d60bfa 100644 --- a/src/Stryker.Core/Stryker.Core/Compiling/CSharpRollbackProcess.cs +++ b/src/Stryker.Core/Stryker.Core/Compiling/CSharpRollbackProcess.cs @@ -76,7 +76,7 @@ public CSharpRollbackProcessResult Start(CSharpCompilation compiler, ImmutableAr DumpBuildErrors(syntaxTreeMap); Logger.LogDebug("Current code: {OriginalTree}", originalTree); } - throw new CompilationException("Stryker.NET could not compile the project after mutation. This is probably an error for Stryker.NET and not your project. Please report this issue on github with the previous error message."); + throw new CompilationException("Stryker.NET could not compile the project after mutation. This is probably an error for Stryker.NET and not your project. Please report this issue on Github with the previous error message."); } Logger.LogTrace("RolledBack to {UpdatedSyntaxTree}", updatedSyntaxTree.ToString()); diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index 9416c14fb5..34e4966803 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -352,10 +352,11 @@ private IAnalyzerResults AnalyzeSingleProject(ProjectAnalyzerContext project, IS var buildResult = project.Analyze(); var buildResultOverallSuccess = project.HasValidResults(); - // retry if it failed + // if buildalyzer failed, we can try again with a nuget restore, as missing packages is a common cause of + // buildalyzer failure, especially for full framework projects if (!buildResult.OverallSuccess) { - _logger.LogWarning("Project {ProjectFilePath} analysis failed. Trying again with a nugget restore.", projectLogName); + _logger.LogWarning("Project {ProjectFilePath} analysis failed. Trying again with a nuget restore.", projectLogName); // if this is a full framework project, we can retry after a nuget restore buildResult = project.Analyze(withRestore: true); @@ -383,7 +384,8 @@ private IAnalyzerResults AnalyzeSingleProject(ProjectAnalyzerContext project, IS if (buildResultOverallSuccess) { - _logger.LogDebug("Analysis of project {projectFilePath} succeeded.", projectLogName); + _logger.LogDebug("Analysis of project {ProjectFilePath} succeeded{Extra}", projectLogName, + buildResult.OverallSuccess ? "." : " but simulated build failed, Stryker may fail later."); return buildResult; } diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTarget.cs b/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTarget.cs index a91d33f60f..3fb587b92e 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTarget.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTarget.cs @@ -31,11 +31,11 @@ internal class MutableProjectTarget(IAnalyzerResult target, ILogger logger) /// Builds a instance describing a project its associated test project(s) /// /// Stryker options - /// + /// /// filesystem /// public SourceProjectInfo BuildSourceProjectInfo(IStrykerOptions options, - TargetsForMutation solutionInfo, IFileSystem fileSystem ) + TargetsForMutation targetsForMutation, IFileSystem fileSystem ) { var targetProjectInfo = new SourceProjectInfo { @@ -53,7 +53,7 @@ public SourceProjectInfo BuildSourceProjectInfo(IStrykerOptions options, builder.InjectHelpers(inputFiles); targetProjectInfo.OnProjectBuilt = builder.PostBuildAction(); targetProjectInfo.ProjectContents = inputFiles; - targetProjectInfo.TargetsForMutation = solutionInfo; + targetProjectInfo.TargetsForMutation = targetsForMutation; logger.LogInformation("Found project {ProjectFileName} to mutate.", ProjectTarget.ProjectFilePath); targetProjectInfo.TestProjectsInfo = new TestProjectsInfo(fileSystem) { diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs index b56378f8ac..8d6e20541a 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs @@ -104,8 +104,8 @@ private void InitializeTargetFrameworks() _targetFrameworks = projectFileTargetFrameworks; } - public IEnumerable FailedFrameworks => _targetFrameworks.Where(tf => - !AnalyzerLastResults.Any( ar => ar.TargetFramework == tf && ar.IsValid())); + public IEnumerable FailedFrameworks => _targetFrameworks?.Where(tf => + AnalyzerLastResults?.Any( ar => ar.TargetFramework == tf && ar.IsValid())== false) ?? []; public bool IsTest => AnalyzerLastResults?.IsTestProject() == true; From fc61c27fc133b28b438ecbd3128aefc7596669dd Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Tue, 7 Apr 2026 11:55:45 +0200 Subject: [PATCH 25/41] chore: renanming and minor redesign --- .../Initialisation/BuildAnalyzerTestsBase.cs | 1 + .../InitialisationProcessTests.cs | 39 ++++----- .../Initialisation/ProjectMutatorTests.cs | 22 ++--- .../MutationTest/MutationTestProcessTests.cs | 3 +- .../CsharpProjectComponentsBuilder.cs | 38 ++++----- .../Initialisation/InitialisationProcess.cs | 18 +--- .../Initialisation/InputFileResolver.cs | 82 +++++++++++-------- .../Initialisation/MutableProjectTarget.cs | 16 ++-- .../Initialisation/MutableProjectTree.cs | 19 ++--- .../Initialisation/ProjectMutator.cs | 2 +- .../Initialisation/ProjectOrchestrator.cs | 2 +- ...ext.cs => ProjectSimulatedBuildHandler.cs} | 53 ++++++------ ...rgetsForMutation.cs => ProjectsTracker.cs} | 46 +++++++++-- .../MutationTest/MutationTestInput.cs | 10 +-- .../MutationTest/MutationTestProcess.cs | 4 +- .../SourceProjects/SourceProjectInfo.cs | 6 +- .../Stryker.Core/StrykerRunner.cs | 2 +- .../VsTestMockingHelper.cs | 3 - .../Buildalyzer/IAnalyzerResultExtensions.cs | 7 +- src/Stryker.slnx | 13 +++ 20 files changed, 216 insertions(+), 170 deletions(-) rename src/Stryker.Core/Stryker.Core/Initialisation/{ProjectAnalyzerContext.cs => ProjectSimulatedBuildHandler.cs} (80%) rename src/Stryker.Core/Stryker.Core/Initialisation/{TargetsForMutation.cs => ProjectsTracker.cs} (69%) diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/BuildAnalyzerTestsBase.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/BuildAnalyzerTestsBase.cs index 4c4f665ce7..6ad1847c62 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/BuildAnalyzerTestsBase.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/BuildAnalyzerTestsBase.cs @@ -290,6 +290,7 @@ internal Mock BuildProjectAnalyzerMock(string csprojPathName, projectFileMock.Setup(x => x.Path).Returns(csprojPathName); projectFileMock.Setup(x => x.Name).Returns(FileSystem.Path.GetFileName(csprojPathName)); projectFileMock.Setup(x=> x.TargetFrameworks).Returns(frameworks.ToArray() ); + projectFileMock.Setup(x => x.RequiresNetFramework).Returns(frameworks.Any(f => f.StartsWith("net") && !f.StartsWith("netcoreapp") && !f.StartsWith("netstandard"))); return projectAnalyzerMock; } diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InitialisationProcessTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InitialisationProcessTests.cs index 3f5ba23af5..7e61fd2094 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InitialisationProcessTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InitialisationProcessTests.cs @@ -73,12 +73,12 @@ public async Task InitialisationProcess_ShouldThrowOnFailedInitialTestRun() var folder = new CsharpFolderComposite(); folder.Add(new CsharpFileLeaf()); + var loggerMock = new Mock>(); var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); + var projectTracker = new ProjectsTracker(null, new StrykerOptions(), null, null, null, loggerMock.Object); inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())).Returns( - new[] {new SourceProjectInfo() { - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult( - references: Array.Empty()).Object, - TestProjectsInfo = new TestProjectsInfo(new MockFileSystem()) - }}); + [new SourceProjectInfo { AnalyzerResult = TestHelper.SetupProjectAnalyzerResult(references: []).Object, + TestProjectsInfo = new TestProjectsInfo(new MockFileSystem()), ProjectsTracker = projectTracker} + ]); inputFileResolverMock.SetupGet(x => x.FileSystem).Returns(new FileSystem()); initialBuildProcessMock.Setup(x => x.InitialBuild(It.IsAny(), @@ -88,7 +88,6 @@ public async Task InitialisationProcess_ShouldThrowOnFailedInitialTestRun() testRunnerMock.Setup(x => x.DiscoverTestsAsync(It.IsAny())).Returns(Task.FromResult(true)); initialTestProcessMock.Setup(x => x.InitialTestAsync(It.IsAny(), It.IsAny(), It.IsAny())).ThrowsAsync(new InputException("")); // failing test - var loggerMock = new Mock>(); var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); var options = new StrykerOptions { ProjectName = "TheProjectName", @@ -113,9 +112,12 @@ public async Task InitialisationProcess_ShouldThrowIfHalfTestsAreFailing() var folder = new CsharpFolderComposite(); folder.Add(new CsharpFileLeaf()); - + var loggerMock = new Mock>(); var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); + var projectTracker = new ProjectsTracker(null, new StrykerOptions(), null, null, null, loggerMock.Object); inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())).Returns( - new[] { new SourceProjectInfo { AnalyzerResult = TestHelper.SetupProjectAnalyzerResult(references: Array.Empty()).Object, TestProjectsInfo = new TestProjectsInfo(new MockFileSystem()) } }); + [new SourceProjectInfo { AnalyzerResult = TestHelper.SetupProjectAnalyzerResult(references: []).Object, + TestProjectsInfo = new TestProjectsInfo(new MockFileSystem()), ProjectsTracker = projectTracker} + ]); inputFileResolverMock.SetupGet(x => x.FileSystem).Returns(fileSystemMock); initialBuildProcessMock.Setup(x => x.InitialBuild(It.IsAny(), @@ -135,7 +137,6 @@ public async Task InitialisationProcess_ShouldThrowIfHalfTestsAreFailing() new InitialTestRun( new TestRunResult(Array.Empty(), ranTests, failedTests, TestIdentifierList.NoTest(), string.Empty, Enumerable.Empty(), TimeSpan.Zero), new TimeoutValueCalculator(0))); // failing test - var loggerMock = new Mock>(); var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); var options = new StrykerOptions { ProjectName = "TheProjectName", @@ -161,18 +162,17 @@ public async Task InitialisationProcess_ShouldThrowOnTestTestIfAskedFor(bool bre var folder = new CsharpFolderComposite(); folder.Add(new CsharpFileLeaf()); - + var loggerMock = new Mock>(); var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); + var projectTracker = new ProjectsTracker(null, new StrykerOptions(), null, null, null, loggerMock.Object); inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())).Returns( - new[] {new SourceProjectInfo { - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult( - references: Array.Empty()).Object, - TestProjectsInfo = new TestProjectsInfo(new MockFileSystem()) - }}); + [new SourceProjectInfo { AnalyzerResult = TestHelper.SetupProjectAnalyzerResult(references: []).Object, + TestProjectsInfo = new TestProjectsInfo(new MockFileSystem()), ProjectsTracker = projectTracker} + ]); inputFileResolverMock.SetupGet(x => x.FileSystem).Returns(new FileSystem()); initialBuildProcessMock.Setup(x => x.InitialBuild(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), null, It.IsAny())); - var failedTest = "testid"; + const string failedTest = "testid"; var ranTests = new TestIdentifierList(failedTest, "othertest", "anothertest"); var testSet = new TestSet(); foreach (var ranTest in ranTests.GetIdentifiers()) @@ -185,7 +185,6 @@ public async Task InitialisationProcess_ShouldThrowOnTestTestIfAskedFor(bool bre initialTestProcessMock.Setup(x => x.InitialTestAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(new InitialTestRun( new TestRunResult(Array.Empty(), ranTests, failedTests, TestIdentifierList.NoTest(), string.Empty, Enumerable.Empty(), TimeSpan.Zero), new TimeoutValueCalculator(0))); // failing test - var loggerMock = new Mock>(); var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); var options = new StrykerOptions { ProjectName = "TheProjectName", @@ -218,8 +217,11 @@ public async Task InitialisationProcess_ShouldRunTestSession() var folder = new CsharpFolderComposite(); folder.Add(new CsharpFileLeaf()); + var loggerMock = new Mock>(); var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); + var projectTracker = new ProjectsTracker(null, new StrykerOptions(), null, null, null, loggerMock.Object); inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())).Returns( - new[] { new SourceProjectInfo() { AnalyzerResult = TestHelper.SetupProjectAnalyzerResult(references: Array.Empty()).Object, TestProjectsInfo = new TestProjectsInfo(new MockFileSystem()) } }); + [new SourceProjectInfo { AnalyzerResult = TestHelper.SetupProjectAnalyzerResult(references: []).Object, TestProjectsInfo = new TestProjectsInfo(new MockFileSystem()), ProjectsTracker = projectTracker} + ]); inputFileResolverMock.SetupGet(x => x.FileSystem).Returns(new FileSystem()); initialBuildProcessMock.Setup(x => x.InitialBuild(It.IsAny(), It.IsAny(), @@ -231,7 +233,6 @@ public async Task InitialisationProcess_ShouldRunTestSession() initialTestProcessMock.Setup(x => x.InitialTestAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.FromResult(new InitialTestRun(new TestRunResult(true), null))); // failing test - var loggerMock = new Mock>(); var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); var options = new StrykerOptions { ProjectName = "TheProjectName", diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/ProjectMutatorTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/ProjectMutatorTests.cs index 6decd2550d..02f8d74ecc 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/ProjectMutatorTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/ProjectMutatorTests.cs @@ -84,16 +84,16 @@ public ProjectMutatorTests() SourceProjectInfo = new Stryker.Core.ProjectComponents.SourceProjects.SourceProjectInfo() { AnalyzerResult = analyzerResult, - ProjectContents = folder - }, - TestProjectsInfo = new TestProjectsInfo(_fileSystemMock) - { - TestProjects = new List + ProjectContents = folder, + TestProjectsInfo = new TestProjectsInfo(_fileSystemMock) { - new(_fileSystemMock, TestHelper.SetupProjectAnalyzerResult( - projectFilePath: "c:\\testproject.csproj", - targetFramework: "netcoreapp3.1", - sourceFiles: new [] { _testFilePath }).Object) + TestProjects = new List + { + new(_fileSystemMock, TestHelper.SetupProjectAnalyzerResult( + projectFilePath: "c:\\testproject.csproj", + targetFramework: "netcoreapp3.1", + sourceFiles: [_testFilePath]).Object) + } } } }; @@ -126,7 +126,7 @@ public void ShouldInitializeEachProjectInSolution() executedTests: new TestIdentifierList(failedTest, successfulTest), failedTests: new TestIdentifierList(failedTest), timedOutTest: TestIdentifierList.NoTest(), - message: "testrun succesful", + message: "testrun successful", Enumerable.Empty(), timeSpan: TimeSpan.FromSeconds(2)); @@ -138,7 +138,7 @@ public void ShouldInitializeEachProjectInSolution() // assert result.ShouldNotBeNull(); - var testFile = _mutationTestInput.TestProjectsInfo.TestFiles.ShouldHaveSingleItem(); + var testFile = _mutationTestInput.SourceProjectInfo.TestProjectsInfo.TestFiles.ShouldHaveSingleItem(); testFile.Tests.Count.ShouldBe(2); } } diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/MutationTest/MutationTestProcessTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/MutationTest/MutationTestProcessTests.cs index f2e3789c86..a2ff29f733 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/MutationTest/MutationTestProcessTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/MutationTest/MutationTestProcessTests.cs @@ -68,8 +68,7 @@ public MutationTestProcessTests() }).Object, ProjectContents = Folder, TestProjectsInfo = testProjectsInfo - }, - TestProjectsInfo = testProjectsInfo, + } }; } diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/CsharpProjectComponentsBuilder.cs b/src/Stryker.Core/Stryker.Core/Initialisation/CsharpProjectComponentsBuilder.cs index c4b38f34dd..e12022b2d3 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/CsharpProjectComponentsBuilder.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/CsharpProjectComponentsBuilder.cs @@ -37,13 +37,13 @@ public CsharpProjectComponentsBuilder(SourceProjectInfo projectInfo, IStrykerOpt public override IReadOnlyProjectComponent Build() { CsharpFolderComposite inputFiles; - if (_projectInfo.AnalyzerResult.SourceFiles != null && _projectInfo.AnalyzerResult.SourceFiles.Any()) + if (_projectInfo.AnalyzerResult.SourceFiles != null && _projectInfo.AnalyzerResult.SourceFiles.Length != 0) { inputFiles = FindProjectFilesUsingBuildalyzer(_projectInfo.AnalyzerResult, _options); } else { - _logger.LogWarning("Buildalyzer could not find sourcefiles. This should not happen. We fallback to filesystem scan. Please report an issue at github."); + _logger.LogWarning("Buildalyzer could not find source files. This should not happen. We fallback to filesystem scan. Please report an issue at github."); inputFiles = FindProjectFilesScanningProjectFolders(_projectInfo.AnalyzerResult); } return inputFiles; @@ -53,11 +53,11 @@ public override IReadOnlyProjectComponent Build() private CsharpFolderComposite FindProjectFilesScanningProjectFolders(IAnalyzerResult analyzerResult) { var inputFiles = new CsharpFolderComposite(); - var sourceProjectDir = Path.GetDirectoryName(analyzerResult.ProjectFilePath); + var sourceProjectDir = FileSystem.Path.GetDirectoryName(analyzerResult.ProjectFilePath); var cSharpParseOptions = analyzerResult.GetParseOptions(_options); foreach (var dir in ExtractProjectFolders(analyzerResult)) { - var folder = FileSystem.Path.Combine(Path.GetDirectoryName(sourceProjectDir), dir); + var folder = FileSystem.Path.Combine(FileSystem.Path.GetDirectoryName(sourceProjectDir), dir); _logger.LogDebug("Scanning {Folder}", folder); inputFiles.Add(FindInputFiles(folder, sourceProjectDir, analyzerResult, cSharpParseOptions)); } @@ -71,10 +71,11 @@ public override void InjectHelpers(IReadOnlyProjectComponent inputFiles) private CsharpFolderComposite FindProjectFilesUsingBuildalyzer(IAnalyzerResult analyzerResult, IStrykerOptions options) { var generatedAssemblyInfo = analyzerResult.AssemblyAttributeFileName(); + var sourceProjectDir = FileSystem.Path.GetDirectoryName(analyzerResult.ProjectFilePath); var projectUnderTestFolderComposite = new CsharpFolderComposite() { - FullPath = Path.GetDirectoryName(analyzerResult.ProjectFilePath), - RelativePath = Path.GetDirectoryName(Path.GetDirectoryName(analyzerResult.ProjectFilePath)), + FullPath = sourceProjectDir, + RelativePath = FileSystem.Path.GetDirectoryName(sourceProjectDir), }; var cache = new Dictionary { [string.Empty] = projectUnderTestFolderComposite }; @@ -83,8 +84,8 @@ private CsharpFolderComposite FindProjectFilesUsingBuildalyzer(IAnalyzerResult a foreach (var sourceFile in analyzerResult.SourceFiles) { - var relativePath = Path.GetRelativePath(Path.GetDirectoryName(analyzerResult.ProjectFilePath), sourceFile); - var folderComposite = GetOrBuildFolderComposite(cache, Path.GetDirectoryName(relativePath), Path.GetDirectoryName(analyzerResult.ProjectFilePath), projectUnderTestFolderComposite); + var relativePath = FileSystem.Path.GetRelativePath(sourceProjectDir, sourceFile); + var folderComposite = GetOrBuildFolderComposite(cache, FileSystem.Path.GetDirectoryName(relativePath), sourceProjectDir, projectUnderTestFolderComposite); var file = new CsharpFileLeaf() { @@ -100,7 +101,7 @@ private CsharpFolderComposite FindProjectFilesUsingBuildalyzer(IAnalyzerResult a if (syntaxTree.IsGenerated()) { // we found the generated assemblyinfo file - if (FileSystem.Path.GetFileName(sourceFile).ToLowerInvariant() == generatedAssemblyInfo) + if (FileSystem.Path.GetFileName(sourceFile).Equals(generatedAssemblyInfo, StringComparison.InvariantCultureIgnoreCase)) { // add the mutated text syntaxTree = InjectMutationLabel(syntaxTree); @@ -118,11 +119,11 @@ private CsharpFolderComposite FindProjectFilesUsingBuildalyzer(IAnalyzerResult a public override Action PostBuildAction() => () => ScanPackageContentFiles(_projectInfo.AnalyzerResult, (CsharpFolderComposite)_projectInfo.ProjectContents); - public void ScanPackageContentFiles(IAnalyzerResult analyzerResult, CsharpFolderComposite projectUnderTestFolderComposite) + private void ScanPackageContentFiles(IAnalyzerResult analyzerResult, CsharpFolderComposite projectUnderTestFolderComposite) { // look for extra source files coming from Nuget packages var folder = analyzerResult.GetProperty("ContentPreprocessorOutputDirectory"); - var sourceProjectDir = Path.GetDirectoryName(analyzerResult.ProjectFilePath); + var sourceProjectDir = FileSystem.Path.GetDirectoryName(analyzerResult.ProjectFilePath); if (string.IsNullOrEmpty(folder)) { return; @@ -169,13 +170,12 @@ private CsharpFolderComposite FindInputFiles(string path, string sourceProjectDi { var rootFolderComposite = new CsharpFolderComposite { - FullPath = Path.GetFullPath(path), - RelativePath = Path.GetRelativePath(sourceProjectDir, Path.GetFullPath(path)) + FullPath = FileSystem.Path.GetFullPath(path), + RelativePath = FileSystem.Path.GetRelativePath(sourceProjectDir, FileSystem.Path.GetFullPath(path)) }; - rootFolderComposite.Add( - FindInputFiles(path, Path.GetDirectoryName(analyzerResult.ProjectFilePath), cSharpParseOptions) + FindInputFiles(path, FileSystem.Path.GetDirectoryName(analyzerResult.ProjectFilePath), cSharpParseOptions) ); return rootFolderComposite; } @@ -189,11 +189,11 @@ private CsharpFolderComposite FindInputFiles(string path, string sourceProjectDi var folderComposite = new CsharpFolderComposite { - FullPath = Path.GetFullPath(path), - RelativePath = Path.GetRelativePath(sourceProjectDir, Path.GetFullPath(path)) + FullPath = FileSystem.Path.GetFullPath(path), + RelativePath = FileSystem.Path.GetRelativePath(sourceProjectDir, FileSystem.Path.GetFullPath(path)) }; - foreach (var folder in FileSystem.Directory.EnumerateDirectories(folderComposite.FullPath).Where(x => !_foldersToExclude.Contains(Path.GetFileName(x)))) + foreach (var folder in FileSystem.Directory.EnumerateDirectories(folderComposite.FullPath).Where(x => !_foldersToExclude.Contains(FileSystem.Path.GetFileName(x)))) { folderComposite.Add(FindInputFiles(folder, sourceProjectDir, cSharpParseOptions, mutate)); } @@ -207,7 +207,7 @@ private CsharpFolderComposite FindInputFiles(string path, string sourceProjectDi { SourceCode = FileSystem.File.ReadAllText(file), FullPath = file, - RelativePath = Path.GetRelativePath(sourceProjectDir, file) + RelativePath = FileSystem.Path.GetRelativePath(sourceProjectDir, file) }; // Get the syntax tree for the source file diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InitialisationProcess.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InitialisationProcess.cs index dd8e189075..b96176abac 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InitialisationProcess.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InitialisationProcess.cs @@ -66,23 +66,12 @@ public IReadOnlyCollection GetMutableProjectsInfo(IStrykerOpt /// public void BuildProjects(IStrykerOptions options, IEnumerable projects) { - var solutionInfo = projects.First().TargetsForMutation; + var solutionInfo = projects.First().ProjectsTracker; // pick configuration and platform from solution if available - var configuration = solutionInfo?.Configuration ?? options.Configuration; - var platform = solutionInfo?.Platform ?? options.Platform; - var solutionFilePath = solutionInfo?.SolutionFilePath ?? options.SolutionPath; // we build the whole solution if we have a solution file path, even in project mode - if (!string.IsNullOrEmpty(solutionFilePath)) + if (!string.IsNullOrEmpty(solutionInfo.SolutionFilePath)) { - var framework = projects.Any(p => p.IsFullFramework); - // Build the complete solution - _logger.LogInformation("Building solution {SolutionPathName}.", FileSystem.Path.GetRelativePath(options.WorkingDirectory, solutionFilePath)); - - _initialBuildProcess.InitialBuild( - framework, - _inputFileResolver.FileSystem.Path.GetDirectoryName(solutionFilePath), - solutionFilePath, configuration, platform, - options.TargetFramework, options.MsBuildPath); + solutionInfo.BuildSolution(_initialBuildProcess, projects.Select(p => p.AnalyzerResult)); } else { @@ -120,7 +109,6 @@ public async Task> GetMutationTestInputsA { var getInputs = projects.Select(async info => new MutationTestInput { SourceProjectInfo = info, - TestProjectsInfo = info.TestProjectsInfo, TestRunner = runner, InitialTestRun = await InitialTestAsync(options, info, runner, projects.Count == 1) }); diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index 34e4966803..a9295cfa04 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -83,13 +83,13 @@ public IReadOnlyCollection ResolveSourceProjectInfos(IStryker } } - var solutionInfo = new TargetsForMutation(solution, options, _analyzerProvider ,_logger, _nugetRestoreProcess) + var solutionInfo = new ProjectsTracker(solution, options, _analyzerProvider, _nugetRestoreProcess, FileSystem, _logger) { TargetFramework = options.TargetFramework }; return options.IsSolutionContext ? FindProjectsInSolutionMode(options, solutionInfo, normalizedProjectUnderTestNameFilter) : FindProjectInTargetProjectMode(options, solutionInfo, normalizedProjectUnderTestNameFilter); } - private IReadOnlyCollection FindProjectsInSolutionMode(IStrykerOptions options, TargetsForMutation solutionInfo, + private IReadOnlyCollection FindProjectsInSolutionMode(IStrykerOptions options, ProjectsTracker solutionInfo, string normalizedProjectUnderTestNameFilter) { _logger.LogInformation("Identifying projects to mutate in {Solution}. This can take a while.", @@ -105,7 +105,7 @@ private IReadOnlyCollection FindProjectsInSolutionMode(IStryk ScanMode.NoScan); // we identify target projects and their associated test projects var (findMutableAnalyzerResults, orphanedProjects) = - FindMutableAnalyzerResults(mutableProjectsAnalyzerResults); + ExtractMutableProjectTrees(mutableProjectsAnalyzerResults); // we keep only suitable candidates return AnalyzeAndIdentifyProjects(options, solutionInfo, findMutableAnalyzerResults, orphanedProjects); } @@ -118,7 +118,7 @@ private IReadOnlyCollection FindProjectsInSolutionMode(IStryk /// solution file if any /// name filter to apply to the mutated projects /// identified mutable projects matching the provided name filter (when provided). Can be empty - private List FindProjectInTargetProjectMode(IStrykerOptions options, TargetsForMutation solution, + private List FindProjectInTargetProjectMode(IStrykerOptions options, ProjectsTracker solution, string normalizedProjectUnderTestNameFilter) { // we analyze the test project(s) and identify the project to be mutated @@ -150,7 +150,7 @@ private List FindProjectInTargetProjectMode(IStrykerOptions o var analyzeAllNeededProjects = AnalyzeAllNeededProjects(solution, normalizedProjectUnderTestNameFilter, options, ScanMode.ScanTestProjectReferences); // we match test projects to mutable projects - var (findMutableAnalyzerResults, orphans) = FindMutableAnalyzerResults(analyzeAllNeededProjects); + var (findMutableAnalyzerResults, orphans) = ExtractMutableProjectTrees(analyzeAllNeededProjects); var result = AnalyzeAndIdentifyProjects(options, solution, findMutableAnalyzerResults, orphans); return SelectSingleProject(normalizedProjectUnderTestNameFilter, result, targetProjectMode, testProjectFileNames); @@ -219,9 +219,9 @@ private enum ScanMode // analyze projects, do same for their upstream dependencies if activated, and identify which one(s) // to proceed with private List AnalyzeAndIdentifyProjects(IStrykerOptions options, - TargetsForMutation solutionInfo, + ProjectsTracker solutionInfo, List findMutableAnalyzerResults, - List unusedTestProjects) + List unusedTestProjects) { // build all projects _logger.LogDebug("Scanning {Count} possible targets.", findMutableAnalyzerResults.Count); @@ -259,14 +259,14 @@ private List AnalyzeAndIdentifyProjects(IStrykerOptions optio // Log the analysis results private void LogAnalysis(List findMutableAnalyzerResults, - List unusedTestProjects, bool optionsDiagMode) + List unusedTestProjects, bool optionsDiagMode) { if (findMutableAnalyzerResults.Count == 0) { _logger.LogWarning( optionsDiagMode ? "No project found, check settings and ensure project file is not corrupted.": """ No project found, check settings and ensure project file is not corrupted. - Use --diag option to have the analysis logs in the log file. + Use --diag option to have the simulated build logs. """); return; } @@ -277,7 +277,7 @@ private void LogAnalysis(List findMutableAnalyzerResults, // dump test projects that do not reference any mutable project foreach (var unusedTestProject in unusedTestProjects) { - _logger.LogInformation("Test project {ProjectName} does not appear to test any mutable project, analysis {Result}.", + _logger.LogInformation("Test project {ProjectName} does not appear to test any mutable project, simulated build {Result}.", unusedTestProject.ProjectFileName, unusedTestProject.HasValidResults() ? "succeeded" : "failed"); } @@ -288,12 +288,12 @@ private void LogAnalysis(List findMutableAnalyzerResults, } } - private ConcurrentBag AnalyzeAllNeededProjects( - TargetsForMutation solutionInfo, + private ConcurrentBag AnalyzeAllNeededProjects( + ProjectsTracker solutionInfo, string normalizedProjectUnderTestNameFilter, IStrykerOptions options, ScanMode mode) { - var mutableProjectsAnalyzerResults = new ConcurrentBag(); + var mutableProjectsAnalyzerResults = new ConcurrentBag(); var list = new DynamicEnumerableQueue(solutionInfo.SelectedProjects); try @@ -344,7 +344,7 @@ private ConcurrentBag AnalyzeAllNeededProjects( return mutableProjectsAnalyzerResults; } - private IAnalyzerResults AnalyzeSingleProject(ProjectAnalyzerContext project, IStrykerOptions options) + private IAnalyzerResults AnalyzeSingleProject(ProjectSimulatedBuildHandler project, IStrykerOptions options) { var projectLogName = FileSystem.Path.GetRelativePath(options.WorkingDirectory, project.ProjectFileName); _logger.LogDebug("Analyzing {ProjectFilePath}", projectLogName); @@ -354,9 +354,15 @@ private IAnalyzerResults AnalyzeSingleProject(ProjectAnalyzerContext project, IS // if buildalyzer failed, we can try again with a nuget restore, as missing packages is a common cause of // buildalyzer failure, especially for full framework projects - if (!buildResult.OverallSuccess) + if (buildResult.All(ar=>!ar.Succeeded)) { - _logger.LogWarning("Project {ProjectFilePath} analysis failed. Trying again with a nuget restore.", projectLogName); + if (project.IsNetFramework && Environment.OSVersion.Platform!=PlatformID.Win32NT) + { + _logger.LogWarning("Project {ProjectFilePath} is a .NET Framework project. It requires Windows for mutation testing. Discarding.", projectLogName); + return buildResult; + } + + _logger.LogWarning("Project {ProjectFilePath} simulated build failed. Trying again with a nuget restore.", projectLogName); // if this is a full framework project, we can retry after a nuget restore buildResult = project.Analyze(withRestore: true); @@ -367,7 +373,7 @@ private IAnalyzerResults AnalyzeSingleProject(ProjectAnalyzerContext project, IS if (!buildResultOverallSuccess && !string.IsNullOrEmpty(options.TargetFramework)) { // still failed, we can try using target framework option - _logger.LogWarning("Project {ProjectFilePath} analysis failed. Last attempt, forcing the target framework.", projectLogName); + _logger.LogWarning("Project {ProjectFilePath} simulated build failed again. Last attempt, forcing the target framework.", projectLogName); buildResult = project.Analyze(forceFramework: true); buildResultOverallSuccess = project.HasValidResults(); } @@ -396,24 +402,35 @@ private IAnalyzerResults AnalyzeSingleProject(ProjectAnalyzerContext project, IS if (options.DiagMode) { - _logger.LogWarning("Project {ProjectFilePath} analysis failed. The MsBuild log is: {Log}", projectLogName, project.LastBuildLog); + _logger.LogWarning("{ProjectFilePath}'s build log is: {Log}", projectLogName, project.LastBuildLog); } return buildResult; } - private (List, List) FindMutableAnalyzerResults( - IEnumerable mutableProjectsAnalyzerResults) + private (List, List) ExtractMutableProjectTrees( + IEnumerable mutableProjectsAnalyzerResults) { // separate test projects from mutable projects, and keep only analyzer results building an assembly (exclude solution folders and such) - var analyzerTestProjects = mutableProjectsAnalyzerResults.Where(p => p.IsTest && p.BuildsAnAssembly()); - var mutableProjects = mutableProjectsAnalyzerResults.Where(p => !p.IsTest && p.BuildsAnAssembly()).ToArray(); - + var scan = mutableProjectsAnalyzerResults.Where(p=> p.BuildsAnAssembly()).GroupBy(p => p.IsTest); + var testProjects = new List(); + var mutableProjects = new List(); + foreach (var project in mutableProjectsAnalyzerResults) + { + if (project.IsTestProject()) + { + testProjects.Add(project); + } + else if (project.BuildsAnAssembly()) + { + mutableProjects.Add(project); + } + } var mutableToTestMap = mutableProjects.ToDictionary(p =>p, p => new MutableProjectTree(p, _logger)); - var unusedTestProjects = new List(); + var unusedTestProjects = new List(); // for each test project - foreach (var testProject in analyzerTestProjects) + foreach (var testProject in testProjects) { if (ScanAssemblyReferences(mutableToTestMap, mutableProjects, testProject)) { @@ -431,10 +448,10 @@ private IAnalyzerResults AnalyzeSingleProject(ProjectAnalyzerContext project, IS return (mutableToTestMap.Values.ToList(), unusedTestProjects); } - private static bool ScanAssemblyReferences(Dictionary mutableToTestMap, - ProjectAnalyzerContext[] mutableProjects, ProjectAnalyzerContext testProject) + private static bool ScanAssemblyReferences(Dictionary mutableToTestMap, + List mutableProjects, ProjectSimulatedBuildHandler testProject) { - if (testProject.AnalyzerLastResults == null) + if (testProject.AnalyzerLastResults.Count == 0) { throw new InvalidOperationException($"You must analyze the test project {testProject.ProjectFileName} before trying to find its references."); } @@ -461,10 +478,10 @@ private static bool ScanAssemblyReferences(Dictionary mutableToTestMap, - ProjectAnalyzerContext[] mutableProjects, ProjectAnalyzerContext testProject) + private static bool ScanProjectReferences(Dictionary mutableToTestMap, + List mutableProjects, ProjectSimulatedBuildHandler testProject) { - if (testProject.AnalyzerLastResults == null) + if (testProject.AnalyzerLastResults.Count == 0) { throw new InvalidOperationException($"You must analyze the test project {testProject.ProjectFileName} before trying to find its references."); } @@ -474,8 +491,7 @@ private static bool ScanProjectReferences(Dictionary p.ProjectFileName == projectReference); - var candidateProjectVariants = candidateProject - ?.AnalyzerLastResults; + var candidateProjectVariants = candidateProject?.AnalyzerLastResults; if (candidateProjectVariants == null) { // probably another test project diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTarget.cs b/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTarget.cs index 3fb587b92e..bfa04789b3 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTarget.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTarget.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.IO.Abstractions; using System.Linq; using Buildalyzer; @@ -31,11 +32,11 @@ internal class MutableProjectTarget(IAnalyzerResult target, ILogger logger) /// Builds a instance describing a project its associated test project(s) /// /// Stryker options - /// + /// /// filesystem /// public SourceProjectInfo BuildSourceProjectInfo(IStrykerOptions options, - TargetsForMutation targetsForMutation, IFileSystem fileSystem ) + ProjectsTracker projectsTracker, IFileSystem fileSystem ) { var targetProjectInfo = new SourceProjectInfo { @@ -44,7 +45,7 @@ public SourceProjectInfo BuildSourceProjectInfo(IStrykerOptions options, var language = targetProjectInfo.AnalyzerResult.GetLanguage(); - ProjectComponentsBuilder builder = language == Language.Csharp + var builder = language == Language.Csharp ? new CsharpProjectComponentsBuilder(targetProjectInfo, options, FoldersToExclude, logger, fileSystem) : throw new NotSupportedException($"Language not supported: {language}"); @@ -53,7 +54,7 @@ public SourceProjectInfo BuildSourceProjectInfo(IStrykerOptions options, builder.InjectHelpers(inputFiles); targetProjectInfo.OnProjectBuilt = builder.PostBuildAction(); targetProjectInfo.ProjectContents = inputFiles; - targetProjectInfo.TargetsForMutation = targetsForMutation; + targetProjectInfo.ProjectsTracker = projectsTracker; logger.LogInformation("Found project {ProjectFileName} to mutate.", ProjectTarget.ProjectFilePath); targetProjectInfo.TestProjectsInfo = new TestProjectsInfo(fileSystem) { @@ -81,20 +82,19 @@ public void DumpForAnalysis() foreach (var testProject in TestProjects) { logger.LogInformation(" referenced by test project {ProjectName}, analysis {Result}.", - testProject.ProjectFilePath, + Path.GetFileName(testProject.ProjectFilePath), testProject.IsValid() ? "succeeded" : "failed"); - } // dump associated test projects // provide synthetic status if (TestProjects.Any(r => r.IsValid())) { - logger.LogInformation(" can be mutated."); + logger.LogInformation(ProjectTarget.IsValid() ? " can be mutated." : " can't be mutated because its simulated build failed."); } else { - logger.LogWarning(" can't be mutated because all referencing test projects' analysis failed."); + logger.LogWarning(" can't be mutated because all referencing test projects' simulated build failed."); } } } diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTree.cs b/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTree.cs index 69e2df583b..ee9f84a126 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTree.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTree.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Buildalyzer; using Microsoft.Extensions.Logging; @@ -12,12 +13,8 @@ namespace Stryker.Core.Initialisation; /// It allows us to keep track of the targets that are valid for mutation and to keep track of the project analyzer context. /// /// -internal class MutableProjectTree(ProjectAnalyzerContext project, ILogger logger) +internal class MutableProjectTree(ProjectSimulatedBuildHandler project, ILogger logger) { - private readonly ILogger _logger = logger; - - public ProjectAnalyzerContext Project { get; } = project; - public List Targets { get; } = []; public bool IsValidTarget => Targets.Any(t => t.IsValidTarget); @@ -31,7 +28,7 @@ public MutableProjectTarget this[IAnalyzerResult target] { return existingTarget; } - var newTarget = new MutableProjectTarget(target, _logger); + var newTarget = new MutableProjectTarget(target, logger); Targets.Add(newTarget); return newTarget; } @@ -57,24 +54,24 @@ public void KeepOnlyOneTarget(string optionsTargetFramework) Targets.Add(targetToKeep); return; } - _logger.LogWarning("Failed to find a valid {Framework} target for project {Project}. ", optionsTargetFramework, Project.ProjectFileName); + logger.LogWarning("Failed to find a valid {Framework} target for project {Project}. ", optionsTargetFramework, project.ProjectFileName); } targetToKeep = Targets.Where(t => t.IsValidTarget).FirstOrDefault( t => OperatingSystem.IsWindows() || !t.ProjectTarget.TargetsFullFramework()); Targets.Clear(); if (targetToKeep == null) { - _logger.LogWarning("Failed to find a valid target for project {Project}. ", Project.ProjectFileName); + logger.LogWarning("Failed to find a valid target for project {Project}. ", project.ProjectFileName); return; } - _logger.LogInformation("Picking {Framework} for project {Project}. ", targetToKeep.ProjectTarget.TargetFramework, Project.ProjectFileName); + logger.LogInformation("Picking {Framework} for project {Project}. ", targetToKeep.ProjectTarget.TargetFramework, project.ProjectFileName); Targets.Add(targetToKeep); } public void DumpForAnalysis() { - _logger.LogInformation("Project {ProjectPath} overall analysis {Result}.", - Project.ProjectFileName, + logger.LogInformation("Project {ProjectPath} overall analysis {Result}.", + Path.GetFileName(project.ProjectFileName), IsValidTarget ? "succeeded" : "failed hence can't be mutated"); foreach (var target in Targets) { diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectMutator.cs b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectMutator.cs index 5f589dd302..0f917477e0 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectMutator.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectMutator.cs @@ -33,7 +33,7 @@ public IMutationTestProcess MutateProject(IStrykerOptions options, MutationTestI process.Initialize(input, options, reporters); // Enrich test projects info with unit tests - EnrichTestProjectsWithTestInfo(input.InitialTestRun, input.TestProjectsInfo); + EnrichTestProjectsWithTestInfo(input.InitialTestRun, input.SourceProjectInfo.TestProjectsInfo); // mutate process.Mutate(); diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectOrchestrator.cs b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectOrchestrator.cs index cd294b8966..0fede5929a 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectOrchestrator.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectOrchestrator.cs @@ -95,7 +95,7 @@ private void InitializeDashboardProjectInformation(IStrykerOptions options, Sour var requiresProjectInformation = dashboardReporterEnabled || dashboardBaselineEnabled; var missingProjectName = string.IsNullOrEmpty(options.ProjectName); var missingProjectVersion = string.IsNullOrEmpty(options.ProjectVersion); - if (!requiresProjectInformation || !missingProjectVersion && !missingProjectName) + if (!requiresProjectInformation || (!missingProjectVersion && !missingProjectName)) { return; } diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectSimulatedBuildHandler.cs similarity index 80% rename from src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs rename to src/Stryker.Core/Stryker.Core/Initialisation/ProjectSimulatedBuildHandler.cs index 8d6e20541a..705043905d 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectAnalyzerContext.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectSimulatedBuildHandler.cs @@ -10,27 +10,30 @@ namespace Stryker.Core.Initialisation; -public class ProjectAnalyzerContext +/// +/// Encapsulates the context of a project analysis, including the project analyzer, the last analysis results +/// for said project. It also provides reference discovery methods +/// +public class ProjectSimulatedBuildHandler { private readonly IProjectAnalyzer _analyzer; - private readonly TargetsForMutation _targetsForMutation; + private readonly ProjectsTracker _projectsTracker; private readonly string _msBuildPath; private readonly string _configuration; private readonly string _platform; private readonly string? _framework; private readonly ILogger _logger; private readonly StringWriter _buildLogger; - public IAnalyzerResults? AnalyzerLastResults { get; private set; } private string[] _targetFrameworks=[]; - public ProjectAnalyzerContext(IBuildalyzerProvider buildalyzerProvider, + public ProjectSimulatedBuildHandler(IBuildalyzerProvider buildalyzerProvider, string projectFile, string msBuildPath, (string configuration, string platform, string? framework) target, ILogger logger, - TargetsForMutation targetsForMutation) + ProjectsTracker projectsTracker) { _buildLogger = new StringWriter(); var manager = buildalyzerProvider.Provide(new AnalyzerManagerOptions{LogWriter = _buildLogger}); @@ -38,7 +41,7 @@ public ProjectAnalyzerContext(IBuildalyzerProvider buildalyzerProvider, _analyzer = analyzer; ProjectFileName = projectFile; - _targetsForMutation = targetsForMutation; + _projectsTracker = projectsTracker; _msBuildPath = msBuildPath; _configuration = target.configuration; _platform = target.platform; @@ -46,15 +49,23 @@ public ProjectAnalyzerContext(IBuildalyzerProvider buildalyzerProvider, _logger = logger; } + public IAnalyzerResults AnalyzerLastResults { get; private set; } = new AnalyzerResults(); + public string LastBuildLog => _buildLogger.ToString(); public string ProjectFileName { get; } public IAnalyzerResults Analyze(bool withRestore = false, bool forceFramework = false) { - if (withRestore && AnalyzerLastResults?.Any(ar => ar.TargetsFullFramework()) == true) + if (forceFramework && string.IsNullOrEmpty(_framework)) { - _targetsForMutation.RestoreSolution(AnalyzerLastResults); + throw new InvalidOperationException("Cannot force framework when no framework is specified in options."); + } + + if (withRestore && AnalyzerLastResults.Any(ar => ar.TargetsFullFramework()) == true) + { + _projectsTracker.RestoreSolution(AnalyzerLastResults); + withRestore= false; } _buildLogger.GetStringBuilder().Clear(); var env = new EnvironmentOptions @@ -104,16 +115,18 @@ private void InitializeTargetFrameworks() _targetFrameworks = projectFileTargetFrameworks; } + public bool IsNetFramework => _analyzer.ProjectFile.RequiresNetFramework; + public IEnumerable FailedFrameworks => _targetFrameworks?.Where(tf => - AnalyzerLastResults?.Any( ar => ar.TargetFramework == tf && ar.IsValid())== false) ?? []; + AnalyzerLastResults.Any( ar => ar.TargetFramework == tf && ar.IsValid())== false) ?? []; - public bool IsTest => AnalyzerLastResults?.IsTestProject() == true; + public bool IsTest => AnalyzerLastResults.IsTestProject() == true; - public bool HasValidResults() => AnalyzerLastResults != null && AnalyzerLastResults.IsValidFor(_targetFrameworks); + public bool HasValidResults() => AnalyzerLastResults.IsValidFor(_targetFrameworks); - public bool IsTestProject() => AnalyzerLastResults != null && AnalyzerLastResults.IsTestProject(); + public bool IsTestProject() => AnalyzerLastResults.IsTestProject(); - public IEnumerable GetProjectReferences() => AnalyzerLastResults?.SelectMany(r => r.ProjectReferences).Distinct(); + public IEnumerable GetProjectReferences() => AnalyzerLastResults.SelectMany(r => r.ProjectReferences).Distinct(); private static readonly HashSet ImportantProperties = ["Configuration", "Platform", "AssemblyName", "Configurations", "TargetPath", "OS"]; @@ -162,10 +175,8 @@ private void DumpTestAnalyzerResult(StringBuilder log, IAnalyzerResult analyzerR if (_logger.IsEnabled(LogLevel.Trace)) { // dumps all other properties as well, as they can be useful for diagnosing build issues - foreach (var property in properties.Where( p => !ImportantProperties.Contains(p.Key) )) - { - log.AppendLine($"Property {property.Key}={property.Value.Replace(Environment.NewLine, "\\n")}"); - } + log.AppendLine($"Other properties: { + string.Join(", ", properties.Where(p => !ImportantProperties.Contains(p.Key)).Select(p => $"{p.Key}={p.Value.Replace(Environment.NewLine, "\\n")}"))}."); } log.AppendLine(); @@ -204,16 +215,10 @@ private static void DumpReferences(StringBuilder log, IAnalyzerResult analyzerRe } } - public bool BuildsAnAssembly() => AnalyzerLastResults?.Any(p => p.BuildsAnAssembly()) == true; + public bool BuildsAnAssembly() => AnalyzerLastResults.Any(p => p.BuildsAnAssembly()) == true; public bool FindMatchingVariant(string assemblyPath, out IAnalyzerResult? analyzerResult) { - if (AnalyzerLastResults == null) - { - analyzerResult = null; - return false; - } - analyzerResult= AnalyzerLastResults.FirstOrDefault( r=> string.Compare(assemblyPath, r.GetAssemblyPath(), StringComparison.OrdinalIgnoreCase) == 0 || string.Compare(assemblyPath, r.GetReferenceAssemblyPath(), StringComparison.OrdinalIgnoreCase) == 0); diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectsTracker.cs similarity index 69% rename from src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs rename to src/Stryker.Core/Stryker.Core/Initialisation/ProjectsTracker.cs index 6a52d3c1b1..8e85993d8f 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/TargetsForMutation.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectsTracker.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO.Abstractions; using System.Linq; using Buildalyzer; using Microsoft.Extensions.Logging; @@ -9,27 +10,33 @@ namespace Stryker.Core.Initialisation; -public class TargetsForMutation +/// +/// This class is used to keep track of the solution and the projects that are selected for mutation during the initialization process. +/// +public class ProjectsTracker { private List _selectedProjects = []; private bool _solutionRestored; + private bool _solutionBuilt; private readonly IStrykerOptions _options; private readonly INugetRestoreProcess _nugetRestoreProcess; + private readonly IFileSystem _fileSystem; private readonly IBuildalyzerProvider _buildalyzerProvider; private readonly ILogger _logger; - public TargetsForMutation(SolutionFile? file, IStrykerOptions options, IBuildalyzerProvider buildalyzerProvider ,ILogger logger, INugetRestoreProcess nugetRestoreProcess) + public ProjectsTracker(SolutionFile file, IStrykerOptions options, IBuildalyzerProvider buildalyzerProvider, + INugetRestoreProcess nugetRestoreProcess, IFileSystem fileSystem, ILogger logger) { _options = options; _buildalyzerProvider = buildalyzerProvider; _logger = logger; _nugetRestoreProcess = nugetRestoreProcess; + _fileSystem = fileSystem; Solution = file; SelectConfiguration(); } - private SolutionFile? Solution { get; } public string? SolutionFilePath => Solution?.FileName; @@ -84,14 +91,14 @@ internal void RestoreSolution(IAnalyzerResults results) { lock (_nugetRestoreProcess) { - if (_solutionRestored) + if (_solutionRestored || string.IsNullOrEmpty(SolutionFilePath)) { return; } if (Environment.OSVersion.Platform == PlatformID.Win32NT) { - _logger.LogWarning("Project analysis failed. Stryker will retry after a solution level nuget restore"); + _logger.LogWarning("Project analysis failed. Stryker will retry after a solution level nuget restore"); var optionsMsBuildPath = _options.MsBuildPath ?? results.First().MsBuildPath(); if (string.IsNullOrEmpty(optionsMsBuildPath)) { @@ -101,7 +108,30 @@ internal void RestoreSolution(IAnalyzerResults results) } _solutionRestored = true; + } + } + + internal void BuildSolution(IInitialBuildProcess buildProcess, IEnumerable results) + { + lock (_nugetRestoreProcess) + { + if (_solutionBuilt || string.IsNullOrEmpty(SolutionFilePath)) + { + return; + } + var framework = results.Any(p => p.TargetsFullFramework()); + // Build the complete solution + _logger.LogInformation("Building solution {SolutionPathName}.", + _fileSystem.Path.GetRelativePath(_options.WorkingDirectory, SolutionFilePath)); + + buildProcess.InitialBuild( + framework, + _fileSystem.Path.GetDirectoryName(SolutionFilePath), + SolutionFilePath, Configuration, Platform, + TargetFramework, _options.MsBuildPath ?? results.First().MsBuildPath() + ); + _solutionBuilt = true; } } @@ -109,8 +139,8 @@ internal void RestoreSolution(IAnalyzerResults results) /// Gets a project analysis context for the given project file, using the configuration and platform from the solution if available, otherwise using the configuration and platform from the options. /// /// target project file - /// a instance for . - public ProjectAnalyzerContext GetProjectAnalysisContext(string projectFile) + /// a instance for . + public ProjectSimulatedBuildHandler GetProjectAnalysisContext(string projectFile) { string configuration; string platform; @@ -123,7 +153,7 @@ public ProjectAnalyzerContext GetProjectAnalysisContext(string projectFile) { (configuration, platform) = (Configuration, Platform); } - return new ProjectAnalyzerContext(_buildalyzerProvider, projectFile, _options.MsBuildPath, (configuration, + return new ProjectSimulatedBuildHandler(_buildalyzerProvider, projectFile, _options.MsBuildPath, (configuration, platform, _options.TargetFramework), _logger, this); } } diff --git a/src/Stryker.Core/Stryker.Core/MutationTest/MutationTestInput.cs b/src/Stryker.Core/Stryker.Core/MutationTest/MutationTestInput.cs index b6edc2cf66..96e71bbf05 100644 --- a/src/Stryker.Core/Stryker.Core/MutationTest/MutationTestInput.cs +++ b/src/Stryker.Core/Stryker.Core/MutationTest/MutationTestInput.cs @@ -2,7 +2,6 @@ using Stryker.Abstractions.Testing; using Stryker.Core.Initialisation; using Stryker.Core.ProjectComponents.SourceProjects; -using Stryker.Core.ProjectComponents.TestProjects; namespace Stryker.Core.MutationTest; @@ -14,17 +13,12 @@ public class MutationTestInput /// /// Contains all information about the project to mutate /// - public SourceProjectInfo SourceProjectInfo { get; set; } - - /// - /// Contains all information about the tests to run - /// - public ITestProjectsInfo TestProjectsInfo { get; set; } + public SourceProjectInfo SourceProjectInfo { get; init; } /// /// The testrunner that will be used for the mutation test run /// - public ITestRunner TestRunner { get; set; } + public ITestRunner TestRunner { get; init; } /// /// Get/Set the initial test diff --git a/src/Stryker.Core/Stryker.Core/MutationTest/MutationTestProcess.cs b/src/Stryker.Core/Stryker.Core/MutationTest/MutationTestProcess.cs index 4363b8f086..c03153d3fb 100644 --- a/src/Stryker.Core/Stryker.Core/MutationTest/MutationTestProcess.cs +++ b/src/Stryker.Core/Stryker.Core/MutationTest/MutationTestProcess.cs @@ -57,7 +57,7 @@ public void Initialize(MutationTestInput input, IStrykerOptions options, IReport _options = options; _reporter = reporter; _projectContents = input.SourceProjectInfo.ProjectContents; - Input.TestProjectsInfo.BackupOriginalAssembly(Input.SourceProjectInfo.AnalyzerResult); + Input.SourceProjectInfo.BackupOriginalAssembly(Input.SourceProjectInfo.AnalyzerResult); } public void Mutate() @@ -79,7 +79,7 @@ public async Task TestAsync(IEnumerable mutantsToTest return new StrykerRunResult(_options, _projectContents.GetMutationScore()); } - public void Restore() => Input.TestProjectsInfo.RestoreOriginalAssembly(Input.SourceProjectInfo.AnalyzerResult); + public void Restore() => Input.SourceProjectInfo.RestoreOriginalAssembly(Input.SourceProjectInfo.AnalyzerResult); private async Task TestMutantsAsync(IEnumerable mutantsToTest) { diff --git a/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/SourceProjectInfo.cs b/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/SourceProjectInfo.cs index 46e4af0488..edfc6d7659 100644 --- a/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/SourceProjectInfo.cs +++ b/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/SourceProjectInfo.cs @@ -15,7 +15,7 @@ public class SourceProjectInfo : IProjectAndTests public Action OnProjectBuilt { get; set; } - public TargetsForMutation TargetsForMutation { get; set; } + public ProjectsTracker ProjectsTracker { get; set; } public IAnalyzerResult AnalyzerResult { get; init; } @@ -42,4 +42,8 @@ public string LogError(string error) _warnings.Add(error); return error; } + + public void BackupOriginalAssembly(IAnalyzerResult analyzerResult) => TestProjectsInfo.BackupOriginalAssembly(analyzerResult); + + public void RestoreOriginalAssembly(IAnalyzerResult analyzerResult) => TestProjectsInfo.RestoreOriginalAssembly(analyzerResult); } diff --git a/src/Stryker.Core/Stryker.Core/StrykerRunner.cs b/src/Stryker.Core/Stryker.Core/StrykerRunner.cs index 108b1c567b..73a3a6e4ae 100644 --- a/src/Stryker.Core/Stryker.Core/StrykerRunner.cs +++ b/src/Stryker.Core/Stryker.Core/StrykerRunner.cs @@ -61,7 +61,7 @@ public async Task RunMutationTestAsync(IStrykerInputs inputs) _mutationTestProcesses = (await _projectOrchestrator.MutateProjectsAsync(options, reporters)).ToList(); var rootComponent = AddRootFolderIfMultiProject(_mutationTestProcesses.Select(x => x.Input.SourceProjectInfo.ProjectContents).ToList(), options); - var combinedTestProjectsInfo = _mutationTestProcesses.Select(mtp => mtp.Input.TestProjectsInfo).Aggregate((a, b) => (TestProjectsInfo)a + (TestProjectsInfo)b); + var combinedTestProjectsInfo = _mutationTestProcesses.Select(mtp => mtp.Input.SourceProjectInfo.TestProjectsInfo).Aggregate((a, b) => (TestProjectsInfo)a + (TestProjectsInfo)b); _logger.LogInformation("{MutantsCount} mutants created", rootComponent.Mutants.Count()); diff --git a/src/Stryker.TestRunner.VsTest.UnitTest/VsTestMockingHelper.cs b/src/Stryker.TestRunner.VsTest.UnitTest/VsTestMockingHelper.cs index fb5db01178..4c6097e83a 100644 --- a/src/Stryker.TestRunner.VsTest.UnitTest/VsTestMockingHelper.cs +++ b/src/Stryker.TestRunner.VsTest.UnitTest/VsTestMockingHelper.cs @@ -547,7 +547,6 @@ protected MutationTestProcess BuildMutationTestProcess(VsTestRunnerPool runner, SourceProjectInfo = sourceProject ?? SourceProjectInfo, TestRunner = runner, InitialTestRun = new InitialTestRun(testRunResult, new TimeoutValueCalculator(500)), - TestProjectsInfo = _testProjectsInfo }; var mutator = new CsharpMutationProcess(_fileSystem, TestLoggerFactory.CreateLogger()); var executor = new MutationTestExecutor(TestLoggerFactory.CreateLogger()); @@ -568,7 +567,5 @@ private class MockStrykerTestHostLauncher : IStrykerTestHostLauncher public int LaunchTestHost(VsTestObjModel.TestProcessStartInfo defaultTestHostStartInfo, CancellationToken cancellationToken) => throw new NotImplementedException(); public bool IsDebug { get; } - - public int ErrorCode { get; } } } diff --git a/src/Stryker.Utilities/Buildalyzer/IAnalyzerResultExtensions.cs b/src/Stryker.Utilities/Buildalyzer/IAnalyzerResultExtensions.cs index c88c5b27bf..a31d6332d3 100644 --- a/src/Stryker.Utilities/Buildalyzer/IAnalyzerResultExtensions.cs +++ b/src/Stryker.Utilities/Buildalyzer/IAnalyzerResultExtensions.cs @@ -122,11 +122,12 @@ public static IEnumerable LoadReferences(this IAnalyzerResult public static NuGetFramework? GetNuGetFramework(this IAnalyzerResult analyzerResult) { - if (string.IsNullOrEmpty(analyzerResult.TargetFramework)) + var frameworkText = analyzerResult.TargetFramework; + if (string.IsNullOrEmpty(frameworkText)) { return null; } - var framework = NuGetFramework.Parse(analyzerResult.TargetFramework); + var framework = NuGetFramework.Parse(frameworkText); if (framework != NuGetFramework.UnsupportedFramework) { return framework; @@ -136,7 +137,7 @@ public static IEnumerable LoadReferences(this IAnalyzerResult ? "" : $" at '{analyzerResult.ProjectFilePath}'"; var message = - $"The target framework '{analyzerResult.TargetFramework}' is not supported. Please fix the target framework in the csproj{atPath}."; + $"The target framework '{frameworkText}' is not supported. Please fix the target framework in the csproj{atPath}."; throw new InputException(message); } diff --git a/src/Stryker.slnx b/src/Stryker.slnx index f074e2f1c6..6446cdbb3c 100644 --- a/src/Stryker.slnx +++ b/src/Stryker.slnx @@ -5,6 +5,19 @@ + + + + + + + + + + + + + From f1a9ff6c70dac35ef843a6d190c391361fadc15d Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Tue, 7 Apr 2026 15:53:01 +0200 Subject: [PATCH 26/41] fix: failing unit tests --- .../InitialisationProcessTests.cs | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InitialisationProcessTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InitialisationProcessTests.cs index 7e61fd2094..f76b1c78d3 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InitialisationProcessTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InitialisationProcessTests.cs @@ -40,12 +40,13 @@ public void InitialisationProcess_ShouldCallNeededResolvers() new CsharpFileLeaf() }); inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())) - .Returns(new[] {new SourceProjectInfo + .Returns([ + new SourceProjectInfo { - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult(references: Array.Empty()).Object, + AnalyzerResult = TestHelper.SetupProjectAnalyzerResult(references: []).Object, ProjectContents = folder } - }); + ]); inputFileResolverMock.SetupGet(x => x.FileSystem).Returns(new FileSystem()); var loggerMock = new Mock>(); @@ -271,13 +272,21 @@ public async Task InitialisationProcess_ShouldThrowOnWhenNoTestDetected(string l inputFileResolverMock.SetupGet(x => x.FileSystem).Returns(new FileSystem()); + var loggerMock = new Mock>(); var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); + var projectTracker = new ProjectsTracker(null, new StrykerOptions(), null, null, null, loggerMock.Object); inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())).Returns( - new[] {new SourceProjectInfo + [ + new SourceProjectInfo { AnalyzerResult = TestHelper.SetupProjectAnalyzerResult( references: []).Object, - TestProjectsInfo = new TestProjectsInfo(new MockFileSystem()){TestProjects = new List {new(new MockFileSystem(), testProjectAnalyzerResult)}} - }}); + ProjectsTracker = projectTracker, + 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(), It.IsAny(), null, It.IsAny())); @@ -286,7 +295,6 @@ public async Task InitialisationProcess_ShouldThrowOnWhenNoTestDetected(string l initialTestProcessMock.Setup(x => x.InitialTestAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.FromResult(new InitialTestRun(new TestRunResult(Array.Empty(), TestIdentifierList.NoTest(), TestIdentifierList.NoTest(), TestIdentifierList.NoTest(), string.Empty, Enumerable.Empty(), TimeSpan.Zero), null))); // failing test - var loggerMock = new Mock>(); var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); var options = new StrykerOptions { ProjectName = "TheProjectName", @@ -322,13 +330,19 @@ public void InitialisationProcess_ShouldThrowOnWhenNoTestDetectedAndCorrectDepen inputFileResolverMock.SetupGet(x => x.FileSystem).Returns(new FileSystem()); + var loggerMock = new Mock>(); var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); + var projectTracker = new ProjectsTracker(null, new StrykerOptions(), null, null, null, loggerMock.Object); + inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())).Returns( - new[] {new SourceProjectInfo + [ + new SourceProjectInfo { + ProjectsTracker = projectTracker, AnalyzerResult = TestHelper.SetupProjectAnalyzerResult( references: []).Object, 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(), It.IsAny(), null, It.IsAny())); @@ -337,7 +351,6 @@ public void InitialisationProcess_ShouldThrowOnWhenNoTestDetectedAndCorrectDepen initialTestProcessMock.Setup(x => x.InitialTestAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.FromResult(new InitialTestRun(new TestRunResult(Array.Empty(), TestIdentifierList.NoTest(), TestIdentifierList.NoTest(), TestIdentifierList.NoTest(), string.Empty, Enumerable.Empty(), TimeSpan.Zero), null))); // failing test - var loggerMock = new Mock>(); var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); var options = new StrykerOptions { ProjectName = "TheProjectName", From 16a2376520a2e33b96983252df451b0be1783b13 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Tue, 7 Apr 2026 18:11:57 +0200 Subject: [PATCH 27/41] fix: failing nuget test --- .../Initialisation/BuildAnalyzerTestsBase.cs | 2 +- .../Initialisation/InputFileResolverTests.cs | 4 ++-- .../Initialisation/ProjectOrchestratorTests.cs | 2 -- src/Stryker.Solutions.Test/SolutionFileShould.cs | 14 +++++++------- src/Stryker.Solutions/SolutionFile.cs | 5 +++-- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/BuildAnalyzerTestsBase.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/BuildAnalyzerTestsBase.cs index 6ad1847c62..9836f3accc 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/BuildAnalyzerTestsBase.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/BuildAnalyzerTestsBase.cs @@ -333,6 +333,6 @@ public SolutionFile GetSolution(string solutionPath) { throw new InvalidOperationException($"Solution file {solutionPath} does not exist in the file system."); } - return SolutionFile.BuildFromProjectList(_projectCache.Keys.ToList()); + return SolutionFile.BuildFromProjectList(solutionPath, _projectCache.Keys.ToList()); } } diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InputFileResolverTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InputFileResolverTests.cs index f3b7929813..ff0f47416b 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InputFileResolverTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InputFileResolverTests.cs @@ -1679,7 +1679,7 @@ public void ShouldPassPlatformFromSolutionToBuildalyzer() var managerMock = BuildBuildAnalyzerMock(analyzerResults); // Build a solution that assigns x64 platform to projects - var solution = SolutionFile.BuildFromProjectList( + var solution = SolutionFile.BuildFromProjectList(solutionPath, [_sourceProjectFilePath, _testProjectFilePath], ["x64"]); var target = BuildTestResolverWithSolutionProvider(fileSystem, @@ -1755,7 +1755,7 @@ public void ShouldUseNormalizedSolutionConfigurationWhenGetMatchingFallsBack() var analyzerManagerMock = BuildBuildAnalyzerMock(analyzerResults); // Build a solution with only Release|x86 available - var solution = SolutionFile.BuildFromProjectList( + var solution = SolutionFile.BuildFromProjectList(solutionPath, [_sourceProjectFilePath, _testProjectFilePath], ["x86"]); var target = BuildTestResolverWithSolutionProvider(fileSystem, diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/ProjectOrchestratorTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/ProjectOrchestratorTests.cs index ae829e178e..afc92aff73 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/ProjectOrchestratorTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/ProjectOrchestratorTests.cs @@ -1,13 +1,11 @@ using System; using System.Collections.Generic; -using System.IO; using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; using Buildalyzer; using Buildalyzer.Environment; using Microsoft.CodeAnalysis; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; diff --git a/src/Stryker.Solutions.Test/SolutionFileShould.cs b/src/Stryker.Solutions.Test/SolutionFileShould.cs index e355539f7a..e8b8548b66 100644 --- a/src/Stryker.Solutions.Test/SolutionFileShould.cs +++ b/src/Stryker.Solutions.Test/SolutionFileShould.cs @@ -45,7 +45,7 @@ public void DetectPlatformIfNotSpecified(string platform) // Arrange List projects = ["Project.csproj", "Test.csproj"]; // Act - var solution = SolutionFile.BuildFromProjectList( projects, [platform], [platform]); + var solution = SolutionFile.BuildFromProjectList("Solution.sln", projects, [platform], [platform]); // Assert solution.GetProjectsWithDetails("Debug").ShouldBe(projects.Select(p => (p, "Debug", platform))); @@ -57,7 +57,7 @@ public void DefaultPlatformToAnyCpuIfNotSpecified() // Arrange List projects = ["Project.csproj", "Test.csproj"]; // Act - var solution = SolutionFile.BuildFromProjectList( projects, ["Z80", "Any CPU"]); + var solution = SolutionFile.BuildFromProjectList("Solution.sln", projects, ["Z80", "Any CPU"]); // Assert solution.GetProjectsWithDetails("Debug").ShouldBe(projects.Select(p => (p, "Debug", "Any CPU"))); @@ -69,7 +69,7 @@ public void DefaultPlatformToFirstIfAnyCpuNotProvided() // Arrange List projects = ["Project.csproj", "Test.csproj"]; // Act - var solution = SolutionFile.BuildFromProjectList( projects, ["Z80", "6502"]); + var solution = SolutionFile.BuildFromProjectList("Solution.sln", projects, ["Z80", "6502"]); // Assert solution.GetProjectsWithDetails("Debug").ShouldBe(projects.Select(p => (p, "Debug", "Z80"))); @@ -140,7 +140,7 @@ public void ProvideProjectListForGivenConfigurationOnSolutionWithMultiplePlatfor public void PickExactMatch() { // Arrange - var solution = SolutionFile.BuildFromProjectList(["Project.csproj", "Test.csproj"], ["x86", "x64"]); + var solution = SolutionFile.BuildFromProjectList("Solution.sln", ["Project.csproj", "Test.csproj"], ["x86", "x64"]); var match = solution.GetMatching("Debug", "x64"); // Assert @@ -151,7 +151,7 @@ public void PickExactMatch() public void FallBackOnDebug() { // Arrange - var solution = SolutionFile.BuildFromProjectList(["Project.csproj", "Test.csproj"], ["x86", "x64"]); + var solution = SolutionFile.BuildFromProjectList("Solution.sln", ["Project.csproj", "Test.csproj"], ["x86", "x64"]); var match = solution.GetMatching("Stryker", "x64"); // Assert @@ -162,7 +162,7 @@ public void FallBackOnDebug() public void FallBackOnAnyCPU() { // Arrange - var solution = SolutionFile.BuildFromProjectList(["Project.csproj", "Test.csproj"], ["AnyCPU"]); + var solution = SolutionFile.BuildFromProjectList("Solution.sln", ["Project.csproj", "Test.csproj"], ["AnyCPU"]); var match = solution.GetMatching("Debug", "x64"); // Assert @@ -173,7 +173,7 @@ public void FallBackOnAnyCPU() public void PickFirstIfNoMatch() { // Arrange - var solution = SolutionFile.BuildFromProjectList(["Project.csproj", "Test.csproj"], ["AnyCPU"]); + var solution = SolutionFile.BuildFromProjectList("Solution.sln", ["Project.csproj", "Test.csproj"], ["AnyCPU"]); var match = solution.GetMatching("Stryker", "x64"); // Assert diff --git a/src/Stryker.Solutions/SolutionFile.cs b/src/Stryker.Solutions/SolutionFile.cs index 54be267c6c..3d6c6eb5e1 100644 --- a/src/Stryker.Solutions/SolutionFile.cs +++ b/src/Stryker.Solutions/SolutionFile.cs @@ -187,15 +187,16 @@ private string GetEffectiveBuildType(string? buildType) /// /// Create a solution file from a list of projects having two build types, Debug and Release, and the provided platforms. /// + /// solution file name /// list of csproj filenames /// list of declared platforms. Default to AnyCpu and x86 /// list of solution level platforms /// a solution instance /// This method is used for testing purposes. It is mandatory as the underlying solution parser does not support any form of mocking - public static SolutionFile BuildFromProjectList(List projects, string[]? platforms = null + public static SolutionFile BuildFromProjectList(string filePath, List projects, string[]? platforms = null , string[]? solutionPlatforms = null) { - var result = new SolutionFile(); + var result = new SolutionFile {FileName = filePath}; solutionPlatforms = DefineSolutionPlatforms(platforms, solutionPlatforms); platforms ??= [ "AnyCPU", "x86" ]; if (platforms.Length != solutionPlatforms.Length) From 4a5d47e98d92252d123cefc1c42a7db15f1bbdb2 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Tue, 7 Apr 2026 19:17:56 +0200 Subject: [PATCH 28/41] fix: misc issues (Sonar/Copilot) --- .../Initialisation/CsharpProjectComponentsBuilder.cs | 2 +- .../Initialisation/InitialisationProcess.cs | 3 --- .../Stryker.Core/Initialisation/InputFileResolver.cs | 5 ++--- .../Initialisation/ProjectSimulatedBuildHandler.cs | 10 +++++----- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/CsharpProjectComponentsBuilder.cs b/src/Stryker.Core/Stryker.Core/Initialisation/CsharpProjectComponentsBuilder.cs index e12022b2d3..73be23fa1d 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/CsharpProjectComponentsBuilder.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/CsharpProjectComponentsBuilder.cs @@ -43,7 +43,7 @@ public override IReadOnlyProjectComponent Build() } else { - _logger.LogWarning("Buildalyzer could not find source files. This should not happen. We fallback to filesystem scan. Please report an issue at github."); + _logger.LogWarning("Buildalyzer could not find source files. This should not happen. We fallback to filesystem scan. Please report an issue at GitHub."); inputFiles = FindProjectFilesScanningProjectFolders(_projectInfo.AnalyzerResult); } return inputFiles; diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InitialisationProcess.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InitialisationProcess.cs index b96176abac..438583d30b 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InitialisationProcess.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InitialisationProcess.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.IO.Abstractions; using System.Linq; using System.Threading.Tasks; @@ -101,8 +100,6 @@ public void BuildProjects(IStrykerOptions options, IEnumerable _inputFileResolver.FileSystem; - public async Task> GetMutationTestInputsAsync(IStrykerOptions options, IReadOnlyCollection projects, ITestRunner runner) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index a9295cfa04..c1d5bfe957 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -412,7 +412,6 @@ private IAnalyzerResults AnalyzeSingleProject(ProjectSimulatedBuildHandler proje IEnumerable mutableProjectsAnalyzerResults) { // separate test projects from mutable projects, and keep only analyzer results building an assembly (exclude solution folders and such) - var scan = mutableProjectsAnalyzerResults.Where(p=> p.BuildsAnAssembly()).GroupBy(p => p.IsTest); var testProjects = new List(); var mutableProjects = new List(); foreach (var project in mutableProjectsAnalyzerResults) @@ -585,9 +584,9 @@ public DynamicEnumerableQueue(IEnumerable init) public bool Empty => _queue.IsEmpty; - public void Add(IEnumerable where) + public void Add(IEnumerable entries) { - foreach (var entry in where) + foreach (var entry in entries) { if (!_cache.TryAdd(entry, true)) { diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectSimulatedBuildHandler.cs b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectSimulatedBuildHandler.cs index 705043905d..f0cc402d12 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectSimulatedBuildHandler.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectSimulatedBuildHandler.cs @@ -62,7 +62,7 @@ public IAnalyzerResults Analyze(bool withRestore = false, bool forceFramework = throw new InvalidOperationException("Cannot force framework when no framework is specified in options."); } - if (withRestore && AnalyzerLastResults.Any(ar => ar.TargetsFullFramework()) == true) + if (withRestore && AnalyzerLastResults.Any(ar => ar.TargetsFullFramework())) { _projectsTracker.RestoreSolution(AnalyzerLastResults); withRestore= false; @@ -118,14 +118,16 @@ private void InitializeTargetFrameworks() public bool IsNetFramework => _analyzer.ProjectFile.RequiresNetFramework; public IEnumerable FailedFrameworks => _targetFrameworks?.Where(tf => - AnalyzerLastResults.Any( ar => ar.TargetFramework == tf && ar.IsValid())== false) ?? []; + !AnalyzerLastResults.Any( ar => ar.TargetFramework == tf && ar.IsValid())) ?? []; - public bool IsTest => AnalyzerLastResults.IsTestProject() == true; + public bool IsTest => AnalyzerLastResults.IsTestProject(); public bool HasValidResults() => AnalyzerLastResults.IsValidFor(_targetFrameworks); public bool IsTestProject() => AnalyzerLastResults.IsTestProject(); + public bool BuildsAnAssembly() => AnalyzerLastResults.Any(p => p.BuildsAnAssembly()); + public IEnumerable GetProjectReferences() => AnalyzerLastResults.SelectMany(r => r.ProjectReferences).Distinct(); private static readonly HashSet ImportantProperties = @@ -215,8 +217,6 @@ private static void DumpReferences(StringBuilder log, IAnalyzerResult analyzerRe } } - public bool BuildsAnAssembly() => AnalyzerLastResults.Any(p => p.BuildsAnAssembly()) == true; - public bool FindMatchingVariant(string assemblyPath, out IAnalyzerResult? analyzerResult) { analyzerResult= AnalyzerLastResults.FirstOrDefault( r=> From d6d4b680ada19a5d69e3e5463dfa7c9c6d41dbe6 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Sat, 11 Apr 2026 00:30:01 +0200 Subject: [PATCH 29/41] chore: signature changes --- .../Compiling/CSharpCompilingProcessTests.cs | 305 +++++++----------- .../Compiling/CSharpRollbackProcessTests.cs | 67 ++-- .../InitialisationProcessTests.cs | 192 +++++------ .../Initialisation/InputFileResolverTests.cs | 56 ++-- .../Initialisation/ProjectMutatorTests.cs | 23 +- .../CSharpMutationTestProcessTests.cs | 15 +- .../MutationTest/MutationTestProcessTests.cs | 12 +- .../CollectionExpressionMutatorTests.cs | 38 +-- .../SourceProjects/SourceProjectInfoTests.cs | 42 ++- .../StrykerRunnerTests.cs | 19 +- .../Compiling/CsharpCompilingProcess.cs | 27 +- .../Initialisation/InitialisationProcess.cs | 52 ++- .../Initialisation/InputFileResolver.cs | 15 +- .../Initialisation/MutableProjectTarget.cs | 13 +- .../Initialisation/ProjectOrchestrator.cs | 47 ++- .../Initialisation/ProjectsTracker.cs | 14 +- .../MutationTest/CsharpMutationProcess.cs | 2 +- .../MutationTest/MutationTestInput.cs | 1 - .../RelatedSourceProjectsInfo.cs | 16 + .../SourceProjects/SourceProjectInfo.cs | 25 +- .../VsTestMockingHelper.cs | 8 +- .../Buildalyzer/Buildalyzer.cs | 4 +- 22 files changed, 446 insertions(+), 547 deletions(-) create mode 100644 src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/RelatedSourceProjectsInfo.cs diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Compiling/CSharpCompilingProcessTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Compiling/CSharpCompilingProcessTests.cs index b91fb038d6..f23093ba6f 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Compiling/CSharpCompilingProcessTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Compiling/CSharpCompilingProcessTests.cs @@ -42,26 +42,20 @@ public int Subtract(int first, int second) } } }"); - var input = new MutationTestInput() - { - SourceProjectInfo = new SourceProjectInfo - { - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult( - projectFilePath: "/c/project.csproj", - properties: new Dictionary() - { - { "TargetDir", "" }, - { "AssemblyName", "AssemblyName"}, - { "TargetFileName", "TargetFileName.dll"}, - }, - // add a reference to system so the example code can compile - references: new[] { typeof(object).Assembly.Location } - ).Object - } - }; + var rollbackProcessMock = new Mock(MockBehavior.Strict); - var target = new CsharpCompilingProcess(input, rollbackProcessMock.Object); + var target = new CsharpCompilingProcess(TestHelper.SetupProjectAnalyzerResult( + projectFilePath: "/c/project.csproj", + properties: new Dictionary() + { + { "TargetDir", "" }, + { "AssemblyName", "AssemblyName"}, + { "TargetFileName", "TargetFileName.dll"}, + }, + // add a reference to system so the example code can compile + references: [typeof(object).Assembly.Location] + ).Object, rollbackProcessMock.Object); using var ms = new MemoryStream(); using var symbol = new MemoryStream(); @@ -91,35 +85,26 @@ public int Subtract(int first, int second) var immutableArray = ImmutableArray.Create("TheAlias"); alias[typeof(object).Assembly.Location]=immutableArray; - var input = new MutationTestInput() - { - SourceProjectInfo = new SourceProjectInfo + var analyzerResult = TestHelper.SetupProjectAnalyzerResult( + projectFilePath: "/c/project.csproj", + properties: new Dictionary() { - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult( - projectFilePath: "/c/project.csproj", - properties: new Dictionary() - { - { "TargetDir", "" }, - { "AssemblyName", "AssemblyName" }, - { "TargetFileName", "TargetFileName.dll" }, - }, - // add a reference to system so the example code can compile - references: [typeof(object).Assembly.Location], - aliases: alias.ToImmutableDictionary() - ).Object - } - }; + { "TargetDir", "" }, + { "AssemblyName", "AssemblyName" }, + { "TargetFileName", "TargetFileName.dll" }, + }, + // add a reference to system so the example code can compile + references: [typeof(object).Assembly.Location], + aliases: alias.ToImmutableDictionary() + ).Object; var rollbackProcessMock = new Mock(MockBehavior.Strict); + var target = new CsharpCompilingProcess(analyzerResult, rollbackProcessMock.Object); - var target = new CsharpCompilingProcess(input, rollbackProcessMock.Object); - - using (var ms = new MemoryStream()) - { - var result = target.Compile(new Collection() { syntaxTree }, ms, null); - result.Success.ShouldBe(true); - ms.Length.ShouldBeGreaterThan(100, "No value was written to the MemoryStream by the compiler"); - } + using var ms = new MemoryStream(); + var result = target.Compile(new Collection() { syntaxTree }, ms, null); + result.Success.ShouldBe(true); + ms.Length.ShouldBeGreaterThan(100, "No value was written to the MemoryStream by the compiler"); } [TestMethod] @@ -137,29 +122,24 @@ public int Subtract(string first, string second) } } }"); - var input = new MutationTestInput() - { - SourceProjectInfo = new SourceProjectInfo + var analyzerResult = TestHelper.SetupProjectAnalyzerResult( + projectFilePath: "/c/project.csproj", + properties: new Dictionary() { - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult( - projectFilePath: "/c/project.csproj", - properties: new Dictionary() - { - { "TargetDir", "" }, - { "AssemblyName", "AssemblyName"}, - { "TargetFileName", "TargetFileName.dll"}, - }, - // add a reference to system so the example code can compile - references: new string[] { typeof(object).Assembly.Location } - ).Object - } - }; + { "TargetDir", "" }, + { "AssemblyName", "AssemblyName"}, + { "TargetFileName", "TargetFileName.dll"}, + }, + // add a reference to system so the example code can compile + references: [typeof(object).Assembly.Location] + ).Object; + var rollbackProcessMock = new Mock(MockBehavior.Strict); rollbackProcessMock.Setup(x => x.Start(It.IsAny(), It.IsAny>(), It.IsAny(), false)) .Returns((CSharpCompilation compilation, ImmutableArray diagnostics, bool _, bool _) => new(compilation, null)); - var target = new CsharpCompilingProcess(input, rollbackProcessMock.Object, new StrykerOptions()); + var target = new CsharpCompilingProcess(analyzerResult, rollbackProcessMock.Object, new StrykerOptions()); using (var ms = new MemoryStream()) { @@ -184,26 +164,21 @@ public int Subtract(int first, int second) } } }"); - var input = new MutationTestInput() - { - SourceProjectInfo = new SourceProjectInfo + var analyzerResult = TestHelper.SetupProjectAnalyzerResult( + projectFilePath: "/c/project.csproj", + properties: new Dictionary() { - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult( - projectFilePath: "/c/project.csproj", - properties: new Dictionary() - { - { "TargetDir", "" }, - { "AssemblyName", "AssemblyName"}, - { "TargetFileName", "TargetFileName.dll"}, - }, - // add a reference to system so the example code can compile - references: new string[] { typeof(object).Assembly.Location } - ).Object - } - }; + { "TargetDir", "" }, + { "AssemblyName", "AssemblyName"}, + { "TargetFileName", "TargetFileName.dll"}, + }, + // add a reference to system so the example code can compile + references: [typeof(object).Assembly.Location] + ).Object; + var rollbackProcessMock = new Mock(MockBehavior.Strict); - var target = new CsharpCompilingProcess(input, rollbackProcessMock.Object); + var target = new CsharpCompilingProcess(analyzerResult, rollbackProcessMock.Object); using (var ms = new MemoryStream()) { @@ -228,38 +203,31 @@ public int Subtract(int first, int second) } } }"); - var input = new MutationTestInput() - { - SourceProjectInfo = new SourceProjectInfo + var analyzerResult = TestHelper.SetupProjectAnalyzerResult( + properties: new Dictionary() { - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult( - properties: new Dictionary() - { - { "TargetDir", "" }, - { "AssemblyName", "AssemblyName" }, - { "TargetFileName", "TargetFileName.dll" }, - { "SignAssembly", "true" }, - { "AssemblyOriginatorKeyFile", Path.GetFullPath(Path.Combine("TestResources", "StrongNameKeyFile.snk")) } - }, - // add a reference to system so the example code can compile - references: new string[] { typeof(object).Assembly.Location }, - projectFilePath: "TestResources" - ).Object - } - }; + { "TargetDir", "" }, + { "AssemblyName", "AssemblyName" }, + { "TargetFileName", "TargetFileName.dll" }, + { "SignAssembly", "true" }, + { "AssemblyOriginatorKeyFile", Path.GetFullPath(Path.Combine("TestResources", "StrongNameKeyFile.snk")) } + }, + // add a reference to system so the example code can compile + references: [typeof(object).Assembly.Location], + projectFilePath: "TestResources" + ).Object; + var rollbackProcessMock = new Mock(MockBehavior.Strict); - var target = new CsharpCompilingProcess(input, rollbackProcessMock.Object); + var target = new CsharpCompilingProcess(analyzerResult, rollbackProcessMock.Object); - using (var ms = new MemoryStream()) - { - var result = target.Compile(new Collection() { syntaxTree }, ms, null); - result.Success.ShouldBe(true); + using var ms = new MemoryStream(); + var result = target.Compile(new Collection() { syntaxTree }, ms, null); + result.Success.ShouldBe(true); - var key = Assembly.Load(ms.ToArray()).GetName().GetPublicKey(); - key.Length.ShouldBe(160, "Assembly was not signed"); - ms.Length.ShouldBeGreaterThan(100, "No value was written to the MemoryStream by the compiler"); - } + var key = Assembly.Load(ms.ToArray()).GetName().GetPublicKey(); + key.Length.ShouldBe(160, "Assembly was not signed"); + ms.Length.ShouldBeGreaterThan(100, "No value was written to the MemoryStream by the compiler"); } [TestMethod] @@ -277,38 +245,30 @@ public int Subtract(int first, int second) } } }"); - var input = new MutationTestInput() - { - SourceProjectInfo = new SourceProjectInfo + var analyzerResult = TestHelper.SetupProjectAnalyzerResult( + properties: new Dictionary() { - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult( - properties: new Dictionary() - { - { "TargetDir", "" }, - { "AssemblyName", "AssemblyName"}, - { "TargetFileName", "TargetFileName.dll"}, - { "SignAssembly", "true" } - }, - // add a reference to system so the example code can compile - references: new string[] { typeof(object).Assembly.Location }, - projectFilePath: "TestResources" - ).Object - } + { "TargetDir", "" }, + { "AssemblyName", "AssemblyName"}, + { "TargetFileName", "TargetFileName.dll"}, + { "SignAssembly", "true" } + }, + // add a reference to system so the example code can compile + references: [typeof(object).Assembly.Location], + projectFilePath: "TestResources" + ).Object; - }; var rollbackProcessMock = new Mock(MockBehavior.Strict); - var target = new CsharpCompilingProcess(input, rollbackProcessMock.Object); + var target = new CsharpCompilingProcess(analyzerResult, rollbackProcessMock.Object); - using (var ms = new MemoryStream()) - { - var result = target.Compile(new Collection() { syntaxTree }, ms, null); - result.Success.ShouldBe(true); + using var ms = new MemoryStream(); + var result = target.Compile(new Collection() { syntaxTree }, ms, null); + result.Success.ShouldBe(true); - var key = Assembly.Load(ms.ToArray()).GetName().GetPublicKey(); - key.Length.ShouldBe(0, "Assembly was signed"); - ms.Length.ShouldBeGreaterThan(100, "No value was written to the MemoryStream by the compiler"); - } + var key = Assembly.Load(ms.ToArray()).GetName().GetPublicKey(); + key.Length.ShouldBe(0, "Assembly was signed"); + ms.Length.ShouldBeGreaterThan(100, "No value was written to the MemoryStream by the compiler"); } [TestMethod] @@ -326,33 +286,25 @@ public int Subtract(int first, int second) } } }"); - var input = new MutationTestInput() - { - SourceProjectInfo = new SourceProjectInfo + var analyzerResult = TestHelper.SetupProjectAnalyzerResult(properties: new Dictionary() { - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult(properties: new Dictionary() - { - { "TargetDir", "" }, - { "TargetFileName", "TargetFileName.dll"}, - { "AssemblyName", "AssemblyName"}, - { "SignAssembly", "true" }, - { "AssemblyOriginatorKeyFile", "DoesNotExist.snk" } - }, - projectFilePath: "project.csproj", - // add a reference to system so the example code can compile - references: new string[] { typeof(object).Assembly.Location } - ).Object - } + { "TargetDir", "" }, + { "TargetFileName", "TargetFileName.dll"}, + { "AssemblyName", "AssemblyName"}, + { "SignAssembly", "true" }, + { "AssemblyOriginatorKeyFile", "DoesNotExist.snk" } + }, + projectFilePath: "project.csproj", + // add a reference to system so the example code can compile + references: [typeof(object).Assembly.Location] + ).Object; - }; var rollbackProcessMock = new Mock(MockBehavior.Strict); - var target = new CsharpCompilingProcess(input, rollbackProcessMock.Object); + var target = new CsharpCompilingProcess(analyzerResult, rollbackProcessMock.Object); - using (var ms = new MemoryStream()) - { - Should.Throw(() => target.Compile(new Collection() { syntaxTree }, ms, null)); - } + using var ms = new MemoryStream(); + Should.Throw(() => target.Compile(new Collection() { syntaxTree }, ms, null)); } [TestMethod] @@ -370,34 +322,27 @@ public int Subtract(int first, int second) } } }"); - var input = new MutationTestInput() - { - SourceProjectInfo = new SourceProjectInfo + var analyzerResult = TestHelper.SetupProjectAnalyzerResult( + projectFilePath: "/c/project.csproj", + properties: new Dictionary() { - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult( - projectFilePath: "/c/project.csproj", - properties: new Dictionary() - { - { "TargetDir", "" }, - { "TargetFileName", "TargetFileName.dll" }, - { "AssemblyName", "AssemblyName"}, - }, - // add a reference to system so the example code can compile - references: new string[] { typeof(object).Assembly.Location } - ).Object - } - }; + { "TargetDir", "" }, + { "TargetFileName", "TargetFileName.dll" }, + { "AssemblyName", "AssemblyName"}, + }, + // add a reference to system so the example code can compile + references: [typeof(object).Assembly.Location] + ).Object; + var rollbackProcessMock = new Mock(MockBehavior.Strict); - var target = new CsharpCompilingProcess(input, rollbackProcessMock.Object); + var target = new CsharpCompilingProcess(analyzerResult, rollbackProcessMock.Object); - using (var ms = new MemoryStream()) - { - var result = target.Compile(new Collection() { syntaxTree }, ms, null); - result.Success.ShouldBe(true); + using var ms = new MemoryStream(); + var result = target.Compile(new Collection() { syntaxTree }, ms, null); + result.Success.ShouldBe(true); - Assembly.Load(ms.ToArray()).GetName().Version.ToString().ShouldBe("0.0.0.0"); - } + Assembly.Load(ms.ToArray()).GetName().Version.ToString().ShouldBe("0.0.0.0"); } [TestMethod] @@ -493,8 +438,8 @@ private static IEnumerable MutateAndCompileSource(string sourceFile) var input = new MutationTestInput { SourceProjectInfo = new SourceProjectInfo - { - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult( + ( + TestHelper.SetupProjectAnalyzerResult( projectFilePath: "/c/project.csproj", properties: new Dictionary { @@ -503,9 +448,9 @@ private static IEnumerable MutateAndCompileSource(string sourceFile) { "TargetFileName", "TargetFileName.dll" }, }, // add a reference to system so the example code can compile - references: new[] { typeof(object).Assembly.Location } + references: [typeof(object).Assembly.Location] ).Object, - TestProjectsInfo = new TestProjectsInfo(fileSystem) + new TestProjectsInfo(fileSystem) { TestProjects = new List { new TestProject(fileSystem, TestHelper.SetupProjectAnalyzerResult( @@ -521,7 +466,7 @@ private static IEnumerable MutateAndCompileSource(string sourceFile) ).Object), } } - }, + ), TestRunner = new Mock(MockBehavior.Default).Object }; diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Compiling/CSharpRollbackProcessTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Compiling/CSharpRollbackProcessTests.cs index 88d64fb7fa..8b425888a4 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Compiling/CSharpRollbackProcessTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Compiling/CSharpRollbackProcessTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; +using System.IO.Abstractions.TestingHelpers; using System.IO.Pipes; using System.Linq; using System.Reflection; @@ -15,10 +16,12 @@ using Stryker.Abstractions.Exceptions; using Stryker.Configuration.Options; using Stryker.Core.Compiling; +using Stryker.Core.Initialisation; using Stryker.Core.InjectedHelpers; using Stryker.Core.Mutants; using Stryker.Core.MutationTest; using Stryker.Core.ProjectComponents.SourceProjects; +using Stryker.Core.ProjectComponents.TestProjects; namespace Stryker.Core.UnitTest.Compiling; @@ -130,29 +133,23 @@ public void SomeLinq() }; Assembly.GetEntryAssembly().GetReferencedAssemblies().ToList().ForEach(a => references.Add(Assembly.Load(a).Location)); - var input = new MutationTestInput() - { - SourceProjectInfo = new SourceProjectInfo + var analyzerResult = TestHelper.SetupProjectAnalyzerResult( + properties: new Dictionary { - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult( - properties: new Dictionary() - { - { "TargetDir", "" }, - { "AssemblyName", "AssemblyName"}, - { "TargetFileName", "TargetFileName.dll"}, - { "SignAssembly", "true" }, - { "AssemblyOriginatorKeyFile", Path.GetFullPath(Path.Combine("TestResources", "StrongNameKeyFile.snk")) } - }, - projectFilePath: "TestResources", - // add a reference to system so the example code can compile - references: references.ToArray() - ).Object - } - }; + { "TargetDir", "" }, + { "AssemblyName", "AssemblyName"}, + { "TargetFileName", "TargetFileName.dll"}, + { "SignAssembly", "true" }, + { "AssemblyOriginatorKeyFile", Path.GetFullPath(Path.Combine("TestResources", "StrongNameKeyFile.snk")) } + }, + projectFilePath: "TestResources", + // add a reference to system so the example code can compile + references: references.ToArray() + ).Object; var rollbackProcess = new CSharpRollbackProcess(); - var target = new CsharpCompilingProcess(input, rollbackProcess, options); + var target = new CsharpCompilingProcess(analyzerResult, rollbackProcess, options); using var ms = new MemoryStream(); var result = target.Compile(helpers, ms, null); @@ -215,29 +212,23 @@ private void RefreshAccountNumber() }; Assembly.GetEntryAssembly().GetReferencedAssemblies().ToList().ForEach(a => references.Add(Assembly.Load(a).Location)); - var input = new MutationTestInput() - { - SourceProjectInfo = new SourceProjectInfo + var analyzerResult = TestHelper.SetupProjectAnalyzerResult( + properties: new Dictionary() { - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult( - properties: new Dictionary() - { - { "TargetDir", "" }, - { "AssemblyName", "AssemblyName"}, - { "TargetFileName", "TargetFileName.dll"}, - { "SignAssembly", "true" }, - { "AssemblyOriginatorKeyFile", Path.GetFullPath(Path.Combine("TestResources", "StrongNameKeyFile.snk")) } - }, - projectFilePath: "TestResources", - // add a reference to system so the example code can compile - references: references.ToArray() - ).Object - } - }; + { "TargetDir", "" }, + { "AssemblyName", "AssemblyName"}, + { "TargetFileName", "TargetFileName.dll"}, + { "SignAssembly", "true" }, + { "AssemblyOriginatorKeyFile", Path.GetFullPath(Path.Combine("TestResources", "StrongNameKeyFile.snk")) } + }, + projectFilePath: "TestResources", + // add a reference to system so the example code can compile + references: references.ToArray() + ).Object; var rollbackProcess = new CSharpRollbackProcess(); - var target = new CsharpCompilingProcess(input, rollbackProcess, options); + var target = new CsharpCompilingProcess(analyzerResult, rollbackProcess, options); using var ms = new MemoryStream(); diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InitialisationProcessTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InitialisationProcessTests.cs index f76b1c78d3..476ef2857e 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InitialisationProcessTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InitialisationProcessTests.cs @@ -21,6 +21,7 @@ using Stryker.TestRunner.Results; using Stryker.TestRunner.Tests; using Stryker.TestRunner.VsTest; +using Stryker.Utilities.Buildalyzer; namespace Stryker.Core.UnitTest.Initialisation; @@ -40,13 +41,12 @@ public void InitialisationProcess_ShouldCallNeededResolvers() new CsharpFileLeaf() }); inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())) - .Returns([ - new SourceProjectInfo + .Returns(new RelatedSourceProjectsInfo(null, [ + new SourceProjectInfo(TestHelper.SetupProjectAnalyzerResult(references: []).Object, null) { - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult(references: []).Object, ProjectContents = folder } - ]); + ])); inputFileResolverMock.SetupGet(x => x.FileSystem).Returns(new FileSystem()); var loggerMock = new Mock>(); @@ -58,7 +58,7 @@ public void InitialisationProcess_ShouldCallNeededResolvers() ProjectVersion = "TheProjectVersion" }; - var result = target.GetMutableProjectsInfo(options).ToList(); + var result = target.GetMutableProjectsInfo(options).SourceProjectInfos.ToList(); result.Count.ShouldBe(1); inputFileResolverMock.Verify(x => x.ResolveSourceProjectInfos(It.IsAny()), Times.Once); } @@ -74,12 +74,23 @@ public async Task InitialisationProcess_ShouldThrowOnFailedInitialTestRun() var folder = new CsharpFolderComposite(); folder.Add(new CsharpFileLeaf()); - var loggerMock = new Mock>(); var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); - var projectTracker = new ProjectsTracker(null, new StrykerOptions(), null, null, null, loggerMock.Object); - inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())).Returns( - [new SourceProjectInfo { AnalyzerResult = TestHelper.SetupProjectAnalyzerResult(references: []).Object, - TestProjectsInfo = new TestProjectsInfo(new MockFileSystem()), ProjectsTracker = projectTracker} - ]); + var loggerMock = new Mock>(); + var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); + + var options = new StrykerOptions + { + ProjectName = "TheProjectName", + ProjectVersion = "TheProjectVersion" + }; + var projectTracker = new ProjectsTracker(null, options, + new Mock(MockBehavior.Strict).Object, + new Mock().Object, + null, loggerMock.Object); + inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())). + Returns(new RelatedSourceProjectsInfo(projectTracker, + [new SourceProjectInfo( TestHelper.SetupProjectAnalyzerResult(references: []).Object, + new TestProjectsInfo(new MockFileSystem())) + ])); inputFileResolverMock.SetupGet(x => x.FileSystem).Returns(new FileSystem()); initialBuildProcessMock.Setup(x => x.InitialBuild(It.IsAny(), @@ -89,12 +100,6 @@ public async Task InitialisationProcess_ShouldThrowOnFailedInitialTestRun() testRunnerMock.Setup(x => x.DiscoverTestsAsync(It.IsAny())).Returns(Task.FromResult(true)); initialTestProcessMock.Setup(x => x.InitialTestAsync(It.IsAny(), It.IsAny(), It.IsAny())).ThrowsAsync(new InputException("")); // failing test - var options = new StrykerOptions - { - ProjectName = "TheProjectName", - ProjectVersion = "TheProjectVersion" - }; - var projects = target.GetMutableProjectsInfo(options); target.BuildProjects(options, projects); await Should.ThrowAsync(async () => await target.GetMutationTestInputsAsync(options, projects, testRunnerMock.Object)); @@ -112,13 +117,19 @@ public async Task InitialisationProcess_ShouldThrowIfHalfTestsAreFailing() var folder = new CsharpFolderComposite(); folder.Add(new CsharpFileLeaf()); - - var loggerMock = new Mock>(); var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); - var projectTracker = new ProjectsTracker(null, new StrykerOptions(), null, null, null, loggerMock.Object); - inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())).Returns( - [new SourceProjectInfo { AnalyzerResult = TestHelper.SetupProjectAnalyzerResult(references: []).Object, - TestProjectsInfo = new TestProjectsInfo(new MockFileSystem()), ProjectsTracker = projectTracker} - ]); + var loggerMock = new Mock>(); + var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); + var options = new StrykerOptions + { + ProjectName = "TheProjectName", + ProjectVersion = "TheProjectVersion" + }; + var projectTracker = new ProjectsTracker(null, options, + new Mock(MockBehavior.Strict).Object, + new Mock().Object, + null, loggerMock.Object); + inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())).Returns(new RelatedSourceProjectsInfo(projectTracker, + [new SourceProjectInfo(TestHelper.SetupProjectAnalyzerResult(references: []).Object, new TestProjectsInfo(new MockFileSystem()))])); inputFileResolverMock.SetupGet(x => x.FileSystem).Returns(fileSystemMock); initialBuildProcessMock.Setup(x => x.InitialBuild(It.IsAny(), @@ -138,11 +149,6 @@ public async Task InitialisationProcess_ShouldThrowIfHalfTestsAreFailing() new InitialTestRun( new TestRunResult(Array.Empty(), ranTests, failedTests, TestIdentifierList.NoTest(), string.Empty, Enumerable.Empty(), TimeSpan.Zero), new TimeoutValueCalculator(0))); // failing test - var options = new StrykerOptions - { - ProjectName = "TheProjectName", - ProjectVersion = "TheProjectVersion" - }; var projects = target.GetMutableProjectsInfo(options); target.BuildProjects(options, projects); await Should.ThrowAsync(async () => await target.GetMutationTestInputsAsync(options, projects, testRunnerMock.Object)); @@ -164,11 +170,18 @@ public async Task InitialisationProcess_ShouldThrowOnTestTestIfAskedFor(bool bre folder.Add(new CsharpFileLeaf()); var loggerMock = new Mock>(); var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); - var projectTracker = new ProjectsTracker(null, new StrykerOptions(), null, null, null, loggerMock.Object); - inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())).Returns( - [new SourceProjectInfo { AnalyzerResult = TestHelper.SetupProjectAnalyzerResult(references: []).Object, - TestProjectsInfo = new TestProjectsInfo(new MockFileSystem()), ProjectsTracker = projectTracker} - ]); + var options = new StrykerOptions + { + ProjectName = "TheProjectName", + ProjectVersion = "TheProjectVersion", + BreakOnInitialTestFailure = breakOnInitialTestFailure + }; + var projectTracker = new ProjectsTracker(null, options, + new Mock(MockBehavior.Strict).Object, + new Mock().Object, + null, loggerMock.Object); + inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())).Returns(new RelatedSourceProjectsInfo(projectTracker, + [new SourceProjectInfo(TestHelper.SetupProjectAnalyzerResult(references: []).Object, new TestProjectsInfo(new MockFileSystem()))])); inputFileResolverMock.SetupGet(x => x.FileSystem).Returns(new FileSystem()); initialBuildProcessMock.Setup(x => x.InitialBuild(It.IsAny(), It.IsAny(), @@ -186,12 +199,6 @@ public async Task InitialisationProcess_ShouldThrowOnTestTestIfAskedFor(bool bre initialTestProcessMock.Setup(x => x.InitialTestAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(new InitialTestRun( new TestRunResult(Array.Empty(), ranTests, failedTests, TestIdentifierList.NoTest(), string.Empty, Enumerable.Empty(), TimeSpan.Zero), new TimeoutValueCalculator(0))); // failing test - var options = new StrykerOptions - { - ProjectName = "TheProjectName", - ProjectVersion = "TheProjectVersion", - BreakOnInitialTestFailure = breakOnInitialTestFailure - }; var projects = target.GetMutableProjectsInfo(options); target.BuildProjects(options, projects); if (breakOnInitialTestFailure) @@ -206,7 +213,6 @@ public async Task InitialisationProcess_ShouldThrowOnTestTestIfAskedFor(bool bre } } - [TestMethod] public async Task InitialisationProcess_ShouldRunTestSession() { @@ -218,28 +224,36 @@ public async Task InitialisationProcess_ShouldRunTestSession() var folder = new CsharpFolderComposite(); folder.Add(new CsharpFileLeaf()); - var loggerMock = new Mock>(); var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); - var projectTracker = new ProjectsTracker(null, new StrykerOptions(), null, null, null, loggerMock.Object); - inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())).Returns( - [new SourceProjectInfo { AnalyzerResult = TestHelper.SetupProjectAnalyzerResult(references: []).Object, TestProjectsInfo = new TestProjectsInfo(new MockFileSystem()), ProjectsTracker = projectTracker} - ]); - - inputFileResolverMock.SetupGet(x => x.FileSystem).Returns(new FileSystem()); + var loggerMock = new Mock>(); + var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); + var options = new StrykerOptions + { + ProjectName = "TheProjectName", + ProjectVersion = "TheProjectVersion" + }; + var projectTracker = new ProjectsTracker(null, options, + new Mock(MockBehavior.Strict).Object, + new Mock().Object, + null, loggerMock.Object); + + inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())). + Returns(new RelatedSourceProjectsInfo(projectTracker, + [new SourceProjectInfo(TestHelper.SetupProjectAnalyzerResult(references: []).Object, + new TestProjectsInfo(new MockFileSystem()))])); + + var fileSystem = new MockFileSystem(); + inputFileResolverMock.SetupGet(x => x.FileSystem).Returns(fileSystem); 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.DiscoverTestsAsync(It.IsAny())).Returns(Task.FromResult(true)); testRunnerMock.Setup(x => x.GetTests(It.IsAny())).Returns(testSet); - initialTestProcessMock.Setup(x => x.InitialTestAsync(It.IsAny(), It.IsAny(), It.IsAny())) + initialTestProcessMock.Setup(x => x.InitialTestAsync(It.IsAny(), + It.IsAny(), + It.IsAny())) .Returns(Task.FromResult(new InitialTestRun(new TestRunResult(true), null))); // failing test - var options = new StrykerOptions - { - ProjectName = "TheProjectName", - ProjectVersion = "TheProjectVersion" - }; - var projects = target.GetMutableProjectsInfo(options); target.BuildProjects(options, projects); await target.GetMutationTestInputsAsync(options, projects, testRunnerMock.Object); @@ -264,7 +278,6 @@ public async Task InitialisationProcess_ShouldThrowOnWhenNoTestDetected(string l var folder = new CsharpFolderComposite(); folder.Add(new CsharpFileLeaf()); - var testProjectAnalyzerResult = TestHelper.SetupProjectAnalyzerResult( projectFilePath: "C://Example/Dir/ProjectFolder", targetFramework: "netcoreapp2.1", @@ -272,21 +285,25 @@ public async Task InitialisationProcess_ShouldThrowOnWhenNoTestDetected(string l inputFileResolverMock.SetupGet(x => x.FileSystem).Returns(new FileSystem()); - var loggerMock = new Mock>(); var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); - var projectTracker = new ProjectsTracker(null, new StrykerOptions(), null, null, null, loggerMock.Object); - inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())).Returns( + var loggerMock = new Mock>(); + var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); + var options = new StrykerOptions + { + ProjectName = "TheProjectName", + ProjectVersion = "TheProjectVersion" + }; + var projectTracker = new ProjectsTracker(null, options, + new Mock(MockBehavior.Strict).Object, + new Mock().Object, + null, loggerMock.Object); + inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())).Returns(new RelatedSourceProjectsInfo(projectTracker, [ - new SourceProjectInfo - { - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult( - references: []).Object, - ProjectsTracker = projectTracker, - TestProjectsInfo = new TestProjectsInfo(new MockFileSystem()) - { - TestProjects = new List {new(new MockFileSystem(), testProjectAnalyzerResult)} - } - } - ]); + new SourceProjectInfo( + TestHelper.SetupProjectAnalyzerResult(references: []).Object, + new TestProjectsInfo(new MockFileSystem()) { + TestProjects = new List {new(new MockFileSystem(), testProjectAnalyzerResult)} } + ) + ])); initialBuildProcessMock.Setup(x => x.InitialBuild(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), null, It.IsAny())); @@ -295,11 +312,6 @@ public async Task InitialisationProcess_ShouldThrowOnWhenNoTestDetected(string l initialTestProcessMock.Setup(x => x.InitialTestAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.FromResult(new InitialTestRun(new TestRunResult(Array.Empty(), TestIdentifierList.NoTest(), TestIdentifierList.NoTest(), TestIdentifierList.NoTest(), string.Empty, Enumerable.Empty(), TimeSpan.Zero), null))); // failing test - var options = new StrykerOptions - { - ProjectName = "TheProjectName", - ProjectVersion = "TheProjectVersion" - }; var projects = target.GetMutableProjectsInfo(options); target.BuildProjects(options, projects); var exception = await Should.ThrowAsync(async () => await target.GetMutationTestInputsAsync(options, projects, testRunnerMock.Object)); @@ -331,18 +343,21 @@ public void InitialisationProcess_ShouldThrowOnWhenNoTestDetectedAndCorrectDepen inputFileResolverMock.SetupGet(x => x.FileSystem).Returns(new FileSystem()); var loggerMock = new Mock>(); var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); - var projectTracker = new ProjectsTracker(null, new StrykerOptions(), null, null, null, loggerMock.Object); - inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())).Returns( - [ - new SourceProjectInfo - { - ProjectsTracker = projectTracker, - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult( - references: []).Object, - TestProjectsInfo = new TestProjectsInfo(new MockFileSystem()){TestProjects = new List {new(new MockFileSystem(), testProjectAnalyzerResult)}} - } - ]); + var options = new StrykerOptions + { + ProjectName = "TheProjectName", + ProjectVersion = "TheProjectVersion" + }; + var projectTracker = new ProjectsTracker(null, options, + new Mock(MockBehavior.Strict).Object, + new Mock().Object, + null, loggerMock.Object); + inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())). + Returns(new RelatedSourceProjectsInfo(projectTracker, + [new SourceProjectInfo(TestHelper.SetupProjectAnalyzerResult( + references: []).Object + , new TestProjectsInfo(new MockFileSystem()){TestProjects = new List {new(new MockFileSystem(), testProjectAnalyzerResult)}})])); initialBuildProcessMock.Setup(x => x.InitialBuild(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), null, It.IsAny())); @@ -351,11 +366,6 @@ public void InitialisationProcess_ShouldThrowOnWhenNoTestDetectedAndCorrectDepen initialTestProcessMock.Setup(x => x.InitialTestAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.FromResult(new InitialTestRun(new TestRunResult(Array.Empty(), TestIdentifierList.NoTest(), TestIdentifierList.NoTest(), TestIdentifierList.NoTest(), string.Empty, Enumerable.Empty(), TimeSpan.Zero), null))); // failing test - var options = new StrykerOptions - { - ProjectName = "TheProjectName", - ProjectVersion = "TheProjectVersion" - }; var projects = target.GetMutableProjectsInfo(options); target.BuildProjects(options, projects); Should.Throw(async () => await target.GetMutationTestInputsAsync(options, projects, testRunnerMock.Object)).Message.ShouldContain("failed to deploy or run."); diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InputFileResolverTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InputFileResolverTests.cs index ff0f47416b..ea08a6feee 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InputFileResolverTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InputFileResolverTests.cs @@ -156,7 +156,7 @@ public void InitializeShouldFindFilesRecursively() var target = BuildTestResolver(fileSystem); - var result = target.ResolveSourceProjectInfos(_options).First(); + var result = target.ResolveSourceProjectInfos(_options).SourceProjectInfos.First(); result.ProjectContents.GetAllFiles().Count().ShouldBe(3); } @@ -191,7 +191,7 @@ public void InitializeShouldUseBuildalyzerResult() var target = BuildTestResolver(fileSystem); - var result = target.ResolveSourceProjectInfos(_options).First(); + var result = target.ResolveSourceProjectInfos(_options).SourceProjectInfos.First(); result.ProjectContents.GetAllFiles().Count().ShouldBe(5); } @@ -230,7 +230,7 @@ public void ShouldUseCustomMsBuildPath() var target = BuildTestResolver(fileSystem); var options = new StrykerOptions { MsBuildPath = "\\msbuild.exe",ProjectPath = _testPath }; - var result = target.ResolveSourceProjectInfos(options).First(); + var result = target.ResolveSourceProjectInfos(options).SourceProjectInfos.First(); result.ProjectContents.GetAllFiles().Count().ShouldBe(4); } @@ -260,7 +260,7 @@ public void ShouldHandleFailedAnalysis() var target = BuildTestResolver(fileSystem); - var action = () => target.ResolveSourceProjectInfos(_options).First(); + var action = () => target.ResolveSourceProjectInfos(_options).SourceProjectInfos.First(); action.ShouldThrow(); } @@ -290,7 +290,7 @@ public void ShouldSupportTestProjectFailedAnalysis() var target = BuildTestResolver(fileSystem); - var action = () => target.ResolveSourceProjectInfos(_options).First(); + var action = () => target.ResolveSourceProjectInfos(_options).SourceProjectInfos.First(); action.ShouldNotThrow(); } @@ -322,7 +322,7 @@ public void ShouldFailIfSolutionNotFound() }; var target = BuildTestResolver(fileSystem); - var action = () => target.ResolveSourceProjectInfos(options).First(); + var action = () => target.ResolveSourceProjectInfos(options).SourceProjectInfos.First(); action.ShouldThrow(); } @@ -355,7 +355,7 @@ public void ShouldFailIfSolutionLoadFails() var target = BuildTestResolverWithSolutionProvider(fileSystem, new CustomSolutionProvider(_ => throw new IOException("Failed to read solution"))); - target.ResolveSourceProjectInfos(options).ShouldBeEmpty(); + target.ResolveSourceProjectInfos(options).SourceProjectInfos.ShouldBeEmpty(); } [TestMethod] @@ -381,7 +381,7 @@ public void ShouldFailIfSolutionCantBeAccessed() var target = BuildTestResolverWithSolutionProvider(fileSystem, new CustomSolutionProvider(_ => throw new UnauthorizedAccessException("Access forbidden"))); - target.ResolveSourceProjectInfos(options).ShouldBeEmpty(); + target.ResolveSourceProjectInfos(options).SourceProjectInfos.ShouldBeEmpty(); } [TestMethod] @@ -409,7 +409,7 @@ public void InitializeShouldNotSkipXamlFiles() var target = BuildTestResolver(fileSystem); - var result = target.ResolveSourceProjectInfos(_options).First(); + var result = target.ResolveSourceProjectInfos(_options).SourceProjectInfos.First(); result.ProjectContents.GetAllFiles().Count().ShouldBe(5); } @@ -459,7 +459,7 @@ public void InitializeShouldMutateAssemblyInfo() var target = BuildTestResolver(fileSystem); - var result = target.ResolveSourceProjectInfos(_options).First(); + var result = target.ResolveSourceProjectInfos(_options).SourceProjectInfos.First(); result.ProjectContents.GetAllFiles().Count().ShouldBe(3); var mutatedFile = ((ProjectComponent)result.ProjectContents).CompilationSyntaxTrees.First(s => s != null && s.FilePath.Contains("AssemblyInfo.cs")); @@ -516,7 +516,7 @@ public void InitializeShouldNotMutateIncompleteAssemblyInfo() var target = BuildTestResolver(fileSystem); - var result = target.ResolveSourceProjectInfos(_options).First(); + var result = target.ResolveSourceProjectInfos(_options).SourceProjectInfos.First(); ((ProjectComponent)result.ProjectContents).CompilationSyntaxTrees.FirstOrDefault(s => s != null && s.FilePath.Contains("AssemblyInfo.cs")). ShouldBeSemantically(CSharpSyntaxTree.ParseText(textContents)); @@ -545,7 +545,7 @@ public void InitializeShouldFindSpecifiedTestProjectFile() var target = BuildTestResolver(fileSystem); - var result = target.ResolveSourceProjectInfos(_options).First(); + var result = target.ResolveSourceProjectInfos(_options).SourceProjectInfos.First(); result.ProjectContents.GetAllFiles().Count().ShouldBe(2); } @@ -602,7 +602,7 @@ public void InitializeShouldResolveImportedProject() var target = BuildTestResolver(fileSystem); - var result = target.ResolveSourceProjectInfos(_options).First(); + var result = target.ResolveSourceProjectInfos(_options).SourceProjectInfos.First(); result.ProjectContents.GetAllFiles().Count().ShouldBe(3); } @@ -648,7 +648,7 @@ public void InitializeShouldNotResolveImportedPropsFile() var target = BuildTestResolver(fileSystem); - var result = target.ResolveSourceProjectInfos(_options).First(); + var result = target.ResolveSourceProjectInfos(_options).SourceProjectInfos.First(); result.ProjectContents.GetAllFiles().Count().ShouldBe(2); } @@ -718,7 +718,7 @@ public void InitializeShouldResolveMultipleImportedProjects() BuildBuildAnalyzerMock(analyzerResults); var target = BuildTestResolver(fileSystem); - var result = target.ResolveSourceProjectInfos(_options).First(); + var result = target.ResolveSourceProjectInfos(_options).SourceProjectInfos.First(); result.ProjectContents.GetAllFiles().Count().ShouldBe(4); } @@ -817,7 +817,7 @@ public void InitializeShouldResolvePropertiesInSharedProjectImports() BuildBuildAnalyzerMock(analyzerResults); var target = BuildTestResolver(fileSystem); - var result = target.ResolveSourceProjectInfos(_options).First(); + var result = target.ResolveSourceProjectInfos(_options).SourceProjectInfos.First(); var allFiles = result.ProjectContents.GetAllFiles(); @@ -901,7 +901,7 @@ public void InitializeShouldIgnoreBinFolder(string folderName) var target = BuildTestResolver(fileSystem); - var result = target.ResolveSourceProjectInfos(_options).First(); + var result = target.ResolveSourceProjectInfos(_options).SourceProjectInfos.First(); ((CsharpFolderComposite)result.ProjectContents).Children.Count().ShouldBe(1); @@ -1082,7 +1082,7 @@ public void ShouldSelectAvailableFramework_WhenDesiredNotFound(string targetFram var target = BuildTestResolver(fileSystem); // Act - var result = target.ResolveSourceProjectInfos(options).First(); + var result = target.ResolveSourceProjectInfos(options).SourceProjectInfos.First(); // Assert result.AnalyzerResult.TargetFramework.ShouldBe(DefaultFramework); @@ -1136,7 +1136,7 @@ public void ShouldSelectFrameworkBasedOnTestProject(string testFrameworks var target = BuildTestResolver(fileSystem); // Act - var result = target.ResolveSourceProjectInfos(options).First(); + var result = target.ResolveSourceProjectInfos(options).SourceProjectInfos.First(); // Assert result.AnalyzerResult.TargetFramework.ShouldBe(expectedFramework); @@ -1188,7 +1188,7 @@ public void ShouldSelectFrameworkBasedOnTestProjectOnWindows(string testFramewor var target = BuildTestResolver(fileSystem); // Act - var result = target.ResolveSourceProjectInfos(options).First(); + var result = target.ResolveSourceProjectInfos(options).SourceProjectInfos.First(); // Assert result.AnalyzerResult.TargetFramework.ShouldBe(expectedFramework); @@ -1244,7 +1244,7 @@ public void ShouldSkipXamlFiles() var target = BuildTestResolver(fileSystem); // Act - var result = target.ResolveSourceProjectInfos(_options).First(); + var result = target.ResolveSourceProjectInfos(_options).SourceProjectInfos.First(); result.ProjectContents.GetAllFiles().Count().ShouldBe(2); } @@ -1288,7 +1288,7 @@ public void ShouldFindAllTestProjects() var target = BuildTestResolver(fileSystem); // Act - var result = target.ResolveSourceProjectInfos(options).First(); + var result = target.ResolveSourceProjectInfos(options).SourceProjectInfos.First(); // Assert result.ProjectContents.GetAllFiles().Count().ShouldBe(1); @@ -1316,7 +1316,7 @@ public void ShouldFindSourceProjectWhenSingleProjectReferenceAndNoFilter() var target = BuildTestResolver(fileSystem); - var result = target.ResolveSourceProjectInfos(_options).First(); + var result = target.ResolveSourceProjectInfos(_options).SourceProjectInfos.First(); result.AnalyzerResult.ProjectFilePath.ShouldBe(_sourceProjectFilePath); } @@ -1416,7 +1416,7 @@ public void ShouldNotThrowIfMultipleProjectButOneIsAlwaysReferenced() WorkingDirectory = test2Path }; // Act - var result = target.ResolveSourceProjectInfos(options).First(); + var result = target.ResolveSourceProjectInfos(options).SourceProjectInfos.First(); // Assert result.AnalyzerResult.ProjectFilePath.ShouldBe(_sourceProjectFilePath); @@ -1459,7 +1459,7 @@ public void ShouldSelectProjectInWorkingDir() WorkingDirectory = _sourcePath }; // Act - var result = target.ResolveSourceProjectInfos(options).First(); + var result = target.ResolveSourceProjectInfos(options).SourceProjectInfos.First(); // Assert result.AnalyzerResult.ProjectFilePath.ShouldBe(_sourceProjectFilePath); @@ -1498,7 +1498,7 @@ public void ShouldMatchFromMultipleProjectByName(string shouldMatch) var target = BuildTestResolver(fileSystem); var options = new StrykerOptions { SourceProjectName = shouldMatch, ProjectPath = _testPath }; - var result = target.ResolveSourceProjectInfos(options).First(); + var result = target.ResolveSourceProjectInfos(options).SourceProjectInfos.First(); result.AnalyzerResult.ProjectFilePath.ShouldBe(_sourceProjectFilePath); } @@ -1588,7 +1588,7 @@ public void ShouldFallbackToProjectReferenceIfDependencyNotFound() var target = BuildTestResolver(fileSystem); - var result = target.ResolveSourceProjectInfos(_options).First(); + var result = target.ResolveSourceProjectInfos(_options).SourceProjectInfos.First(); result.AnalyzerResult.ProjectFilePath.ShouldBe(_sourceProjectFilePath); } @@ -1725,7 +1725,7 @@ public void ShouldMatchOnBothForwardAndBackwardsSlash(string shouldMatch) var target = BuildTestResolver(fileSystem); var options = new StrykerOptions { SourceProjectName = shouldMatch, ProjectPath = _testPath }; - var result = target.ResolveSourceProjectInfos(_options).First(); + var result = target.ResolveSourceProjectInfos(_options).SourceProjectInfos.First(); result.AnalyzerResult.ProjectFilePath.ShouldBe(_sourceProjectFilePath); } diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/ProjectMutatorTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/ProjectMutatorTests.cs index 02f8d74ecc..9b0cf1c5e0 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/ProjectMutatorTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/ProjectMutatorTests.cs @@ -79,22 +79,21 @@ public ProjectMutatorTests() SyntaxTree = CSharpSyntaxTree.ParseText("class TestClass { }") }); + var testProjectsInfo = new TestProjectsInfo(_fileSystemMock) + { + TestProjects = new List + { + new(_fileSystemMock, TestHelper.SetupProjectAnalyzerResult( + projectFilePath: "c:\\testproject.csproj", + targetFramework: "netcoreapp3.1", + sourceFiles: [_testFilePath]).Object) + } + }; _mutationTestInput = new MutationTestInput() { - SourceProjectInfo = new Stryker.Core.ProjectComponents.SourceProjects.SourceProjectInfo() + SourceProjectInfo = new Stryker.Core.ProjectComponents.SourceProjects.SourceProjectInfo(analyzerResult, testProjectsInfo) { - AnalyzerResult = analyzerResult, ProjectContents = folder, - TestProjectsInfo = new TestProjectsInfo(_fileSystemMock) - { - TestProjects = new List - { - new(_fileSystemMock, TestHelper.SetupProjectAnalyzerResult( - projectFilePath: "c:\\testproject.csproj", - targetFramework: "netcoreapp3.1", - sourceFiles: [_testFilePath]).Object) - } - } } }; } diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/MutationTest/CSharpMutationTestProcessTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/MutationTest/CSharpMutationTestProcessTests.cs index 9f74a65cda..e22d52fdf5 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/MutationTest/CSharpMutationTestProcessTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/MutationTest/CSharpMutationTestProcessTests.cs @@ -7,7 +7,6 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; -using Stryker.Abstractions; using Stryker.Configuration.Options; using Stryker.Core.Mutants; using Stryker.Core.MutationTest; @@ -49,9 +48,8 @@ public void MutateShouldWriteToDisk_IfCompilationIsSuccessful() var input = new MutationTestInput() { - SourceProjectInfo = new SourceProjectInfo() - { - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult( + SourceProjectInfo = new SourceProjectInfo( + TestHelper.SetupProjectAnalyzerResult( projectFilePath: "/c/ProjectUnderTest/ProjectUnderTest.csproj", properties: new Dictionary() { @@ -60,9 +58,8 @@ public void MutateShouldWriteToDisk_IfCompilationIsSuccessful() { "AssemblyName", "AssemblyName" }, { "Language", "C#" } }, - references: new[] { typeof(object).Assembly.Location }).Object, - ProjectContents = folder, - TestProjectsInfo = new TestProjectsInfo(fileSystem) + references: [typeof(object).Assembly.Location]).Object, + new TestProjectsInfo(fileSystem) { TestProjects = new List { new(fileSystem, TestHelper.SetupProjectAnalyzerResult(properties: new Dictionary() @@ -72,7 +69,9 @@ public void MutateShouldWriteToDisk_IfCompilationIsSuccessful() { "Language", "C#" } }).Object) } - } + }) + { + ProjectContents = folder, } }; diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/MutationTest/MutationTestProcessTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/MutationTest/MutationTestProcessTests.cs index a2ff29f733..a27ec5a8af 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/MutationTest/MutationTestProcessTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/MutationTest/MutationTestProcessTests.cs @@ -55,9 +55,8 @@ public MutationTestProcessTests() }; Input = new MutationTestInput() { - SourceProjectInfo = new SourceProjectInfo() - { - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult( + SourceProjectInfo = new SourceProjectInfo( + TestHelper.SetupProjectAnalyzerResult( projectFilePath: Path.Combine(FilesystemRoot, "ProjectUnderTest", "ProjectUnderTest.csproj"), properties: new Dictionary() { @@ -66,8 +65,9 @@ public MutationTestProcessTests() { "AssemblyName", "ProjectUnderTest" }, { "Language", "C#" } }).Object, - ProjectContents = Folder, - TestProjectsInfo = testProjectsInfo + testProjectsInfo) + { + ProjectContents = Folder } }; } @@ -78,7 +78,7 @@ public void ShouldCallMutationProcess_MutateAndFilterMutants() // Arrange var options = new StrykerOptions() { - ExcludedMutations = new Mutator[] { } + ExcludedMutations = [] }; var executorMock = new Mock(); diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/CollectionExpressionMutatorTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/CollectionExpressionMutatorTests.cs index 0cee079fce..c95dfac992 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/CollectionExpressionMutatorTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Mutators/CollectionExpressionMutatorTests.cs @@ -17,9 +17,7 @@ using Stryker.Core.Compiling; using Stryker.Core.InjectedHelpers; using Stryker.Core.Mutants; -using Stryker.Core.MutationTest; using Stryker.Core.Mutators; -using Stryker.Core.ProjectComponents.SourceProjects; namespace Stryker.Core.UnitTest.Mutators; @@ -473,27 +471,21 @@ public void MutatedCollectionExpressionsShouldCompile(string inputText, int expe ..Assembly.GetEntryAssembly()?.GetReferencedAssemblies().Select(a => Assembly.Load(a).Location) ?? [] ]; - var input = new MutationTestInput - { - SourceProjectInfo = new SourceProjectInfo - { - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult(projectFilePath: "/c/project.csproj", - properties: new Dictionary - { - { "TargetDir", "" }, - { "AssemblyName", "AssemblyName" }, - { - "TargetFileName", - "TargetFileName.dll" - } - }, - references: references.ToArray() - ) - .Object - } - }; - - var target = new CsharpCompilingProcess(input); + var analyzerResult = TestHelper.SetupProjectAnalyzerResult(projectFilePath: "/c/project.csproj", + properties: new Dictionary + { + { "TargetDir", "" }, + { "AssemblyName", "AssemblyName" }, + { + "TargetFileName", + "TargetFileName.dll" + } + }, + references: references.ToArray() + ) + .Object; + + var target = new CsharpCompilingProcess(analyzerResult); try { diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/ProjectComponents/SourceProjects/SourceProjectInfoTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/ProjectComponents/SourceProjects/SourceProjectInfoTests.cs index fdcfdc6f21..e7a24e3101 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/ProjectComponents/SourceProjects/SourceProjectInfoTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/ProjectComponents/SourceProjects/SourceProjectInfoTests.cs @@ -13,15 +13,14 @@ public class SourceProjectInfoTests : TestBase [TestMethod] public void ShouldGenerateProperDefaultCompilationOptions() { - var target = new SourceProjectInfo() - { - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult( - properties: new Dictionary() { + var target = new SourceProjectInfo( + TestHelper.SetupProjectAnalyzerResult( + properties: new Dictionary() + { { "TargetDir", "/test/bin/Debug/" }, { "TargetFileName", "TestName.dll" }, { "AssemblyName", "AssemblyName" } - }).Object - }; + }).Object, null); var options = target.AnalyzerResult.GetCompilationOptions(); @@ -35,17 +34,16 @@ public void ShouldGenerateProperDefaultCompilationOptions() [DataRow("AppContainerExe", OutputKind.WindowsRuntimeApplication)] public void ShouldGenerateProperCompilationOptions(string kindParam, OutputKind output) { - var target = new SourceProjectInfo() - { - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult( - properties: new Dictionary() { - { "AssemblyTitle", "TargetFileName"}, + var target = new SourceProjectInfo( + TestHelper.SetupProjectAnalyzerResult( + properties: new Dictionary() + { + { "AssemblyTitle", "TargetFileName" }, { "TargetDir", "/test/bin/Debug/" }, - { "TargetFileName", "TargetFileName.dll"}, + { "TargetFileName", "TargetFileName.dll" }, { "OutputType", kindParam }, { "AssemblyName", "AssemblyName" } - }).Object - }; + }).Object, null); var options = target.AnalyzerResult.GetCompilationOptions(); @@ -60,18 +58,17 @@ public void ShouldGenerateProperCompilationOptions(string kindParam, OutputKind [DataRow(true, true)] public void ShouldGenerateProperSigningCompilationOptions(bool signAssembly, bool delaySign) { - var target = new SourceProjectInfo() - { - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult( - properties: new Dictionary() { + var target = new SourceProjectInfo( + TestHelper.SetupProjectAnalyzerResult( + properties: new Dictionary() + { { "TargetDir", "/test/bin/Debug/" }, { "TargetFileName", "TestName.dll" }, { "AssemblyName", "AssemblyName" }, { "SignAssembly", signAssembly.ToString() }, { "DelaySign", delaySign.ToString() }, { "AssemblyOriginatorKeyFile", "test/keyfile.snk" } - }).Object - }; + }).Object, null); var options = target.AnalyzerResult.GetCompilationOptions(); @@ -118,10 +115,7 @@ public void ShouldGenerateProperNullableCompilationOptions( properties.Add(nullableTuple.Value.Key, nullableTuple.Value.Value); } - var target = new SourceProjectInfo() - { - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult(properties: properties).Object - }; + var target = new SourceProjectInfo(TestHelper.SetupProjectAnalyzerResult(properties: properties).Object, null); var options = target.AnalyzerResult.GetCompilationOptions(); options.NullableContextOptions.ShouldBe(expectedNullable); diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/StrykerRunnerTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/StrykerRunnerTests.cs index 8b9c5cf080..49ea185e56 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/StrykerRunnerTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/StrykerRunnerTests.cs @@ -43,12 +43,11 @@ public async Task Stryker_ShouldInvokeAllProcesses() Mutants = new List { new Mutant { Id = 1 } } }); - var projectInfo = Mock.Of(); - projectInfo.ProjectContents = folder; + var info = new SourceProjectInfo(null, new TestProjectsInfo(null)) { ProjectContents = folder }; var mutationTestInput = new MutationTestInput() { - SourceProjectInfo = projectInfo + SourceProjectInfo = info }; inputsMock.Setup(x => x.ValidateAll()).Returns(new StrykerOptions @@ -111,10 +110,10 @@ public async Task ShouldStop_WhenAllMutationsWereIgnored() { Mutants = new Collection() { new Mutant() { Id = 1, ResultStatus = MutantStatus.Ignored } } }); - + var mutationTestInput = new MutationTestInput() { - SourceProjectInfo = new SourceProjectInfo() + SourceProjectInfo = new SourceProjectInfo(null, null) { ProjectContents = folder }, @@ -160,22 +159,12 @@ public async Task ShouldThrow_WhenNoProjectsFound() var reporterFactoryMock = new Mock(MockBehavior.Strict); var reporterMock = new Mock(MockBehavior.Strict); var inputsMock = new Mock(MockBehavior.Strict); - var fileSystemMock = new MockFileSystem(); var folder = new CsharpFolderComposite(); folder.Add(new CsharpFileLeaf { Mutants = new Collection() { new Mutant() { Id = 1, ResultStatus = MutantStatus.Ignored } } }); - var testRunnerMock = new Mock(MockBehavior.Strict); - - var mutationTestInput = new MutationTestInput() - { - SourceProjectInfo = new SourceProjectInfo() - { - ProjectContents = folder - } - }; inputsMock.Setup(x => x.ValidateAll()).Returns(new StrykerOptions { diff --git a/src/Stryker.Core/Stryker.Core/Compiling/CsharpCompilingProcess.cs b/src/Stryker.Core/Stryker.Core/Compiling/CsharpCompilingProcess.cs index 79091f64b2..ea81a3591f 100644 --- a/src/Stryker.Core/Stryker.Core/Compiling/CsharpCompilingProcess.cs +++ b/src/Stryker.Core/Stryker.Core/Compiling/CsharpCompilingProcess.cs @@ -11,11 +11,9 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Emit; using Microsoft.Extensions.Logging; -using Stryker.Abstractions; using Stryker.Abstractions.Exceptions; using Stryker.Abstractions.Options; using Stryker.Configuration.Options; -using Stryker.Core.MutationTest; using Stryker.Utilities.Buildalyzer; using Stryker.Utilities.EmbeddedResources; using Stryker.Utilities.Logging; @@ -35,12 +33,12 @@ public interface ICSharpCompilingProcess public class CsharpCompilingProcess : ICSharpCompilingProcess { private const int MaxAttempt = 50; - private readonly MutationTestInput _input; + private readonly IAnalyzerResult _input; private readonly IStrykerOptions _options; private readonly ICSharpRollbackProcess _rollbackProcess; private readonly ILogger _logger; - public CsharpCompilingProcess(MutationTestInput input, + public CsharpCompilingProcess(IAnalyzerResult input, ICSharpRollbackProcess rollbackProcess = null, IStrykerOptions options = null) { @@ -51,7 +49,7 @@ public CsharpCompilingProcess(MutationTestInput input, } private string AssemblyName => - _input.SourceProjectInfo.AnalyzerResult.GetAssemblyName(); + _input.GetAssemblyName(); /// /// Compiles the given input onto the memory stream @@ -117,11 +115,11 @@ public IEnumerable GetSemanticModels(IEnumerable synt // Can't test or mock code generators, so we exclude them from coverage [ExcludeFromCodeCoverage] - private CSharpCompilation RunSourceGenerators(IAnalyzerResult analyzerResult, Compilation compilation) + private CSharpCompilation RunSourceGenerators(Compilation compilation) { - var generators = analyzerResult.GetSourceGenerators(_logger); + var generators = _input.GetSourceGenerators(_logger); _ = CSharpGeneratorDriver - .Create(generators, parseOptions: analyzerResult.GetParseOptions(_options), optionsProvider: new SimpleAnalyserConfigOptionsProvider(analyzerResult)) + .Create(generators, parseOptions: _input.GetParseOptions(_options), optionsProvider: new SimpleAnalyserConfigOptionsProvider(_input)) .RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics); var errors = diagnostics.Where(diagnostic => IgnoredErrors.Contains(diagnostic.Id) || (diagnostic.Severity == DiagnosticSeverity.Error && diagnostic.Location == Location.None)).ToList(); @@ -151,15 +149,14 @@ private CSharpCompilation RunSourceGenerators(IAnalyzerResult analyzerResult, Co private CSharpCompilation GetCSharpCompilation(IEnumerable syntaxTrees) { - var analyzerResult = _input.SourceProjectInfo.AnalyzerResult; var compilation = CSharpCompilation.Create(AssemblyName, syntaxTrees.ToList(), - _input.SourceProjectInfo.AnalyzerResult.LoadReferences(), - analyzerResult.GetCompilationOptions()); + _input.LoadReferences(), + _input.GetCompilationOptions()); // C# source generators must be executed before compilation - return RunSourceGenerators(analyzerResult, compilation); + return RunSourceGenerators(compilation); } private (CSharpRollbackProcessResult, EmitResult, int) TryCompilation( @@ -175,9 +172,9 @@ private CSharpCompilation GetCSharpCompilation(IEnumerable syntaxTre _logger.LogDebug("Trying compilation for the {retryCount} time.", ReadableNumber(retryCount)); var emitOptions = symbolStream == null ? null : new EmitOptions(false, DebugInformationFormat.PortablePdb, - _input.SourceProjectInfo.AnalyzerResult.GetSymbolFileName()); + _input.GetSymbolFileName()); EmitResult emitResult = null; - var resourceDescriptions = _input.SourceProjectInfo.AnalyzerResult.GetResources(_logger); + var resourceDescriptions = _input.GetResources(_logger); while (emitResult == null) { if (previousEmitResult != null) @@ -231,7 +228,7 @@ private CSharpCompilation ScanForCauseOfException(CSharpCompilation compilation) using var ms = new MemoryStream(); local.Emit( ms, - manifestResources: _input.SourceProjectInfo.AnalyzerResult.GetResources(_logger), + manifestResources: _input.GetResources(_logger), options: null); } catch (Exception e) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InitialisationProcess.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InitialisationProcess.cs index 438583d30b..d986fc5957 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InitialisationProcess.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InitialisationProcess.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO.Abstractions; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -20,35 +19,28 @@ public interface IInitialisationProcess /// /// stryker options /// an enumeration of , one for each found project (if any). - IReadOnlyCollection GetMutableProjectsInfo(IStrykerOptions options); + RelatedSourceProjectsInfo GetMutableProjectsInfo(IStrykerOptions options); - void BuildProjects(IStrykerOptions options, IEnumerable projects); + void BuildProjects(IStrykerOptions options, RelatedSourceProjectsInfo projects); Task> GetMutationTestInputsAsync(IStrykerOptions options, - IReadOnlyCollection projects, ITestRunner runner); + RelatedSourceProjectsInfo projects, ITestRunner runner); } -public class InitialisationProcess : IInitialisationProcess +public class InitialisationProcess( + IInputFileResolver inputFileResolver, + IInitialBuildProcess initialBuildProcess, + IInitialTestProcess initialTestProcess, + ILogger logger = null) + : IInitialisationProcess { - private readonly IInputFileResolver _inputFileResolver; - private readonly IInitialBuildProcess _initialBuildProcess; - private readonly IInitialTestProcess _initialTestProcess; - private readonly ILogger _logger; - - public InitialisationProcess( - IInputFileResolver inputFileResolver, - IInitialBuildProcess initialBuildProcess, - IInitialTestProcess initialTestProcess, - ILogger logger = null) - { - _inputFileResolver = inputFileResolver ?? throw new ArgumentNullException(nameof(inputFileResolver)); - _initialBuildProcess = initialBuildProcess ?? throw new ArgumentNullException(nameof(initialBuildProcess)); - _initialTestProcess = initialTestProcess ?? throw new ArgumentNullException(nameof(initialTestProcess)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } + private readonly IInputFileResolver _inputFileResolver = inputFileResolver ?? throw new ArgumentNullException(nameof(inputFileResolver)); + private readonly IInitialBuildProcess _initialBuildProcess = initialBuildProcess ?? throw new ArgumentNullException(nameof(initialBuildProcess)); + private readonly IInitialTestProcess _initialTestProcess = initialTestProcess ?? throw new ArgumentNullException(nameof(initialTestProcess)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); /// - public IReadOnlyCollection GetMutableProjectsInfo(IStrykerOptions options) + public RelatedSourceProjectsInfo GetMutableProjectsInfo(IStrykerOptions options) { _logger.LogInformation("Analysis starting."); try @@ -63,19 +55,19 @@ public IReadOnlyCollection GetMutableProjectsInfo(IStrykerOpt } /// - public void BuildProjects(IStrykerOptions options, IEnumerable projects) + public void BuildProjects(IStrykerOptions options, RelatedSourceProjectsInfo projects) { - var solutionInfo = projects.First().ProjectsTracker; + var solutionInfo = projects.Tracker; // pick configuration and platform from solution if available // we build the whole solution if we have a solution file path, even in project mode if (!string.IsNullOrEmpty(solutionInfo.SolutionFilePath)) { - solutionInfo.BuildSolution(_initialBuildProcess, projects.Select(p => p.AnalyzerResult)); + solutionInfo.BuildSolution(_initialBuildProcess, projects.SourceProjectInfos.Select(p => p.AnalyzerResult)); } else { // build every test projects - var testProjects = projects.SelectMany(p => p.TestProjectsInfo.AnalyzerResults).Distinct().ToList(); + var testProjects = projects.SourceProjectInfos.SelectMany(p => p.TestProjectsInfo.AnalyzerResults).Distinct().ToList(); for (var i = 0; i < testProjects.Count; i++) { _logger.LogInformation( @@ -94,20 +86,20 @@ public void BuildProjects(IStrykerOptions options, IEnumerable> GetMutationTestInputsAsync(IStrykerOptions options, - IReadOnlyCollection projects, + RelatedSourceProjectsInfo projects, ITestRunner runner) { - var getInputs = projects.Select(async info => new MutationTestInput { + var getInputs = projects.SourceProjectInfos.Select(async info => new MutationTestInput { SourceProjectInfo = info, TestRunner = runner, - InitialTestRun = await InitialTestAsync(options, info, runner, projects.Count == 1) + InitialTestRun = await InitialTestAsync(options, info, runner, projects.SourceProjectInfos.Count == 1) }); return await Task.WhenAll(getInputs); } diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index c1d5bfe957..8266514513 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -18,7 +18,7 @@ namespace Stryker.Core.Initialisation; public interface IInputFileResolver { - IReadOnlyCollection ResolveSourceProjectInfos(IStrykerOptions options); + RelatedSourceProjectsInfo ResolveSourceProjectInfos(IStrykerOptions options); IFileSystem FileSystem { get; } } @@ -49,7 +49,7 @@ public class InputFileResolver( /// Stryker options /// a collection of describing mutable project /// Thrown if the method fails during analysis. - public IReadOnlyCollection ResolveSourceProjectInfos(IStrykerOptions options) + public RelatedSourceProjectsInfo ResolveSourceProjectInfos(IStrykerOptions options) { var normalizedProjectUnderTestNameFilter = NormalizePath(options.SourceProjectName); @@ -69,24 +69,25 @@ public IReadOnlyCollection ResolveSourceProjectInfos(IStryker catch (IOException e) { _logger.LogCritical(e, "Failed to load solution file {SolutionFile}.", options.SolutionPath); - return []; + return new RelatedSourceProjectsInfo(null, []); } catch (UnauthorizedAccessException e) { _logger.LogCritical(e, "Failed to access solution file {SolutionFile}.", options.SolutionPath); - return []; + return new RelatedSourceProjectsInfo(null, []); } catch (AggregateException e) // Handles exceptions from .Result on Task { _logger.LogCritical(e, "Failed to load solution file {SolutionFile}.", options.SolutionPath); - return []; + return new RelatedSourceProjectsInfo(null, []); } } var solutionInfo = new ProjectsTracker(solution, options, _analyzerProvider, _nugetRestoreProcess, FileSystem, _logger) { TargetFramework = options.TargetFramework }; - return options.IsSolutionContext ? FindProjectsInSolutionMode(options, solutionInfo, normalizedProjectUnderTestNameFilter) + var sourceProjectInfos = options.IsSolutionContext ? FindProjectsInSolutionMode(options, solutionInfo, normalizedProjectUnderTestNameFilter) : FindProjectInTargetProjectMode(options, solutionInfo, normalizedProjectUnderTestNameFilter); + return new RelatedSourceProjectsInfo(solutionInfo, sourceProjectInfos); } private IReadOnlyCollection FindProjectsInSolutionMode(IStrykerOptions options, ProjectsTracker solutionInfo, @@ -245,7 +246,7 @@ private List AnalyzeAndIdentifyProjects(IStrykerOptions optio // keep only projects with one or more test projects // we must select projects according to framework settings if any var projectInfos = suitableCandidates.Where(p => p.Targets.Count > 0) - .Select(analyzerResult => analyzerResult.Targets[0].BuildSourceProjectInfo(options, solutionInfo, FileSystem)) + .Select(analyzerResult => analyzerResult.Targets[0].BuildSourceProjectInfo(options, FileSystem)) .ToList(); if (projectInfos.Count != 0) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTarget.cs b/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTarget.cs index bfa04789b3..f5909eaa50 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTarget.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTarget.cs @@ -35,13 +35,13 @@ internal class MutableProjectTarget(IAnalyzerResult target, ILogger logger) /// /// filesystem /// - public SourceProjectInfo BuildSourceProjectInfo(IStrykerOptions options, - ProjectsTracker projectsTracker, IFileSystem fileSystem ) + public SourceProjectInfo BuildSourceProjectInfo(IStrykerOptions options, IFileSystem fileSystem ) { - var targetProjectInfo = new SourceProjectInfo + var testProjectInfo = new TestProjectsInfo(fileSystem) { - AnalyzerResult = ProjectTarget + TestProjects = TestProjects.Select(testProjectAnalyzerResult => new TestProject(fileSystem, testProjectAnalyzerResult)).ToList() }; + var targetProjectInfo = new SourceProjectInfo(ProjectTarget , testProjectInfo); var language = targetProjectInfo.AnalyzerResult.GetLanguage(); @@ -54,12 +54,7 @@ public SourceProjectInfo BuildSourceProjectInfo(IStrykerOptions options, builder.InjectHelpers(inputFiles); targetProjectInfo.OnProjectBuilt = builder.PostBuildAction(); targetProjectInfo.ProjectContents = inputFiles; - targetProjectInfo.ProjectsTracker = projectsTracker; logger.LogInformation("Found project {ProjectFileName} to mutate.", ProjectTarget.ProjectFilePath); - targetProjectInfo.TestProjectsInfo = new TestProjectsInfo(fileSystem) - { - TestProjects = TestProjects.Select(testProjectAnalyzerResult => new TestProject(fileSystem, testProjectAnalyzerResult)).ToList() - }; return targetProjectInfo; } diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectOrchestrator.cs b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectOrchestrator.cs index 0fede5929a..ef0b41796d 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectOrchestrator.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectOrchestrator.cs @@ -24,39 +24,30 @@ public interface IProjectOrchestrator : IDisposable Task> MutateProjectsAsync(IStrykerOptions options, IReporter reporters, ITestRunner runner = null); } -public sealed class ProjectOrchestrator : IProjectOrchestrator +public sealed class ProjectOrchestrator( + IProjectMutator projectMutator, + IInitialisationProcess initializationProcess, + IInputFileResolver fileResolver, + IServiceProvider serviceProvider, + IMutationTestExecutor mutationTestExecutor, + ILogger logger) + : IProjectOrchestrator { - private IInitialisationProcess _initializationProcess; - private readonly ILogger _logger; - private readonly IProjectMutator _projectMutator; - private readonly IServiceProvider _serviceProvider; - private readonly IMutationTestExecutor _mutationTestExecutor; - private readonly IInputFileResolver _fileResolver; + private IInitialisationProcess _initializationProcess = initializationProcess ?? throw new ArgumentNullException(nameof(initializationProcess)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IProjectMutator _projectMutator = projectMutator ?? throw new ArgumentNullException(nameof(projectMutator)); + private readonly IServiceProvider _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + private readonly IMutationTestExecutor _mutationTestExecutor = mutationTestExecutor ?? throw new ArgumentNullException(nameof(mutationTestExecutor)); + private readonly IInputFileResolver _fileResolver = fileResolver ?? throw new ArgumentNullException(nameof(fileResolver)); private ITestRunner _runner; - public ProjectOrchestrator( - IProjectMutator projectMutator, - IInitialisationProcess initializationProcess, - IInputFileResolver fileResolver, - IServiceProvider serviceProvider, - IMutationTestExecutor mutationTestExecutor, - ILogger logger) - { - _projectMutator = projectMutator ?? throw new ArgumentNullException(nameof(projectMutator)); - _initializationProcess = initializationProcess ?? throw new ArgumentNullException(nameof(initializationProcess)); - _fileResolver = fileResolver ?? throw new ArgumentNullException(nameof(fileResolver)); - _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - _mutationTestExecutor = mutationTestExecutor ?? throw new ArgumentNullException(nameof(mutationTestExecutor)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - public async Task> MutateProjectsAsync(IStrykerOptions options, IReporter reporters, ITestRunner runner = null) { _initializationProcess ??= _serviceProvider.GetRequiredService(); var projectInfos = _initializationProcess.GetMutableProjectsInfo(options); - if (projectInfos.Count == 0) + if (projectInfos.SourceProjectInfos.Count == 0) { _logger.LogWarning("No project to mutate. Stryker will exit prematurely."); return []; @@ -67,7 +58,7 @@ public async Task> MutateProjectsAsync(IStryke // create a test runner based on the selected option _runner = runner ?? CreateTestRunner(options); _mutationTestExecutor.TestRunner = _runner; - InitializeDashboardProjectInformation(options, projectInfos.First()); + InitializeDashboardProjectInformation(options, projectInfos.SourceProjectInfos.First()); var inputs = await _initializationProcess.GetMutationTestInputsAsync(options, projectInfos, _runner); var mutationTestProcesses = new ConcurrentBag(); @@ -78,15 +69,13 @@ public async Task> MutateProjectsAsync(IStryke return mutationTestProcesses; } - private ITestRunner CreateTestRunner(IStrykerOptions options) - { - return options.TestRunner switch + private ITestRunner CreateTestRunner(IStrykerOptions options) => + options.TestRunner switch { Stryker.Abstractions.Options.TestRunner.VsTest => new VsTestRunnerPool(options, fileSystem: _fileResolver.FileSystem), Stryker.Abstractions.Options.TestRunner.MicrosoftTestPlatform => new MicrosoftTestPlatformRunnerPool(options), _ => throw new InputException($"Unknown test runner: {options.TestRunner}") }; - } private void InitializeDashboardProjectInformation(IStrykerOptions options, SourceProjectInfo projectInfo) { diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectsTracker.cs b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectsTracker.cs index 8e85993d8f..9fe813e139 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectsTracker.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectsTracker.cs @@ -28,10 +28,10 @@ public class ProjectsTracker public ProjectsTracker(SolutionFile file, IStrykerOptions options, IBuildalyzerProvider buildalyzerProvider, INugetRestoreProcess nugetRestoreProcess, IFileSystem fileSystem, ILogger logger) { - _options = options; - _buildalyzerProvider = buildalyzerProvider; - _logger = logger; - _nugetRestoreProcess = nugetRestoreProcess; + _options = options ?? throw new ArgumentNullException(nameof(options)); + _buildalyzerProvider = buildalyzerProvider ?? throw new ArgumentNullException(nameof(buildalyzerProvider)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _nugetRestoreProcess = nugetRestoreProcess ?? throw new ArgumentNullException(nameof(nugetRestoreProcess)); _fileSystem = fileSystem; Solution = file; SelectConfiguration(); @@ -41,11 +41,11 @@ public ProjectsTracker(SolutionFile file, IStrykerOptions options, IBuildalyzerP public string? SolutionFilePath => Solution?.FileName; - public string Configuration { get; private set; } + private string Configuration { get; set; } - public string Platform { get; private set; } + private string Platform { get; set; } - public string? TargetFramework { get; set; } + public string? TargetFramework { get; init; } public int ProjectCount => _selectedProjects.Count; diff --git a/src/Stryker.Core/Stryker.Core/MutationTest/CsharpMutationProcess.cs b/src/Stryker.Core/Stryker.Core/MutationTest/CsharpMutationProcess.cs index f918582741..5ed0f6a1c7 100644 --- a/src/Stryker.Core/Stryker.Core/MutationTest/CsharpMutationProcess.cs +++ b/src/Stryker.Core/Stryker.Core/MutationTest/CsharpMutationProcess.cs @@ -36,7 +36,7 @@ public void Mutate(MutationTestInput input, IStrykerOptions options) _options = options; var projectInfo = input.SourceProjectInfo.ProjectContents; var orchestrator = new CsharpMutantOrchestrator(new MutantPlacer(input.SourceProjectInfo.CodeInjector), options: _options); - var compilingProcess = new CsharpCompilingProcess(input, options: _options); + var compilingProcess = new CsharpCompilingProcess(input.SourceProjectInfo.AnalyzerResult, options: _options); var semanticModels = compilingProcess.GetSemanticModels(projectInfo.GetAllFiles().Cast().Select(x => x.SyntaxTree)); // Mutate source files diff --git a/src/Stryker.Core/Stryker.Core/MutationTest/MutationTestInput.cs b/src/Stryker.Core/Stryker.Core/MutationTest/MutationTestInput.cs index 96e71bbf05..e474f764df 100644 --- a/src/Stryker.Core/Stryker.Core/MutationTest/MutationTestInput.cs +++ b/src/Stryker.Core/Stryker.Core/MutationTest/MutationTestInput.cs @@ -1,4 +1,3 @@ -using Stryker.Abstractions.ProjectComponents; using Stryker.Abstractions.Testing; using Stryker.Core.Initialisation; using Stryker.Core.ProjectComponents.SourceProjects; diff --git a/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/RelatedSourceProjectsInfo.cs b/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/RelatedSourceProjectsInfo.cs new file mode 100644 index 0000000000..b917d8026d --- /dev/null +++ b/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/RelatedSourceProjectsInfo.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Stryker.Core.Initialisation; + +namespace Stryker.Core.ProjectComponents.SourceProjects; + +public class RelatedSourceProjectsInfo +{ + public IReadOnlyCollection SourceProjectInfos { get; } + public ProjectsTracker Tracker { get; } + + public RelatedSourceProjectsInfo(ProjectsTracker projectsTracker, IReadOnlyCollection sourceProjectInfos) + { + Tracker = projectsTracker; + SourceProjectInfos = sourceProjectInfos; + } +} diff --git a/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/SourceProjectInfo.cs b/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/SourceProjectInfo.cs index edfc6d7659..10fc1c1877 100644 --- a/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/SourceProjectInfo.cs +++ b/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/SourceProjectInfo.cs @@ -3,47 +3,38 @@ using Buildalyzer; using Stryker.Abstractions; using Stryker.Abstractions.ProjectComponents; -using Stryker.Core.Initialisation; using Stryker.Core.InjectedHelpers; -using Stryker.Utilities.Buildalyzer; namespace Stryker.Core.ProjectComponents.SourceProjects; -public class SourceProjectInfo : IProjectAndTests +public class SourceProjectInfo(IAnalyzerResult analyzerResult, ITestProjectsInfo testProjectsInfo) + : IProjectAndTests { private readonly List _warnings = []; public Action OnProjectBuilt { get; set; } - public ProjectsTracker ProjectsTracker { get; set; } + public IAnalyzerResult AnalyzerResult { get; init; } = analyzerResult; - public IAnalyzerResult AnalyzerResult { get; init; } + public ITestProjectsInfo? TestProjectsInfo { get; init; } = testProjectsInfo; /// /// The Folder/File structure found in the project under test. /// public IReadOnlyProjectComponent ProjectContents { get; set; } - public bool IsFullFramework => AnalyzerResult.TargetsFullFramework(); - public string HelperNamespace => CodeInjector.HelperNamespace; public CodeInjection CodeInjector { get; } = new(); - public ITestProjectsInfo TestProjectsInfo { get; set; } - public IReadOnlyCollection Warnings => _warnings; public IReadOnlyList GetTestAssemblies() => - TestProjectsInfo.GetTestAssemblies(); + TestProjectsInfo?.GetTestAssemblies() ?? []; - public string LogError(string error) - { - _warnings.Add(error); - return error; - } + public void LogError(string error) => _warnings.Add(error); - public void BackupOriginalAssembly(IAnalyzerResult analyzerResult) => TestProjectsInfo.BackupOriginalAssembly(analyzerResult); + public void BackupOriginalAssembly(IAnalyzerResult analyzerResult) => TestProjectsInfo?.BackupOriginalAssembly(analyzerResult); - public void RestoreOriginalAssembly(IAnalyzerResult analyzerResult) => TestProjectsInfo.RestoreOriginalAssembly(analyzerResult); + public void RestoreOriginalAssembly(IAnalyzerResult analyzerResult) => TestProjectsInfo?.RestoreOriginalAssembly(analyzerResult); } diff --git a/src/Stryker.TestRunner.VsTest.UnitTest/VsTestMockingHelper.cs b/src/Stryker.TestRunner.VsTest.UnitTest/VsTestMockingHelper.cs index 4c6097e83a..ab12a23e02 100644 --- a/src/Stryker.TestRunner.VsTest.UnitTest/VsTestMockingHelper.cs +++ b/src/Stryker.TestRunner.VsTest.UnitTest/VsTestMockingHelper.cs @@ -106,17 +106,17 @@ internal SourceProjectInfo BuildSourceProjectInfo(IEnumerable mutants = { var content = new CsharpFolderComposite(); content.Add(new CsharpFileLeaf { Mutants = mutants ?? new[] { Mutant, OtherMutant } }); - return new SourceProjectInfo - { - AnalyzerResult = TestHelper.SetupProjectAnalyzerResult( + return new SourceProjectInfo( + TestHelper.SetupProjectAnalyzerResult( properties: new Dictionary { { "TargetDir", Path.Combine(_filesystemRoot, "app", "bin", "Debug") }, { "TargetFileName", "AppToTest.dll" }, { "Language", "C#" } }, targetFramework: "netcoreapp2.1").Object, + _testProjectsInfo) + { ProjectContents = content, - TestProjectsInfo = _testProjectsInfo }; } diff --git a/src/Stryker.Utilities/Buildalyzer/Buildalyzer.cs b/src/Stryker.Utilities/Buildalyzer/Buildalyzer.cs index 7444013e04..9438783f87 100644 --- a/src/Stryker.Utilities/Buildalyzer/Buildalyzer.cs +++ b/src/Stryker.Utilities/Buildalyzer/Buildalyzer.cs @@ -8,11 +8,11 @@ namespace Stryker.Utilities.Buildalyzer; /// public interface IBuildalyzerProvider { - IAnalyzerManager Provide(AnalyzerManagerOptions options = null); + IAnalyzerManager Provide(AnalyzerManagerOptions options); } [ExcludeFromCodeCoverage] public class BuildalyzerProvider : IBuildalyzerProvider { - public IAnalyzerManager Provide(AnalyzerManagerOptions options = null) => new AnalyzerManager(options); + public IAnalyzerManager Provide(AnalyzerManagerOptions options) => new AnalyzerManager(options); } From 796709571f396c926b16d464a2088562cd019707 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Sat, 11 Apr 2026 11:34:42 +0200 Subject: [PATCH 30/41] fix: Copilot/Sonar --- .../Initialisation/InputFileResolver.cs | 14 ++++++++------ .../Initialisation/ProjectSimulatedBuildHandler.cs | 6 ++++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index 8266514513..f47efb5b78 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -40,7 +40,6 @@ public class InputFileResolver( private readonly ISolutionProvider _solutionProvider = solutionProvider ?? throw new ArgumentNullException(nameof(solutionProvider)); private readonly INugetRestoreProcess _nugetRestoreProcess = nugetRestoreProcess ?? throw new ArgumentNullException(nameof(nugetRestoreProcess)); - public IFileSystem FileSystem { get; } = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); /// @@ -69,27 +68,30 @@ public RelatedSourceProjectsInfo ResolveSourceProjectInfos(IStrykerOptions optio catch (IOException e) { _logger.LogCritical(e, "Failed to load solution file {SolutionFile}.", options.SolutionPath); - return new RelatedSourceProjectsInfo(null, []); + return new RelatedSourceProjectsInfo(BuildTracker(options, null), []); } catch (UnauthorizedAccessException e) { _logger.LogCritical(e, "Failed to access solution file {SolutionFile}.", options.SolutionPath); - return new RelatedSourceProjectsInfo(null, []); + return new RelatedSourceProjectsInfo(BuildTracker(options, null), []); } catch (AggregateException e) // Handles exceptions from .Result on Task { _logger.LogCritical(e, "Failed to load solution file {SolutionFile}.", options.SolutionPath); - return new RelatedSourceProjectsInfo(null, []); + return new RelatedSourceProjectsInfo(BuildTracker(options, null), []); } } - var solutionInfo = new ProjectsTracker(solution, options, _analyzerProvider, _nugetRestoreProcess, FileSystem, _logger) - { TargetFramework = options.TargetFramework }; + var solutionInfo = BuildTracker(options, solution); var sourceProjectInfos = options.IsSolutionContext ? FindProjectsInSolutionMode(options, solutionInfo, normalizedProjectUnderTestNameFilter) : FindProjectInTargetProjectMode(options, solutionInfo, normalizedProjectUnderTestNameFilter); return new RelatedSourceProjectsInfo(solutionInfo, sourceProjectInfos); } + private ProjectsTracker BuildTracker(IStrykerOptions options, SolutionFile solution) => + new(solution, options, _analyzerProvider, _nugetRestoreProcess, FileSystem, _logger) + { TargetFramework = options.TargetFramework }; + private IReadOnlyCollection FindProjectsInSolutionMode(IStrykerOptions options, ProjectsTracker solutionInfo, string normalizedProjectUnderTestNameFilter) { diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectSimulatedBuildHandler.cs b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectSimulatedBuildHandler.cs index f0cc402d12..bdbc890711 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectSimulatedBuildHandler.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectSimulatedBuildHandler.cs @@ -177,8 +177,10 @@ private void DumpTestAnalyzerResult(StringBuilder log, IAnalyzerResult analyzerR if (_logger.IsEnabled(LogLevel.Trace)) { // dumps all other properties as well, as they can be useful for diagnosing build issues - log.AppendLine($"Other properties: { - string.Join(", ", properties.Where(p => !ImportantProperties.Contains(p.Key)).Select(p => $"{p.Key}={p.Value.Replace(Environment.NewLine, "\\n")}"))}."); + var propertiesString = string.Join(", ", properties. + Where(p => !ImportantProperties.Contains(p.Key)). + Select(p => $"{p.Key}={p.Value.Replace(Environment.NewLine, "\\n")}")); + log.AppendLine($"Other properties: {propertiesString}."); } log.AppendLine(); From 73ad8638b832fc985d0f11ca5243eafc0cc399e9 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Sat, 11 Apr 2026 11:44:29 +0200 Subject: [PATCH 31/41] chore: Sonar --- .../Stryker.Core/Initialisation/InputFileResolver.cs | 5 ++--- .../SourceProjects/RelatedSourceProjectsInfo.cs | 4 ++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index f47efb5b78..b291f7c935 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -110,7 +110,7 @@ private IReadOnlyCollection FindProjectsInSolutionMode(IStryk var (findMutableAnalyzerResults, orphanedProjects) = ExtractMutableProjectTrees(mutableProjectsAnalyzerResults); // we keep only suitable candidates - return AnalyzeAndIdentifyProjects(options, solutionInfo, findMutableAnalyzerResults, orphanedProjects); + return AnalyzeAndIdentifyProjects(options, findMutableAnalyzerResults, orphanedProjects); } /// @@ -155,7 +155,7 @@ private List FindProjectInTargetProjectMode(IStrykerOptions o // we match test projects to mutable projects var (findMutableAnalyzerResults, orphans) = ExtractMutableProjectTrees(analyzeAllNeededProjects); - var result = AnalyzeAndIdentifyProjects(options, solution, findMutableAnalyzerResults, orphans); + var result = AnalyzeAndIdentifyProjects(options, findMutableAnalyzerResults, orphans); return SelectSingleProject(normalizedProjectUnderTestNameFilter, result, targetProjectMode, testProjectFileNames); } @@ -222,7 +222,6 @@ private enum ScanMode // analyze projects, do same for their upstream dependencies if activated, and identify which one(s) // to proceed with private List AnalyzeAndIdentifyProjects(IStrykerOptions options, - ProjectsTracker solutionInfo, List findMutableAnalyzerResults, List unusedTestProjects) { diff --git a/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/RelatedSourceProjectsInfo.cs b/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/RelatedSourceProjectsInfo.cs index b917d8026d..6057e7f34b 100644 --- a/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/RelatedSourceProjectsInfo.cs +++ b/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/RelatedSourceProjectsInfo.cs @@ -3,6 +3,10 @@ namespace Stryker.Core.ProjectComponents.SourceProjects; +/// +/// Simple class aggregating mutable projects and the associated overarching context: platform/framework... settings +/// and solution file when relevant +/// public class RelatedSourceProjectsInfo { public IReadOnlyCollection SourceProjectInfos { get; } From d0c9f03288273568a6e9460c2f19bb6a3c27c04d Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Sun, 12 Apr 2026 17:28:51 +0200 Subject: [PATCH 32/41] chore: move test compilation to RelatedSourceProjectsInfo class --- .../StrykerRunnerTests.cs | 1 - .../Initialisation/InitialisationProcess.cs | 30 +------------ .../Initialisation/InputFileResolver.cs | 8 ++-- .../Initialisation/ProjectsTracker.cs | 7 ++-- .../RelatedSourceProjectsInfo.cs | 42 ++++++++++++++++--- 5 files changed, 46 insertions(+), 42 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/StrykerRunnerTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/StrykerRunnerTests.cs index 49ea185e56..4714259304 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/StrykerRunnerTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/StrykerRunnerTests.cs @@ -35,7 +35,6 @@ public async Task Stryker_ShouldInvokeAllProcesses() var reporterFactoryMock = new Mock(MockBehavior.Strict); var reporterMock = new Mock(MockBehavior.Strict); var inputsMock = new Mock(MockBehavior.Strict); - var fileSystemMock = new MockFileSystem(); var folder = new CsharpFolderComposite(); folder.Add(new CsharpFileLeaf() diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InitialisationProcess.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InitialisationProcess.cs index d986fc5957..27edb07c81 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InitialisationProcess.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InitialisationProcess.cs @@ -57,34 +57,8 @@ public RelatedSourceProjectsInfo GetMutableProjectsInfo(IStrykerOptions options) /// public void BuildProjects(IStrykerOptions options, RelatedSourceProjectsInfo projects) { - var solutionInfo = projects.Tracker; - // pick configuration and platform from solution if available - // we build the whole solution if we have a solution file path, even in project mode - if (!string.IsNullOrEmpty(solutionInfo.SolutionFilePath)) - { - solutionInfo.BuildSolution(_initialBuildProcess, projects.SourceProjectInfos.Select(p => p.AnalyzerResult)); - } - else - { - // build every test projects - var testProjects = projects.SourceProjectInfos.SelectMany(p => p.TestProjectsInfo.AnalyzerResults).Distinct().ToList(); - for (var i = 0; i < testProjects.Count; i++) - { - _logger.LogInformation( - "Building test project {ProjectFilePath} ({CurrentTestProject}/{OfTotalTestProjects})", - testProjects[i].ProjectFilePath, i + 1, - testProjects.Count); - - _initialBuildProcess.InitialBuild( - testProjects[i].TargetsFullFramework(), - testProjects[i].ProjectFilePath, - options.SolutionPath, - testProjects[i].GetProperty("Configuration"), - testProjects[i].GetProperty("Platform"), - msbuildPath: options.MsBuildPath ?? testProjects[i].MsBuildPath()); - } - } - + // ensure test projects are built + projects.BuildTestProjects(_initialBuildProcess); // perform post build update (to capture some content files in C# project for example) foreach (var project in projects.SourceProjectInfos) { diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index b291f7c935..717a0c3e9f 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -68,24 +68,24 @@ public RelatedSourceProjectsInfo ResolveSourceProjectInfos(IStrykerOptions optio catch (IOException e) { _logger.LogCritical(e, "Failed to load solution file {SolutionFile}.", options.SolutionPath); - return new RelatedSourceProjectsInfo(BuildTracker(options, null), []); + return new RelatedSourceProjectsInfo(BuildTracker(options, null), [], _logger); } catch (UnauthorizedAccessException e) { _logger.LogCritical(e, "Failed to access solution file {SolutionFile}.", options.SolutionPath); - return new RelatedSourceProjectsInfo(BuildTracker(options, null), []); + return new RelatedSourceProjectsInfo(BuildTracker(options, null), [], _logger); } catch (AggregateException e) // Handles exceptions from .Result on Task { _logger.LogCritical(e, "Failed to load solution file {SolutionFile}.", options.SolutionPath); - return new RelatedSourceProjectsInfo(BuildTracker(options, null), []); + return new RelatedSourceProjectsInfo(BuildTracker(options, null), [], _logger); } } var solutionInfo = BuildTracker(options, solution); var sourceProjectInfos = options.IsSolutionContext ? FindProjectsInSolutionMode(options, solutionInfo, normalizedProjectUnderTestNameFilter) : FindProjectInTargetProjectMode(options, solutionInfo, normalizedProjectUnderTestNameFilter); - return new RelatedSourceProjectsInfo(solutionInfo, sourceProjectInfos); + return new RelatedSourceProjectsInfo(solutionInfo, sourceProjectInfos, _logger); } private ProjectsTracker BuildTracker(IStrykerOptions options, SolutionFile solution) => diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectsTracker.cs b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectsTracker.cs index 9fe813e139..08400b165b 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectsTracker.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectsTracker.cs @@ -99,7 +99,7 @@ internal void RestoreSolution(IAnalyzerResults results) if (Environment.OSVersion.Platform == PlatformID.Win32NT) { _logger.LogWarning("Project analysis failed. Stryker will retry after a solution level nuget restore"); - var optionsMsBuildPath = _options.MsBuildPath ?? results.First().MsBuildPath(); + var optionsMsBuildPath = MsBuildPath(results); if (string.IsNullOrEmpty(optionsMsBuildPath)) { _logger.LogWarning("Failed to find MSBuild path from analysis results. Nuget restore may fail if MSBuild is not in PATH."); @@ -111,6 +111,8 @@ internal void RestoreSolution(IAnalyzerResults results) } } + private string MsBuildPath(IEnumerable results) => string.IsNullOrEmpty(_options.MsBuildPath) ? results.First().MsBuildPath() : _options.MsBuildPath; + internal void BuildSolution(IInitialBuildProcess buildProcess, IEnumerable results) { lock (_nugetRestoreProcess) @@ -129,8 +131,7 @@ internal void BuildSolution(IInitialBuildProcess buildProcess, IEnumerable -public class RelatedSourceProjectsInfo +public class RelatedSourceProjectsInfo( + ProjectsTracker projectsTracker, + IReadOnlyCollection sourceProjectInfos, + ILogger logger = null) { - public IReadOnlyCollection SourceProjectInfos { get; } - public ProjectsTracker Tracker { get; } + public IReadOnlyCollection SourceProjectInfos { get; } = sourceProjectInfos; - public RelatedSourceProjectsInfo(ProjectsTracker projectsTracker, IReadOnlyCollection sourceProjectInfos) + public ProjectsTracker Tracker { get; } = projectsTracker; + + public bool BuildTestProjects(IInitialBuildProcess buildProcess) { - Tracker = projectsTracker; - SourceProjectInfos = sourceProjectInfos; + if (!string.IsNullOrEmpty(Tracker.SolutionFilePath)) + { + Tracker.BuildSolution(buildProcess, SourceProjectInfos.Select(p => p.AnalyzerResult)); + + return true; + } + var testProjects = SourceProjectInfos.SelectMany(p => p.TestProjectsInfo.AnalyzerResults) + .Distinct().ToList(); + for (var i = 0; i < testProjects.Count; i++) + { + logger?.LogInformation( + "Building test project {ProjectFilePath} ({CurrentTestProject}/{OfTotalTestProjects})", + testProjects[i].ProjectFilePath, i + 1, + testProjects.Count); + + buildProcess.InitialBuild( + false, + testProjects[i].ProjectFilePath, + null, + testProjects[i].GetProperty("Configuration"), + testProjects[i].GetProperty("Platform"), + msbuildPath: testProjects[i].MsBuildPath()); + } + + return true; } } From 8d75016c04fae2bdb10bfb1f7b1f0cffdae4da64 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Sun, 12 Apr 2026 21:14:55 +0200 Subject: [PATCH 33/41] Update src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Stryker.Core/Initialisation/InputFileResolver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index 717a0c3e9f..e9d519b74b 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -88,7 +88,7 @@ public RelatedSourceProjectsInfo ResolveSourceProjectInfos(IStrykerOptions optio return new RelatedSourceProjectsInfo(solutionInfo, sourceProjectInfos, _logger); } - private ProjectsTracker BuildTracker(IStrykerOptions options, SolutionFile solution) => + private ProjectsTracker BuildTracker(IStrykerOptions options, SolutionFile? solution) => new(solution, options, _analyzerProvider, _nugetRestoreProcess, FileSystem, _logger) { TargetFramework = options.TargetFramework }; From 731a3d37706562834960e74f6111f63cd0cb074d Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Sun, 12 Apr 2026 23:22:33 +0200 Subject: [PATCH 34/41] misc: improve log on analysis step --- .../Stryker.Core/Initialisation/InputFileResolver.cs | 9 ++++++--- .../SourceProjects/RelatedSourceProjectsInfo.cs | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index 717a0c3e9f..75e2e67785 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -349,6 +349,7 @@ private ConcurrentBag AnalyzeAllNeededProjects( private IAnalyzerResults AnalyzeSingleProject(ProjectSimulatedBuildHandler project, IStrykerOptions options) { var projectLogName = FileSystem.Path.GetRelativePath(options.WorkingDirectory, project.ProjectFileName); + var shouldConfirmSuccess = false; _logger.LogDebug("Analyzing {ProjectFilePath}", projectLogName); var buildResult = project.Analyze(); @@ -364,6 +365,7 @@ private IAnalyzerResults AnalyzeSingleProject(ProjectSimulatedBuildHandler proje return buildResult; } + shouldConfirmSuccess = true; _logger.LogWarning("Project {ProjectFilePath} simulated build failed. Trying again with a nuget restore.", projectLogName); // if this is a full framework project, we can retry after a nuget restore @@ -382,7 +384,7 @@ private IAnalyzerResults AnalyzeSingleProject(ProjectSimulatedBuildHandler proje } if (!buildResult.OverallSuccess) { - _logger.LogInformation("Project {ProjectFilePath} simulated build failed. The MsBuild log is: {Log}", projectLogName, project.LastBuildLog); + _logger.LogInformation("Project {ProjectFilePath} simulated build failed. Use '--diag' option to have the build log.", projectLogName); } if (options.DiagMode || _logger.IsEnabled(LogLevel.Debug)) @@ -392,8 +394,9 @@ private IAnalyzerResults AnalyzeSingleProject(ProjectSimulatedBuildHandler proje if (buildResultOverallSuccess) { - _logger.LogDebug("Analysis of project {ProjectFilePath} succeeded{Extra}", projectLogName, - buildResult.OverallSuccess ? "." : " but simulated build failed, Stryker may fail later."); + _logger.Log(shouldConfirmSuccess ? LogLevel.Warning : LogLevel.Debug, + "Analysis of project {ProjectFilePath} succeeded{Extra}", projectLogName, + buildResult.OverallSuccess ? "." : " but simulated build failed; Stryker may fail later."); return buildResult; } diff --git a/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/RelatedSourceProjectsInfo.cs b/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/RelatedSourceProjectsInfo.cs index 6385cc2bb7..fc779d2ac9 100644 --- a/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/RelatedSourceProjectsInfo.cs +++ b/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/RelatedSourceProjectsInfo.cs @@ -37,7 +37,7 @@ public bool BuildTestProjects(IInitialBuildProcess buildProcess) testProjects.Count); buildProcess.InitialBuild( - false, + testProjects[i].TargetsFullFramework(), testProjects[i].ProjectFilePath, null, testProjects[i].GetProperty("Configuration"), From c8c5b6084c810a0c0e5e7bd6a140908a330d0de8 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Mon, 13 Apr 2026 07:19:46 +0200 Subject: [PATCH 35/41] misc: fix Sonar --- .../Stryker.Core/Initialisation/InputFileResolver.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index 268c57a9ea..217748f596 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -384,12 +384,7 @@ private IAnalyzerResults AnalyzeSingleProject(ProjectSimulatedBuildHandler proje } if (!buildResult.OverallSuccess) { - _logger.LogInformation("Project {ProjectFilePath} simulated build failed. Use '--diag' option to have the build log.", projectLogName); - } - - if (options.DiagMode || _logger.IsEnabled(LogLevel.Debug)) - { - project.LogAnalyzerResult(); + _logger.LogWarning("Project {ProjectFilePath} simulated build failed. Use '--diag' option to have the build log.", projectLogName); } if (buildResultOverallSuccess) From 557620b8fcbd13b8bc555c9eb58b46252634a3d8 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Mon, 13 Apr 2026 11:06:41 +0200 Subject: [PATCH 36/41] fix: issues --- src/.run/MTP Solution.run.xml | 2 +- .../Stryker.Core/Initialisation/InputFileResolver.cs | 5 +++++ .../Stryker.Core/Initialisation/ProjectsTracker.cs | 7 ++++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/.run/MTP Solution.run.xml b/src/.run/MTP Solution.run.xml index 72ba004ad6..d1baa377a1 100644 --- a/src/.run/MTP Solution.run.xml +++ b/src/.run/MTP Solution.run.xml @@ -2,7 +2,7 @@ /// Stryker options - /// /// filesystem /// public SourceProjectInfo BuildSourceProjectInfo(IStrykerOptions options, IFileSystem fileSystem ) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectsTracker.cs b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectsTracker.cs index e5aa66fe4e..ce2d7cb72f 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectsTracker.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectsTracker.cs @@ -16,6 +16,7 @@ namespace Stryker.Core.Initialisation; public class ProjectsTracker { private List _selectedProjects = []; + private readonly object _lock = new object(); private bool _solutionRestored; private bool _solutionBuilt; @@ -89,7 +90,7 @@ private void SelectConfiguration() // the method is at solution level because it needs only be called once internal void RestoreSolution(IAnalyzerResults results) { - lock (_selectedProjects) + lock (_lock) { if (_solutionRestored || string.IsNullOrEmpty(SolutionFilePath)) { @@ -115,7 +116,7 @@ internal void RestoreSolution(IAnalyzerResults results) internal void BuildSolution(IInitialBuildProcess buildProcess, IEnumerable results) { - lock (_selectedProjects) + lock (_lock) { if (_solutionBuilt || string.IsNullOrEmpty(SolutionFilePath)) { diff --git a/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/SourceProjectInfo.cs b/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/SourceProjectInfo.cs index 10fc1c1877..31e976275a 100644 --- a/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/SourceProjectInfo.cs +++ b/src/Stryker.Core/Stryker.Core/ProjectComponents/SourceProjects/SourceProjectInfo.cs @@ -14,9 +14,9 @@ public class SourceProjectInfo(IAnalyzerResult analyzerResult, ITestProjectsInfo public Action OnProjectBuilt { get; set; } - public IAnalyzerResult AnalyzerResult { get; init; } = analyzerResult; + public IAnalyzerResult AnalyzerResult { get; } = analyzerResult; - public ITestProjectsInfo? TestProjectsInfo { get; init; } = testProjectsInfo; + public ITestProjectsInfo TestProjectsInfo { get; } = testProjectsInfo; /// /// The Folder/File structure found in the project under test. diff --git a/src/Stryker.Core/Stryker.Core/ProjectComponents/TestProjects/TestProjectsInfo.cs b/src/Stryker.Core/Stryker.Core/ProjectComponents/TestProjects/TestProjectsInfo.cs index 741bf71dc5..a4ff620a0f 100644 --- a/src/Stryker.Core/Stryker.Core/ProjectComponents/TestProjects/TestProjectsInfo.cs +++ b/src/Stryker.Core/Stryker.Core/ProjectComponents/TestProjects/TestProjectsInfo.cs @@ -13,7 +13,7 @@ namespace Stryker.Core.ProjectComponents.TestProjects; public class TestProjectsInfo : ITestProjectsInfo { private readonly IFileSystem _fileSystem; - private readonly ILogger _logger; + private readonly ILogger _logger; public IEnumerable TestProjects { get; set; } @@ -24,14 +24,14 @@ public class TestProjectsInfo : ITestProjectsInfo public IReadOnlyList GetTestAssemblies() => AnalyzerResults.Select(a => a.GetAssemblyPath()).ToList(); - public TestProjectsInfo(IFileSystem fileSystem, ILogger logger = null) + public TestProjectsInfo(IFileSystem fileSystem, ILogger logger = null) { _fileSystem = fileSystem ?? new FileSystem(); _logger = logger ?? ApplicationLogging.LoggerFactory.CreateLogger(); TestProjects = []; } - public static TestProjectsInfo operator +(TestProjectsInfo a, TestProjectsInfo b) => + public static TestProjectsInfo operator +(TestProjectsInfo a, ITestProjectsInfo b) => new(a._fileSystem, a._logger) { TestProjects = a.TestProjects.Union(b.TestProjects) diff --git a/src/Stryker.Core/Stryker.Core/StrykerRunner.cs b/src/Stryker.Core/Stryker.Core/StrykerRunner.cs index 73a3a6e4ae..88699bf327 100644 --- a/src/Stryker.Core/Stryker.Core/StrykerRunner.cs +++ b/src/Stryker.Core/Stryker.Core/StrykerRunner.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO.Abstractions; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -61,7 +62,8 @@ public async Task RunMutationTestAsync(IStrykerInputs inputs) _mutationTestProcesses = (await _projectOrchestrator.MutateProjectsAsync(options, reporters)).ToList(); var rootComponent = AddRootFolderIfMultiProject(_mutationTestProcesses.Select(x => x.Input.SourceProjectInfo.ProjectContents).ToList(), options); - var combinedTestProjectsInfo = _mutationTestProcesses.Select(mtp => mtp.Input.SourceProjectInfo.TestProjectsInfo).Aggregate((a, b) => (TestProjectsInfo)a + (TestProjectsInfo)b); + var combinedTestProjectsInfo = _mutationTestProcesses.Select(mtp => mtp.Input.SourceProjectInfo.TestProjectsInfo). + Aggregate(new TestProjectsInfo(new FileSystem(), _logger), (a, b) => a + b); _logger.LogInformation("{MutantsCount} mutants created", rootComponent.Mutants.Count()); From d92b31dd5538592682656e37426c2779680c670a Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Mon, 13 Apr 2026 13:14:14 +0200 Subject: [PATCH 38/41] test: fix failing test --- .../ProjectComponents/TestProjects/TestProjectsInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core/ProjectComponents/TestProjects/TestProjectsInfo.cs b/src/Stryker.Core/Stryker.Core/ProjectComponents/TestProjects/TestProjectsInfo.cs index a4ff620a0f..186838a641 100644 --- a/src/Stryker.Core/Stryker.Core/ProjectComponents/TestProjects/TestProjectsInfo.cs +++ b/src/Stryker.Core/Stryker.Core/ProjectComponents/TestProjects/TestProjectsInfo.cs @@ -31,8 +31,8 @@ public TestProjectsInfo(IFileSystem fileSystem, ILogger logger = null) TestProjects = []; } - public static TestProjectsInfo operator +(TestProjectsInfo a, ITestProjectsInfo b) => - new(a._fileSystem, a._logger) + public static TestProjectsInfo operator +(TestProjectsInfo a, ITestProjectsInfo b) => b == null ? a : + new TestProjectsInfo(a._fileSystem, a._logger) { TestProjects = a.TestProjects.Union(b.TestProjects) }; From ef955e4a69e134d6a8192d443bafdf2f8d3bac5a Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Mon, 13 Apr 2026 13:58:45 +0200 Subject: [PATCH 39/41] misc: pick netframwork first on windows os --- src/Stryker.Core/Stryker.Core.UnitTest/StrykerRunnerTests.cs | 1 - .../Stryker.Core/Initialisation/MutableProjectTree.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/StrykerRunnerTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/StrykerRunnerTests.cs index 4714259304..ced68cf747 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/StrykerRunnerTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/StrykerRunnerTests.cs @@ -102,7 +102,6 @@ public async Task ShouldStop_WhenAllMutationsWereIgnored() var reporterFactoryMock = new Mock(MockBehavior.Strict); var reporterMock = new Mock(MockBehavior.Strict); var inputsMock = new Mock(MockBehavior.Strict); - var fileSystemMock = new MockFileSystem(); var folder = new CsharpFolderComposite(); folder.Add(new CsharpFileLeaf diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTree.cs b/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTree.cs index ee9f84a126..707f5281ab 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTree.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTree.cs @@ -57,7 +57,7 @@ public void KeepOnlyOneTarget(string optionsTargetFramework) logger.LogWarning("Failed to find a valid {Framework} target for project {Project}. ", optionsTargetFramework, project.ProjectFileName); } - targetToKeep = Targets.Where(t => t.IsValidTarget).FirstOrDefault( t => OperatingSystem.IsWindows() || !t.ProjectTarget.TargetsFullFramework()); + targetToKeep = Targets.Where(t => t.IsValidTarget).FirstOrDefault( t => OperatingSystem.IsWindows() == t.ProjectTarget.TargetsFullFramework()); Targets.Clear(); if (targetToKeep == null) { From 2f6bd96613a06bfff4d501dcb1887dacf7b0484d Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Mon, 13 Apr 2026 15:50:39 +0200 Subject: [PATCH 40/41] fix: change target framework seleciton logic --- .../Stryker.Core/Initialisation/MutableProjectTree.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTree.cs b/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTree.cs index 707f5281ab..081f40e6b1 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTree.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/MutableProjectTree.cs @@ -57,7 +57,11 @@ public void KeepOnlyOneTarget(string optionsTargetFramework) logger.LogWarning("Failed to find a valid {Framework} target for project {Project}. ", optionsTargetFramework, project.ProjectFileName); } - targetToKeep = Targets.Where(t => t.IsValidTarget).FirstOrDefault( t => OperatingSystem.IsWindows() == t.ProjectTarget.TargetsFullFramework()); + var mutableProjectTargets = Targets.Where(t => t.IsValidTarget); + // on non windows platform, keep first non netframework target + // on Windows, keep first netframework target if any, otherwise keep first valid target + targetToKeep = mutableProjectTargets.FirstOrDefault( t => OperatingSystem.IsWindows() == t.ProjectTarget.TargetsFullFramework()) ?? + mutableProjectTargets.FirstOrDefault(); Targets.Clear(); if (targetToKeep == null) { From f1b82fc703e9e8f589d1907427a6a6f06cb9c7e3 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Mon, 13 Apr 2026 23:18:48 +0200 Subject: [PATCH 41/41] chore: review nullable --- .../InitialisationProcessTests.cs | 65 +++++++++++++------ .../Initialisation/ProjectsTracker.cs | 4 +- 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InitialisationProcessTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InitialisationProcessTests.cs index 476ef2857e..699d3644d7 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InitialisationProcessTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InitialisationProcessTests.cs @@ -18,6 +18,7 @@ using Stryker.Core.ProjectComponents.Csharp; using Stryker.Core.ProjectComponents.SourceProjects; using Stryker.Core.ProjectComponents.TestProjects; +using Stryker.Solutions; using Stryker.TestRunner.Results; using Stryker.TestRunner.Tests; using Stryker.TestRunner.VsTest; @@ -77,19 +78,23 @@ public async Task InitialisationProcess_ShouldThrowOnFailedInitialTestRun() var loggerMock = new Mock>(); var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); + var mockFileSystem = new MockFileSystem(); + var options = new StrykerOptions { ProjectName = "TheProjectName", - ProjectVersion = "TheProjectVersion" + ProjectVersion = "TheProjectVersion", + WorkingDirectory = "./" }; - var projectTracker = new ProjectsTracker(null, options, + + var projectTracker = new ProjectsTracker(SolutionFile.BuildFromProjectList("solution.sln", []), options, new Mock(MockBehavior.Strict).Object, new Mock().Object, - null, loggerMock.Object); + mockFileSystem, loggerMock.Object); inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())). Returns(new RelatedSourceProjectsInfo(projectTracker, [new SourceProjectInfo( TestHelper.SetupProjectAnalyzerResult(references: []).Object, - new TestProjectsInfo(new MockFileSystem())) + new TestProjectsInfo(mockFileSystem)) ])); inputFileResolverMock.SetupGet(x => x.FileSystem).Returns(new FileSystem()); @@ -119,15 +124,19 @@ public async Task InitialisationProcess_ShouldThrowIfHalfTestsAreFailing() folder.Add(new CsharpFileLeaf()); var loggerMock = new Mock>(); var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); + var mockFileSystem = new MockFileSystem(); + var options = new StrykerOptions { ProjectName = "TheProjectName", - ProjectVersion = "TheProjectVersion" + ProjectVersion = "TheProjectVersion", + WorkingDirectory = "./" }; - var projectTracker = new ProjectsTracker(null, options, + + var projectTracker = new ProjectsTracker(SolutionFile.BuildFromProjectList("solution.sln", []), options, new Mock(MockBehavior.Strict).Object, new Mock().Object, - null, loggerMock.Object); + mockFileSystem, loggerMock.Object); inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())).Returns(new RelatedSourceProjectsInfo(projectTracker, [new SourceProjectInfo(TestHelper.SetupProjectAnalyzerResult(references: []).Object, new TestProjectsInfo(new MockFileSystem()))])); @@ -170,16 +179,21 @@ public async Task InitialisationProcess_ShouldThrowOnTestTestIfAskedFor(bool bre folder.Add(new CsharpFileLeaf()); var loggerMock = new Mock>(); var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); + + var mockFileSystem = new MockFileSystem(); + var options = new StrykerOptions { ProjectName = "TheProjectName", ProjectVersion = "TheProjectVersion", - BreakOnInitialTestFailure = breakOnInitialTestFailure + BreakOnInitialTestFailure = breakOnInitialTestFailure, + WorkingDirectory = "./" }; - var projectTracker = new ProjectsTracker(null, options, + + var projectTracker = new ProjectsTracker(SolutionFile.BuildFromProjectList("solution.sln", []), options, new Mock(MockBehavior.Strict).Object, new Mock().Object, - null, loggerMock.Object); + mockFileSystem, loggerMock.Object); inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())).Returns(new RelatedSourceProjectsInfo(projectTracker, [new SourceProjectInfo(TestHelper.SetupProjectAnalyzerResult(references: []).Object, new TestProjectsInfo(new MockFileSystem()))])); @@ -226,16 +240,19 @@ public async Task InitialisationProcess_ShouldRunTestSession() var loggerMock = new Mock>(); var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); + var mockFileSystem = new MockFileSystem(); + var options = new StrykerOptions { ProjectName = "TheProjectName", - ProjectVersion = "TheProjectVersion" + ProjectVersion = "TheProjectVersion", + WorkingDirectory = "./" }; - var projectTracker = new ProjectsTracker(null, options, + + var projectTracker = new ProjectsTracker(SolutionFile.BuildFromProjectList("solution.sln", []), options, new Mock(MockBehavior.Strict).Object, new Mock().Object, - null, loggerMock.Object); - + mockFileSystem, loggerMock.Object); inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())). Returns(new RelatedSourceProjectsInfo(projectTracker, [new SourceProjectInfo(TestHelper.SetupProjectAnalyzerResult(references: []).Object, @@ -287,15 +304,19 @@ public async Task InitialisationProcess_ShouldThrowOnWhenNoTestDetected(string l var loggerMock = new Mock>(); var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); + var mockFileSystem = new MockFileSystem(); + var options = new StrykerOptions { ProjectName = "TheProjectName", - ProjectVersion = "TheProjectVersion" + ProjectVersion = "TheProjectVersion", + WorkingDirectory = "./" }; - var projectTracker = new ProjectsTracker(null, options, + + var projectTracker = new ProjectsTracker(SolutionFile.BuildFromProjectList("solution.sln", []), options, new Mock(MockBehavior.Strict).Object, new Mock().Object, - null, loggerMock.Object); + mockFileSystem, loggerMock.Object); inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())).Returns(new RelatedSourceProjectsInfo(projectTracker, [ new SourceProjectInfo( @@ -344,15 +365,19 @@ public void InitialisationProcess_ShouldThrowOnWhenNoTestDetectedAndCorrectDepen var loggerMock = new Mock>(); var target = new InitialisationProcess(inputFileResolverMock.Object, initialBuildProcessMock.Object, initialTestProcessMock.Object, loggerMock.Object); + var mockFileSystem = new MockFileSystem(); + var options = new StrykerOptions { ProjectName = "TheProjectName", - ProjectVersion = "TheProjectVersion" + ProjectVersion = "TheProjectVersion", + WorkingDirectory = "./" }; - var projectTracker = new ProjectsTracker(null, options, + + var projectTracker = new ProjectsTracker(SolutionFile.BuildFromProjectList("solution.sln", []), options, new Mock(MockBehavior.Strict).Object, new Mock().Object, - null, loggerMock.Object); + mockFileSystem, loggerMock.Object); inputFileResolverMock.Setup(x => x.ResolveSourceProjectInfos(It.IsAny())). Returns(new RelatedSourceProjectsInfo(projectTracker, [new SourceProjectInfo(TestHelper.SetupProjectAnalyzerResult( diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectsTracker.cs b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectsTracker.cs index ce2d7cb72f..8089101fc9 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/ProjectsTracker.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/ProjectsTracker.cs @@ -16,7 +16,7 @@ namespace Stryker.Core.Initialisation; public class ProjectsTracker { private List _selectedProjects = []; - private readonly object _lock = new object(); + private readonly object _lock = new(); private bool _solutionRestored; private bool _solutionBuilt; @@ -26,7 +26,7 @@ public class ProjectsTracker private readonly IBuildalyzerProvider _buildalyzerProvider; private readonly ILogger _logger; - public ProjectsTracker(SolutionFile file, IStrykerOptions options, IBuildalyzerProvider buildalyzerProvider, + public ProjectsTracker(SolutionFile? file, IStrykerOptions options, IBuildalyzerProvider buildalyzerProvider, INugetRestoreProcess nugetRestoreProcess, IFileSystem fileSystem, ILogger logger) { _options = options ?? throw new ArgumentNullException(nameof(options));