From 342fed52b87f3156cbb937418ff94885e1780faf Mon Sep 17 00:00:00 2001 From: David Federman Date: Tue, 1 Oct 2024 16:09:00 -0700 Subject: [PATCH] Add DotnetSdkPredictor --- .../Predictors/DotnetSdkPredictor.cs | 46 +++++++ src/BuildPrediction/ProjectPredictors.cs | 2 + .../Predictors/DotnetSdkPredictorTests.cs | 115 ++++++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 src/BuildPrediction/Predictors/DotnetSdkPredictor.cs create mode 100644 src/BuildPredictionTests/Predictors/DotnetSdkPredictorTests.cs diff --git a/src/BuildPrediction/Predictors/DotnetSdkPredictor.cs b/src/BuildPrediction/Predictors/DotnetSdkPredictor.cs new file mode 100644 index 0000000..4095bff --- /dev/null +++ b/src/BuildPrediction/Predictors/DotnetSdkPredictor.cs @@ -0,0 +1,46 @@ +// 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.Collections.Concurrent; +using System.IO; +using Microsoft.Build.Execution; + +namespace Microsoft.Build.Prediction.Predictors; + +/// +/// Makes predictions based on using Microsoft.NET.Sdk. +/// +public sealed class DotnetSdkPredictor : IProjectPredictor +{ + internal const string UsingMicrosoftNETSdkPropertyName = "UsingMicrosoftNETSdk"; + + private readonly ConcurrentDictionary _globalJsonExistenceCache = new(PathComparer.Instance); + + /// + public void PredictInputsAndOutputs( + ProjectInstance projectInstance, + ProjectPredictionReporter predictionReporter) + { + var usingMicrosoftNETSdk = projectInstance.GetPropertyValue(UsingMicrosoftNETSdkPropertyName); + if (!usingMicrosoftNETSdk.Equals("true", StringComparison.OrdinalIgnoreCase)) + { + return; + } + + // Microsoft.NET.Sdk reads global.json. + string currentProbeDirectory = projectInstance.Directory; + while (currentProbeDirectory != null) + { + string globalJsonPath = Path.Combine(currentProbeDirectory, "global.json"); + bool globalJsonPathExists = _globalJsonExistenceCache.GetOrAdd(globalJsonPath, File.Exists); + if (globalJsonPathExists) + { + predictionReporter.ReportInputFile(globalJsonPath); + break; + } + + currentProbeDirectory = Path.GetDirectoryName(currentProbeDirectory); + } + } +} \ No newline at end of file diff --git a/src/BuildPrediction/ProjectPredictors.cs b/src/BuildPrediction/ProjectPredictors.cs index 4cd04ea..e61f721 100644 --- a/src/BuildPrediction/ProjectPredictors.cs +++ b/src/BuildPrediction/ProjectPredictors.cs @@ -60,6 +60,7 @@ public static class ProjectPredictors /// /// /// + /// /// /// /// A collection of . @@ -106,6 +107,7 @@ public static class ProjectPredictors new ModuleDefinitionFilePredictor(), new CppContentFilesProjectOutputGroupPredictor(), new LinkItemsPredictor(), + new DotnetSdkPredictor(), //// NOTE! When adding a new predictor here, be sure to update the doc comment above. }; diff --git a/src/BuildPredictionTests/Predictors/DotnetSdkPredictorTests.cs b/src/BuildPredictionTests/Predictors/DotnetSdkPredictorTests.cs new file mode 100644 index 0000000..f21d38e --- /dev/null +++ b/src/BuildPredictionTests/Predictors/DotnetSdkPredictorTests.cs @@ -0,0 +1,115 @@ +// 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 DotnetSdkPredictorTests +{ + private readonly string _rootDir; + + public DotnetSdkPredictorTests() + { + // Isolate each test into its own folder + _rootDir = Path.Combine(Directory.GetCurrentDirectory(), nameof(DotnetSdkPredictorTests), Guid.NewGuid().ToString()); + Directory.CreateDirectory(_rootDir); + } + + [Fact] + public void GlobalJsonExistsAdjacent() + { + ProjectRootElement projectRootElement = ProjectRootElement.Create(Path.Combine(_rootDir, @"src\project.csproj")); + projectRootElement.AddProperty(DotnetSdkPredictor.UsingMicrosoftNETSdkPropertyName, "true"); + + Directory.CreateDirectory(Path.Combine(_rootDir, "src")); + File.WriteAllText(Path.Combine(_rootDir, @"src\global.json"), "{}"); + + // Extraneous one above, which is not predicted as an input. + File.WriteAllText(Path.Combine(_rootDir, "global.json"), "{}"); + + ProjectInstance projectInstance = TestHelpers.CreateProjectInstanceFromRootElement(projectRootElement); + + var expectedInputFiles = new[] + { + new PredictedItem(@"src\global.json", nameof(DotnetSdkPredictor)), + }; + + new DotnetSdkPredictor() + .GetProjectPredictions(projectInstance) + .AssertPredictions( + projectInstance, + expectedInputFiles.MakeAbsolute(_rootDir), + null, + null, + null); + } + + [Fact] + public void GlobalJsonExistsAbove() + { + ProjectRootElement projectRootElement = ProjectRootElement.Create(Path.Combine(_rootDir, @"src\project.csproj")); + projectRootElement.AddProperty(DotnetSdkPredictor.UsingMicrosoftNETSdkPropertyName, "true"); + + File.WriteAllText(Path.Combine(_rootDir, "global.json"), "{}"); + + ProjectInstance projectInstance = TestHelpers.CreateProjectInstanceFromRootElement(projectRootElement); + + var expectedInputFiles = new[] + { + new PredictedItem(@"global.json", nameof(DotnetSdkPredictor)), + }; + + new DotnetSdkPredictor() + .GetProjectPredictions(projectInstance) + .AssertPredictions( + projectInstance, + expectedInputFiles.MakeAbsolute(_rootDir), + null, + null, + null); + } + + [Fact] + public void NoGlobalJsonExists() + { + ProjectRootElement projectRootElement = ProjectRootElement.Create(Path.Combine(_rootDir, @"src\project.csproj")); + projectRootElement.AddProperty(DotnetSdkPredictor.UsingMicrosoftNETSdkPropertyName, "true"); + + ProjectInstance projectInstance = TestHelpers.CreateProjectInstanceFromRootElement(projectRootElement); + + new DotnetSdkPredictor() + .GetProjectPredictions(projectInstance) + .AssertPredictions( + projectInstance, + null, + null, + null, + null); + } + + [Fact] + public void NotUsingDotnetSdk() + { + ProjectRootElement projectRootElement = ProjectRootElement.Create(Path.Combine(_rootDir, @"src\project.csproj")); + + Directory.CreateDirectory(Path.Combine(_rootDir, "src")); + File.WriteAllText(Path.Combine(_rootDir, "global.json"), "{}"); + + ProjectInstance projectInstance = TestHelpers.CreateProjectInstanceFromRootElement(projectRootElement); + + new DotnetSdkPredictor() + .GetProjectPredictions(projectInstance) + .AssertPredictions( + projectInstance, + null, + null, + null, + null); + } +} \ No newline at end of file