From 058f788f0a60d1f431096c41566c4fb2c819f295 Mon Sep 17 00:00:00 2001 From: Cyrille DUPUYDAUBY Date: Wed, 5 Feb 2025 12:40:48 +0100 Subject: [PATCH] Misc: improve project analysis --- .../Initialisation/BuildAnalyzerTestsBase.cs | 22 ++++--- .../Initialisation/InputFileResolverTests.cs | 28 ++++++++- .../Buildalyzer/IAnalyzerResultExtensions.cs | 4 +- .../Initialisation/InputFileResolver.cs | 60 ++++++++++++++----- 4 files changed, 90 insertions(+), 24 deletions(-) diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/BuildAnalyzerTestsBase.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/BuildAnalyzerTestsBase.cs index 1444a9e82..024e05f5d 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/BuildAnalyzerTestsBase.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/BuildAnalyzerTestsBase.cs @@ -9,7 +9,6 @@ using Moq; using Stryker.Core.Initialisation.Buildalyzer; using Stryker.Core.Testing; -using Stryker.Core.UnitTest; namespace Stryker.Core.UnitTest.Initialisation; @@ -79,12 +78,12 @@ public static Dictionary GetSourceProjectDefaultProperties() /// /// a mock project analyzer /// the test project references the production code project and contains no source file - protected Mock TestProjectAnalyzerMock(string testCsprojPathName, string csProj, IEnumerable frameworks = null, bool success = true) + protected Mock TestProjectAnalyzerMock(string testCsprojPathName, string csProj, IEnumerable frameworks = null, bool success = true, bool dontGenerateProjectReference= false) { frameworks??=[DefaultFramework]; var properties = new Dictionary{ { "IsTestProject", "True" }, { "Language", "C#" } }; var projectReferences = string.IsNullOrEmpty(csProj) ? [] : GetProjectResult(csProj, frameworks.First()).ProjectReferences.Append(csProj).ToList(); - return BuildProjectAnalyzerMock(testCsprojPathName, [], properties, projectReferences, frameworks, () => success); + return BuildProjectAnalyzerMock(testCsprojPathName, [], properties, projectReferences, frameworks, () => success, [], dontGenerateProjectReference); } private IAnalyzerResult GetProjectResult(string projectFile, string expectedFramework, bool returnDefaultIfNotFound = true) @@ -199,7 +198,8 @@ internal Mock BuildProjectAnalyzerMock(string csprojPathName, IEnumerable projectReferences= null, IEnumerable frameworks = null, Func success = null, - IEnumerable rawReferences = null) + IEnumerable rawReferences = null, + bool dontResolveProjectReference = false) { var projectFileMock = new Mock(MockBehavior.Strict); success ??= () => true; @@ -226,9 +226,17 @@ internal Mock BuildProjectAnalyzerMock(string csprojPathName, FileSystem.AddFile(FileSystem.Path.Combine(projectUnderTestBin, projectBin), new MockFileData("")); var projectAnalyzerResultMock = new Mock(MockBehavior.Strict); projectAnalyzerResultMock.Setup(x => x.ProjectReferences).Returns(projectReferences); - projectAnalyzerResultMock.Setup(x => x.References).Returns(projectReferences. - Where ( p => p !=null && _projectCache.ContainsKey(p)). - Select( iar => GetProjectResult(iar, framework).GetAssemblyPath()).Union(rawReferences).ToArray()); + if (dontResolveProjectReference) + { + projectAnalyzerResultMock.Setup(x => x.References).Returns([]); + } + else + { + projectAnalyzerResultMock.Setup(x => x.References).Returns(projectReferences. + Where ( p => p !=null && _projectCache.ContainsKey(p)). + Select( iar => GetProjectResult(iar, framework).GetAssemblyPath()).Union(rawReferences).ToArray()); + } + projectAnalyzerResultMock.Setup(x => x.SourceFiles).Returns(sourceFiles); projectAnalyzerResultMock.Setup(x => x.PreprocessorSymbols).Returns(["NET"]); specificProperties.Add("TargetRefPath", projectBin); diff --git a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InputFileResolverTests.cs b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InputFileResolverTests.cs index 44f948355..bd2162a94 100644 --- a/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InputFileResolverTests.cs +++ b/src/Stryker.Core/Stryker.Core.UnitTest/Initialisation/InputFileResolverTests.cs @@ -105,7 +105,6 @@ public void ProjectAnalyzerShouldDecodeFramework(string tfm, string framework, s } [TestMethod] - [DataRow("")] [DataRow("nxt")] [DataRow("mono4.6")] [DataRow("netcoreapp1.2.3.4.5")] @@ -1274,6 +1273,33 @@ public void ShouldThrowWhenTheNameMatchesNone() } + [TestMethod] + public void ShouldFallbackToProjectReferenceIfDependencyNotFound() + { + var fileSystem = new MockFileSystem(new Dictionary + { + { _sourceProjectPath, new MockFileData(_defaultTestProjectFileContents)}, + { Path.Combine(_sourcePath, "source.cs"), new MockFileData(_sourceFile)}, + { _testProjectPath, new MockFileData(_defaultTestProjectFileContents)}, + }); + + var sourceProjectManagerMock = SourceProjectAnalyzerMock(_sourceProjectPath, fileSystem.AllFiles.Where(s => s.EndsWith(".cs")).ToArray()); + var testProjectManagerMock = TestProjectAnalyzerMock(_testProjectPath, _sourceProjectPath, ["netcoreapp2.1"], dontGenerateProjectReference: true); + + var analyzerResults = new Dictionary + { + { "MyProject", sourceProjectManagerMock.Object }, + { "MyProject.UnitTests", testProjectManagerMock.Object } + }; + BuildBuildAnalyzerMock(analyzerResults); + + var target = new InputFileResolver(fileSystem, BuildalyzerProviderMock.Object, _nugetMock.Object); + + var result = target.ResolveSourceProjectInfos(_options).First(); + + result.AnalyzerResult.ProjectFilePath.ShouldBe(_sourceProjectPath); + } + [TestMethod] [DataRow("ExampleProject/ExampleProject.csproj")] [DataRow("ExampleProject\\ExampleProject.csproj")] diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/Buildalyzer/IAnalyzerResultExtensions.cs b/src/Stryker.Core/Stryker.Core/Initialisation/Buildalyzer/IAnalyzerResultExtensions.cs index 67df9cc2d..3b81c57bb 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/Buildalyzer/IAnalyzerResultExtensions.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/Buildalyzer/IAnalyzerResultExtensions.cs @@ -146,6 +146,8 @@ public Assembly LoadFromPath(string fullPath) internal static NuGetFramework GetNuGetFramework(this IAnalyzerResult analyzerResult) { + if (string.IsNullOrEmpty(analyzerResult.TargetFramework)) + return null; var framework = NuGetFramework.Parse(analyzerResult.TargetFramework); if (framework != NuGetFramework.UnsupportedFramework) { @@ -160,7 +162,7 @@ internal static NuGetFramework GetNuGetFramework(this IAnalyzerResult analyzerRe throw new InputException(message); } - internal static bool TargetsFullFramework(this IAnalyzerResult analyzerResult) => analyzerResult.GetNuGetFramework().IsDesktop(); + internal static bool TargetsFullFramework(this IAnalyzerResult analyzerResult) => analyzerResult.GetNuGetFramework()?.IsDesktop() == true; public static Language GetLanguage(this IAnalyzerResult analyzerResult) => analyzerResult.GetPropertyOrDefault("Language") switch diff --git a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs index 15b910366..6336943c8 100644 --- a/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs +++ b/src/Stryker.Core/Stryker.Core/Initialisation/InputFileResolver.cs @@ -400,7 +400,7 @@ IAnalyzerResult PickFrameworkVersion() // checks if an analyzer result is valid private static bool IsValid(IAnalyzerResult br) => br.Succeeded || (br.SourceFiles.Length > 0 && br.References.Length > 0); - private static Dictionary> FindMutableAnalyzerResults(ConcurrentBag<(IEnumerable result, bool isTest)> mutableProjectsAnalyzerResults) + private Dictionary> FindMutableAnalyzerResults(ConcurrentBag<(IEnumerable result, bool isTest)> mutableProjectsAnalyzerResults) { var mutableToTestMap = new Dictionary>(); var analyzerTestProjects = mutableProjectsAnalyzerResults.Where(p => p.isTest).SelectMany(p => p.result).Where(p => p.BuildsAnAssembly()); @@ -408,27 +408,57 @@ private static Dictionary> FindMutableAna // for each test project foreach (var testProject in analyzerTestProjects) { - // we identify which project are referenced by it - foreach (var mutableProject in mutableProjects) + if (!ScanAssemblyReferences(mutableToTestMap, mutableProjects, testProject)) { - if (Array.TrueForAll(testProject.References, r => - !r.Equals(mutableProject.GetAssemblyPath(), StringComparison.OrdinalIgnoreCase) && - !r.Equals(mutableProject.GetReferenceAssemblyPath(), StringComparison.OrdinalIgnoreCase))) - { - continue; - } - if (!mutableToTestMap.TryGetValue(mutableProject, out var dependencies)) - { - dependencies = []; - mutableToTestMap[mutableProject] = dependencies; - } - dependencies.Add(testProject); + _logger.LogInformation("Could not find an assembly reference to a mutable assembly for project {0}. Will look into project references.", testProject.ProjectFilePath); + // we try to find a project reference + ScanProjectReferences(mutableToTestMap, mutableProjects, testProject); } } return mutableToTestMap; } + private static void ScanProjectReferences(Dictionary> mutableToTestMap, IAnalyzerResult[] mutableProjects, IAnalyzerResult testProject) + { + var mutableProject = mutableProjects.FirstOrDefault(p => testProject.ProjectReferences.Contains(p.ProjectFilePath)); + if (mutableProject == null) + { + return; + } + if (!mutableToTestMap.TryGetValue(mutableProject, out var dependencies)) + { + mutableToTestMap[mutableProject] = dependencies = []; + } + + dependencies.Add(testProject); + } + + private static bool ScanAssemblyReferences(Dictionary> mutableToTestMap, IAnalyzerResult[] mutableProjects, IAnalyzerResult testProject) + { + var foundOneProject = false; + // we identify which project are referenced by it + foreach (var mutableProject in mutableProjects) + { + 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)) + { + mutableToTestMap[mutableProject] = dependencies = []; + } + dependencies.Add(testProject); + foundOneProject = true; + } + + return foundOneProject; + } + /// /// Builds a instance describing a project its associated test project(s) ///