From 4ac4ad05d3ed7d4bed20cd6c705b526c78656fd1 Mon Sep 17 00:00:00 2001 From: David Federman Date: Tue, 16 Sep 2025 10:30:32 -0700 Subject: [PATCH] Add more granular predictions for Fakes --- src/BuildPrediction/FakesPredictor.cs | 44 ++++++++ .../Predictors/FakesOutputPathPredictor.cs | 27 ----- ...opyToOutputDirectoryItemsGraphPredictor.cs | 23 ++++ src/BuildPrediction/ProjectPredictors.cs | 4 +- .../FakesOutputPathPredictorTests.cs | 96 ---------------- .../Predictors/FakesPredictorTests.cs | 103 ++++++++++++++++++ ...OutputDirectoryItemsGraphPredictorTests.cs | 45 ++++++++ 7 files changed, 217 insertions(+), 125 deletions(-) create mode 100644 src/BuildPrediction/FakesPredictor.cs delete mode 100644 src/BuildPrediction/Predictors/FakesOutputPathPredictor.cs delete mode 100644 src/BuildPredictionTests/Predictors/FakesOutputPathPredictorTests.cs create mode 100644 src/BuildPredictionTests/Predictors/FakesPredictorTests.cs diff --git a/src/BuildPrediction/FakesPredictor.cs b/src/BuildPrediction/FakesPredictor.cs new file mode 100644 index 0000000..cfe8015 --- /dev/null +++ b/src/BuildPrediction/FakesPredictor.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using Microsoft.Build.Execution; + +namespace Microsoft.Build.Prediction.Predictors +{ + /// + /// Predicts inputs and outputs related to Fakes. + /// + public sealed class FakesPredictor : IProjectPredictor + { + internal const string FakesImportedPropertyName = "FakesImported"; + + internal const string FakesUseV2GenerationPropertyName = "FakesUseV2Generation"; + + internal const string FakesOutputPathPropertyName = "FakesOutputPath"; + + internal const string FakesItemName = "Fakes"; + + /// + public void PredictInputsAndOutputs(ProjectInstance projectInstance, ProjectPredictionReporter predictionReporter) + { + if (!projectInstance.GetPropertyValue(FakesImportedPropertyName).Equals("true", StringComparison.OrdinalIgnoreCase)) + { + return; + } + + string fakesOutputPath = projectInstance.GetPropertyValue(FakesOutputPathPropertyName); + foreach (ProjectItemInstance item in projectInstance.GetItems(FakesItemName)) + { + predictionReporter.ReportInputFile(item.EvaluatedInclude); + + if (!string.IsNullOrWhiteSpace(fakesOutputPath)) + { + string fakesAssembly = Path.Combine(fakesOutputPath, $"{Path.GetFileNameWithoutExtension(item.EvaluatedInclude)}.Fakes.dll"); + predictionReporter.ReportOutputFile(fakesAssembly); + } + } + } + } +} \ No newline at end of file diff --git a/src/BuildPrediction/Predictors/FakesOutputPathPredictor.cs b/src/BuildPrediction/Predictors/FakesOutputPathPredictor.cs deleted file mode 100644 index a5638ef..0000000 --- a/src/BuildPrediction/Predictors/FakesOutputPathPredictor.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Microsoft.Build.Execution; - -namespace Microsoft.Build.Prediction.Predictors -{ - /// - /// Predicts the output directory for Microsoft Fakes assemblies based on the FakesOutputPath property. - /// - public sealed class FakesOutputPathPredictor : IProjectPredictor - { - internal const string FakesOutputPathPropertyName = "FakesOutputPath"; - - /// - public void PredictInputsAndOutputs( - ProjectInstance projectInstance, - ProjectPredictionReporter predictionReporter) - { - string fakesOutputPath = projectInstance.GetPropertyValue(FakesOutputPathPropertyName); - if (!string.IsNullOrWhiteSpace(fakesOutputPath)) - { - predictionReporter.ReportOutputDirectory(fakesOutputPath); - } - } - } -} \ No newline at end of file diff --git a/src/BuildPrediction/Predictors/GetCopyToOutputDirectoryItemsGraphPredictor.cs b/src/BuildPrediction/Predictors/GetCopyToOutputDirectoryItemsGraphPredictor.cs index a2a7b27..ada810c 100644 --- a/src/BuildPrediction/Predictors/GetCopyToOutputDirectoryItemsGraphPredictor.cs +++ b/src/BuildPrediction/Predictors/GetCopyToOutputDirectoryItemsGraphPredictor.cs @@ -96,6 +96,29 @@ private static void PredictInputsAndOutputs( } } } + + // FakesV2 projects add Fakes assemblies as content which are transitively copied to referencing projects. See CopyFakesAssembliesToOutputDir target. + if (dependency.ProjectInstance.GetPropertyValue(FakesPredictor.FakesImportedPropertyName).Equals("true", StringComparison.OrdinalIgnoreCase) + && dependency.ProjectInstance.GetPropertyValue(FakesPredictor.FakesUseV2GenerationPropertyName).Equals("true", StringComparison.OrdinalIgnoreCase)) + { + string fakesOutputPath = dependency.ProjectInstance.GetPropertyValue(FakesPredictor.FakesOutputPathPropertyName); + if (!string.IsNullOrWhiteSpace(fakesOutputPath)) + { + // Make it absolute since it may be relative to the dependency project + fakesOutputPath = Path.Combine(dependency.ProjectInstance.Directory, fakesOutputPath); + + foreach (ProjectItemInstance item in dependency.ProjectInstance.GetItems(FakesPredictor.FakesItemName)) + { + string fakesAssemblyFileName = $"{Path.GetFileNameWithoutExtension(item.EvaluatedInclude)}.Fakes.dll"; + predictionReporter.ReportInputFile(Path.Combine(fakesOutputPath, fakesAssemblyFileName)); + + if (!string.IsNullOrEmpty(outDir)) + { + predictionReporter.ReportOutputFile(Path.Combine(outDir, fakesAssemblyFileName)); + } + } + } + } } } } diff --git a/src/BuildPrediction/ProjectPredictors.cs b/src/BuildPrediction/ProjectPredictors.cs index 83e846b..5892d12 100644 --- a/src/BuildPrediction/ProjectPredictors.cs +++ b/src/BuildPrediction/ProjectPredictors.cs @@ -64,7 +64,7 @@ public static class ProjectPredictors /// /// /// - /// + /// /// /// /// A collection of . @@ -115,7 +115,7 @@ public static class ProjectPredictors new GenerateBuildDependencyFilePredictor(), new GeneratePublishDependencyFilePredictor(), new GenerateRuntimeConfigurationFilesPredictor(), - new FakesOutputPathPredictor(), + new FakesPredictor(), //// NOTE! When adding a new predictor here, be sure to update the doc comment above. }; diff --git a/src/BuildPredictionTests/Predictors/FakesOutputPathPredictorTests.cs b/src/BuildPredictionTests/Predictors/FakesOutputPathPredictorTests.cs deleted file mode 100644 index 821c877..0000000 --- a/src/BuildPredictionTests/Predictors/FakesOutputPathPredictorTests.cs +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Microsoft.Build.Construction; -using Microsoft.Build.Execution; -using Microsoft.Build.Prediction.Predictors; -using Xunit; - -namespace Microsoft.Build.Prediction.Tests.Predictors -{ - public class FakesOutputPathPredictorTests - { - [Fact] - public void FakesOutputPathFoundAsOutputDir() - { - const string FakesOutputPath = @"C:\repo\FakesAssemblies"; - ProjectInstance projectInstance = CreateTestProjectInstance(FakesOutputPath); - new FakesOutputPathPredictor() - .GetProjectPredictions(projectInstance) - .AssertPredictions( - projectInstance, - null, - null, - null, - new[] { new PredictedItem(FakesOutputPath, nameof(FakesOutputPathPredictor)) }); - } - - [Fact] - public void RelativeFakesOutputPathFoundAsOutputDir() - { - const string FakesOutputPath = @"bin\FakesAssemblies"; - ProjectInstance projectInstance = CreateTestProjectInstance(FakesOutputPath); - new FakesOutputPathPredictor() - .GetProjectPredictions(projectInstance) - .AssertPredictions( - projectInstance, - null, - null, - null, - new[] { new PredictedItem(FakesOutputPath, nameof(FakesOutputPathPredictor)) }); - } - - [Fact] - public void DefaultFakesAssembliesDirectoryFoundAsOutputDir() - { - const string FakesOutputPath = @"FakesAssemblies"; - ProjectInstance projectInstance = CreateTestProjectInstance(FakesOutputPath); - new FakesOutputPathPredictor() - .GetProjectPredictions(projectInstance) - .AssertPredictions( - projectInstance, - null, - null, - null, - new[] { new PredictedItem(FakesOutputPath, nameof(FakesOutputPathPredictor)) }); - } - - [Fact] - public void NoOutputsReportedIfNoFakesOutputPath() - { - ProjectInstance projectInstance = CreateTestProjectInstance(null); - new FakesOutputPathPredictor() - .GetProjectPredictions(projectInstance) - .AssertNoPredictions(); - } - - [Fact] - public void NoOutputsReportedIfEmptyFakesOutputPath() - { - ProjectInstance projectInstance = CreateTestProjectInstance(string.Empty); - new FakesOutputPathPredictor() - .GetProjectPredictions(projectInstance) - .AssertNoPredictions(); - } - - [Fact] - public void NoOutputsReportedIfWhitespaceFakesOutputPath() - { - ProjectInstance projectInstance = CreateTestProjectInstance(" "); - new FakesOutputPathPredictor() - .GetProjectPredictions(projectInstance) - .AssertNoPredictions(); - } - - private static ProjectInstance CreateTestProjectInstance(string fakesOutputPath) - { - ProjectRootElement projectRootElement = ProjectRootElement.Create(); - if (fakesOutputPath != null) - { - projectRootElement.AddProperty(FakesOutputPathPredictor.FakesOutputPathPropertyName, fakesOutputPath); - } - - return TestHelpers.CreateProjectInstanceFromRootElement(projectRootElement); - } - } -} \ No newline at end of file diff --git a/src/BuildPredictionTests/Predictors/FakesPredictorTests.cs b/src/BuildPredictionTests/Predictors/FakesPredictorTests.cs new file mode 100644 index 0000000..660e55b --- /dev/null +++ b/src/BuildPredictionTests/Predictors/FakesPredictorTests.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using Microsoft.Build.Construction; +using Microsoft.Build.Execution; +using Microsoft.Build.Prediction.Predictors; +using Xunit; + +namespace Microsoft.Build.Prediction.Tests.Predictors +{ + public class FakesPredictorTests + { + [Fact] + public void FindItems() + { + const string FakesOutputPath = @"bin\FakesAssemblies"; + ProjectInstance projectInstance = CreateTestProjectInstance(FakesOutputPath, ["A.fakes", "B.fakes", "C.fakes"]); + + var expectedInputFiles = new[] + { + new PredictedItem("A.fakes", nameof(FakesPredictor)), + new PredictedItem("B.fakes", nameof(FakesPredictor)), + new PredictedItem("C.fakes", nameof(FakesPredictor)), + }; + var expectedOutputFiles = new[] + { + new PredictedItem(Path.Combine(FakesOutputPath, "A.Fakes.dll"), nameof(FakesPredictor)), + new PredictedItem(Path.Combine(FakesOutputPath, "B.Fakes.dll"), nameof(FakesPredictor)), + new PredictedItem(Path.Combine(FakesOutputPath, "C.Fakes.dll"), nameof(FakesPredictor)), + }; + new FakesPredictor() + .GetProjectPredictions(projectInstance) + .AssertPredictions( + projectInstance, + expectedInputFiles, + null, + expectedOutputFiles, + null); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void NoOutputsReportedIfInvalidFakesOutputPath(string fakesOutputPath) + { + ProjectInstance projectInstance = CreateTestProjectInstance(fakesOutputPath, ["A.fakes", "B.fakes", "C.fakes"]); + var expectedInputFiles = new[] + { + new PredictedItem("A.fakes", nameof(FakesPredictor)), + new PredictedItem("B.fakes", nameof(FakesPredictor)), + new PredictedItem("C.fakes", nameof(FakesPredictor)), + }; + + new FakesPredictor() + .GetProjectPredictions(projectInstance) + .AssertPredictions( + projectInstance, + expectedInputFiles, + null, + null, + null); + } + + [Fact] + public void NoPredictionsReportedNoFakesItems() + { + ProjectInstance projectInstance = CreateTestProjectInstance(@"bin\FakesAssemblies", []); + var expectedInputFiles = new[] + { + new PredictedItem("A.fakes", nameof(FakesPredictor)), + new PredictedItem("B.fakes", nameof(FakesPredictor)), + new PredictedItem("C.fakes", nameof(FakesPredictor)), + }; + + new FakesPredictor() + .GetProjectPredictions(projectInstance) + .AssertNoPredictions(); + } + + private static ProjectInstance CreateTestProjectInstance( + string fakesOutputPath, + ReadOnlySpan fakesItems) + { + ProjectRootElement projectRootElement = ProjectRootElement.Create(); + projectRootElement.AddProperty(FakesPredictor.FakesImportedPropertyName, "true"); + + if (fakesOutputPath != null) + { + projectRootElement.AddProperty(FakesPredictor.FakesOutputPathPropertyName, fakesOutputPath); + } + + foreach (string fakesItem in fakesItems) + { + projectRootElement.AddItem(FakesPredictor.FakesItemName, fakesItem); + } + + return TestHelpers.CreateProjectInstanceFromRootElement(projectRootElement); + } + } +} \ No newline at end of file diff --git a/src/BuildPredictionTests/Predictors/GetCopyToOutputDirectoryItemsGraphPredictorTests.cs b/src/BuildPredictionTests/Predictors/GetCopyToOutputDirectoryItemsGraphPredictorTests.cs index 6f1ca15..d4e273b 100644 --- a/src/BuildPredictionTests/Predictors/GetCopyToOutputDirectoryItemsGraphPredictorTests.cs +++ b/src/BuildPredictionTests/Predictors/GetCopyToOutputDirectoryItemsGraphPredictorTests.cs @@ -225,6 +225,51 @@ void AddPropertyToAllProjects(string propertyName, string propertyValue) } } + [Fact] + public void DependencyWithFakesAssemblies() + { + string projectFile = Path.Combine(_rootDir, @"src\project.csproj"); + ProjectRootElement projectRootElement = ProjectRootElement.Create(projectFile); + projectRootElement.AddProperty(GetCopyToOutputDirectoryItemsGraphPredictor.OutDirPropertyName, @"bin\"); + + string dependencyProjectFile = Path.Combine(_rootDir, @"dep\dep.csproj"); + ProjectRootElement dependencyProjectRootElement = ProjectRootElement.Create(dependencyProjectFile); + dependencyProjectRootElement.AddProperty(FakesPredictor.FakesImportedPropertyName, "true"); + dependencyProjectRootElement.AddProperty(FakesPredictor.FakesUseV2GenerationPropertyName, "true"); + dependencyProjectRootElement.AddProperty(FakesPredictor.FakesOutputPathPropertyName, @"bin\FakesAssemblies"); + dependencyProjectRootElement.AddItem(FakesPredictor.FakesItemName, "A.fakes"); + dependencyProjectRootElement.AddItem(FakesPredictor.FakesItemName, "B.fakes"); + dependencyProjectRootElement.AddItem(FakesPredictor.FakesItemName, "C.fakes"); + + projectRootElement.AddItem("ProjectReference", @"..\dep\dep.csproj"); + + projectRootElement.Save(); + dependencyProjectRootElement.Save(); + + var expectedInputFiles = new[] + { + new PredictedItem(@"dep\bin\FakesAssemblies\A.Fakes.dll", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)), + new PredictedItem(@"dep\bin\FakesAssemblies\B.Fakes.dll", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)), + new PredictedItem(@"dep\bin\FakesAssemblies\C.Fakes.dll", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)), + }; + + var expectedOutputFiles = new[] + { + new PredictedItem(@"src\bin\A.Fakes.dll", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)), + new PredictedItem(@"src\bin\B.Fakes.dll", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)), + new PredictedItem(@"src\bin\C.Fakes.dll", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)), + }; + + new GetCopyToOutputDirectoryItemsGraphPredictor() + .GetProjectPredictions(projectFile) + .AssertPredictions( + _rootDir, + expectedInputFiles, + null, + expectedOutputFiles, + null); + } + private ProjectRootElement CreateDependencyProject(string projectName, bool shouldCopy) { string projectDir = Path.Combine(_rootDir, projectName);