File: ProjectCache\ProjectCacheTests.cs
Web Access
Project: ..\..\..\src\Build.UnitTests\Microsoft.Build.Engine.UnitTests.csproj (Microsoft.Build.Engine.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Build.Construction;
using Microsoft.Build.Execution;
using Microsoft.Build.Experimental.ProjectCache;
using Microsoft.Build.Framework;
using Microsoft.Build.Graph;
using Microsoft.Build.Shared;
using Microsoft.Build.Unittest;
using Microsoft.Build.UnitTests;
using Microsoft.Build.Utilities;
using Shouldly;
using Xunit;
using Xunit.Abstractions;
using Task = System.Threading.Tasks.Task;
 
namespace Microsoft.Build.Engine.UnitTests.ProjectCache
{
    public class ProjectCacheTests : IDisposable
    {
        public ProjectCacheTests(ITestOutputHelper output)
        {
            _output = output;
            _env = TestEnvironment.Create(output);
 
            BuildManager.ProjectCacheDescriptors.ShouldBeEmpty();
            _env.WithInvariant(new CustomConditionInvariant(() => BuildManager.ProjectCacheDescriptors.IsEmpty));
        }
 
        public void Dispose()
        {
            _env.Dispose();
        }
 
        private const string AssemblyMockCache = nameof(AssemblyMockCache);
 
        private static readonly Lazy<string> SamplePluginAssemblyPath =
            new Lazy<string>(
                () =>
                {
                    return Directory.EnumerateFiles(
                        Path.GetFullPath(
                            Path.Combine(
                                BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory,
                                "..",
                                "..",
                                "..",
                                "Samples",
                                "ProjectCachePlugin")),
                        "ProjectCachePlugin.dll",
                        SearchOption.AllDirectories).First();
                });
 
        public class GraphCacheResponse
        {
            private readonly IDictionary<int, string>? _extraContentPerProjectNumber;
            public const string CacheHitByProxy = nameof(CacheHitByProxy);
            public const string CacheHitByTargetResult = nameof(CacheHitByTargetResult);
 
            private static readonly string P2PTargets =
                @$"
                    <ItemGroup>
                        <ProjectReferenceTargets Include=`Build` Targets=`Build` />
                        <{ItemTypeNames.ProjectCachePlugin} Include=`{SamplePluginAssemblyPath.Value}` />
                    </ItemGroup>
 
                    <Target Name=`Build` Returns=`@(ReturnValue)`>
                        <MSBuild Projects=`@(ProjectReference)` Targets=`Build`>
                            <Output TaskParameter=`TargetOutputs` ItemName=`ReferenceReturns` />
                        </MSBuild>
 
                        <Message Text=`Reference: %(ReferenceReturns.Identity) : %(ReferenceReturns.File)` Importance=`High` />
                        <Error Text=`Reference file [%(ReferenceReturns.File)] does not exist` Condition=`@(ReferenceReturns->Count()) != 0 and !Exists(%(ReferenceReturns.File))` />
 
                        <ItemGroup>
                            <ReturnValue Include=`$(MSBuildProjectName)` File=`$(MSBuildProjectFile)` />
                        </ItemGroup>
                    </Target>
 
                    <Target Name=`ProxyBuild` Returns=`@(ReturnValue)`>
                        <ItemGroup>
                            <ReturnValue Include=`$(MSBuildProjectName)` File=`$(MSBuildProjectFile)` {CacheHitByProxy}=`true`/>
                        </ItemGroup>
                    </Target>";
 
            private Dictionary<int, int[]?> GraphEdges { get; }
 
            public Dictionary<int, CacheResult> NonCacheMissResults { get; }
 
            public GraphCacheResponse(Dictionary<int, int[]?> graphEdges, Dictionary<int, CacheResult>? nonCacheMissResults = null, IDictionary<int, string>? extraContentPerProjectNumber = null)
            {
                _extraContentPerProjectNumber = extraContentPerProjectNumber;
                GraphEdges = graphEdges;
                NonCacheMissResults = nonCacheMissResults ?? new Dictionary<int, CacheResult>();
            }
 
            public ProjectGraph CreateGraph(TestEnvironment env)
                => Helpers.CreateProjectGraph(
                    env,
                    GraphEdges,
                    extraContentPerProjectNumber: _extraContentPerProjectNumber,
                    extraContentForAllNodes: P2PTargets);
 
            public static CacheResult SuccessfulProxyTargetResult()
            {
                return CacheResult.IndicateCacheHit(
                    new ProxyTargets(
                        new Dictionary<string, string>
                        {
                            {"ProxyBuild", "Build"}
                        }));
            }
 
            public static CacheResult SuccessfulTargetResult(int projectNumber, string projectPath)
            {
                return CacheResult.IndicateCacheHit(
                    new[]
                    {
                        new PluginTargetResult(
                            "Build",
                            new ITaskItem2[]
                            {
                                new TaskItem(
                                    projectNumber.ToString(),
                                    new Dictionary<string, string>
                                    {
                                        {"File", projectPath},
                                        {CacheHitByTargetResult, "true"}
                                    })
                            },
                            BuildResultCode.Success)
                    });
            }
 
            public CacheResult GetExpectedCacheResultForNode(ProjectGraphNode node)
            {
                return GetExpectedCacheResultForProjectNumber(GetProjectNumber(node));
            }
 
            public CacheResult GetExpectedCacheResultForProjectNumber(int projectNumber)
            {
                return NonCacheMissResults.TryGetValue(projectNumber, out var cacheResult)
                    ? cacheResult
                    : CacheResult.IndicateNonCacheHit(CacheResultType.CacheMiss);
            }
 
            public override string ToString()
            {
                return string.Join(
                    ", ",
                    GraphEdges.Select(e => $"{Node(e.Key)}->{FormatChildren(e.Value)}"));
 
                string FormatChildren(int[]? children)
                {
                    return children == null
                        ? "Null"
                        : string.Join(",", children.Select(c => Node(c)));
                }
 
                string Node(int projectNumber)
                {
                    return $"{projectNumber}({Chr(projectNumber)})";
                }
 
                char Chr(int projectNumber)
                {
                    var cacheResult = GetExpectedCacheResultForProjectNumber(projectNumber);
                    return cacheResult.ResultType switch
                    {
 
                        CacheResultType.CacheHit => cacheResult.ProxyTargets != null
                            ? 'P'
                            : 'T',
                        CacheResultType.CacheMiss => 'M',
                        CacheResultType.CacheNotApplicable => 'N',
                        CacheResultType.None => 'E',
                        _ => throw new ArgumentOutOfRangeException()
                    };
                }
            }
        }
 
        public class DelegatingMockCache : ProjectCachePluginBase
        {
            private readonly Func<BuildRequestData, PluginLoggerBase, CancellationToken, Task<CacheResult>> _getCacheResultDelegate;
 
            public DelegatingMockCache(Func<BuildRequestData, PluginLoggerBase, CancellationToken, Task<CacheResult>> getCacheResultDelegate)
            {
                _getCacheResultDelegate = getCacheResultDelegate;
            }
 
            public override Task BeginBuildAsync(CacheContext context, PluginLoggerBase logger, CancellationToken cancellationToken)
            {
                return Task.CompletedTask;
            }
 
            public override async Task<CacheResult> GetCacheResultAsync(BuildRequestData buildRequest, PluginLoggerBase logger, CancellationToken cancellationToken)
            {
                return await _getCacheResultDelegate(buildRequest, logger, cancellationToken);
            }
 
            public override Task EndBuildAsync(PluginLoggerBase logger, CancellationToken cancellationToken)
            {
                return Task.CompletedTask;
            }
        }
 
        [Flags]
        public enum ErrorLocations
        {
            Constructor = 1 << 0,
            BeginBuildAsync = 1 << 1,
            GetCacheResultAsync = 1 << 2,
            EndBuildAsync = 1 << 3
        }
 
        public enum ErrorKind
        {
            Exception,
            LoggedError
        }
 
        public class ConfigurableMockCache : ProjectCachePluginBase
        {
            public Func<CacheContext, PluginLoggerBase, CancellationToken, Task>? BeginBuildImplementation { get; set; }
            public Func<BuildRequestData, PluginLoggerBase, CancellationToken, Task<CacheResult>>? GetCacheResultImplementation { get; set; }
 
            public override Task BeginBuildAsync(CacheContext context, PluginLoggerBase logger, CancellationToken cancellationToken)
            {
                return BeginBuildImplementation != null
                    ? BeginBuildImplementation(context, logger, cancellationToken)
                    : Task.CompletedTask;
            }
 
            public override Task<CacheResult> GetCacheResultAsync(
                BuildRequestData buildRequest,
                PluginLoggerBase logger,
                CancellationToken cancellationToken)
            {
                return GetCacheResultImplementation != null
                    ? GetCacheResultImplementation(buildRequest, logger, cancellationToken)
                    : Task.FromResult(CacheResult.IndicateNonCacheHit(CacheResultType.CacheNotApplicable));
            }
 
            public override Task EndBuildAsync(PluginLoggerBase logger, CancellationToken cancellationToken)
            {
                return Task.CompletedTask;
            }
        }
 
        public class InstanceMockCache : ProjectCachePluginBase
        {
            private readonly GraphCacheResponse? _testData;
            private readonly TimeSpan? _projectQuerySleepTime;
            public ConcurrentQueue<BuildRequestData> Requests { get; } = new();
 
            public bool BeginBuildCalled { get; set; }
            public bool EndBuildCalled { get; set; }
 
            private int _nextId;
            public ConcurrentQueue<int> QueryStartStops = new();
 
            public InstanceMockCache(GraphCacheResponse? testData = null, TimeSpan? projectQuerySleepTime = null)
            {
                _testData = testData;
                _projectQuerySleepTime = projectQuerySleepTime;
            }
 
            public override Task BeginBuildAsync(CacheContext context, PluginLoggerBase logger, CancellationToken cancellationToken)
            {
                logger.LogMessage("MockCache: BeginBuildAsync", MessageImportance.High);
 
                BeginBuildCalled = true;
 
                return Task.CompletedTask;
            }
 
            public override async Task<CacheResult> GetCacheResultAsync(
                BuildRequestData buildRequest,
                PluginLoggerBase logger,
                CancellationToken cancellationToken)
            {
                var queryId = Interlocked.Increment(ref _nextId);
 
                Requests.Enqueue(buildRequest);
                QueryStartStops.Enqueue(queryId);
 
                logger.LogMessage($"MockCache: GetCacheResultAsync for {buildRequest.ProjectFullPath}", MessageImportance.High);
 
                buildRequest.ProjectInstance.ShouldNotBeNull("The cache plugin expects evaluated projects.");
 
                if (_projectQuerySleepTime is not null)
                {
                    await Task.Delay(_projectQuerySleepTime.Value, cancellationToken);
                }
 
                QueryStartStops.Enqueue(queryId);
 
                return _testData?.GetExpectedCacheResultForProjectNumber(GetProjectNumber(buildRequest.ProjectFullPath))
                        ?? CacheResult.IndicateNonCacheHit(CacheResultType.CacheMiss);
            }
 
            public override Task EndBuildAsync(PluginLoggerBase logger, CancellationToken cancellationToken)
            {
                logger.LogMessage("MockCache: EndBuildAsync", MessageImportance.High);
 
                EndBuildCalled = true;
 
                return Task.CompletedTask;
            }
 
            public CacheResult GetCacheResultForNode(ProjectGraphNode node)
            {
                throw new NotImplementedException();
            }
        }
 
        private readonly TestEnvironment _env;
 
        private readonly ITestOutputHelper _output;
 
        public static IEnumerable<GraphCacheResponse> SuccessfulGraphs
        {
            get
            {
                yield return new GraphCacheResponse(
                    new Dictionary<int, int[]?>
                    {
                        {1, null}
                    });
 
                yield return new GraphCacheResponse(
                    new Dictionary<int, int[]?>
                    {
                        {1, null}
                    },
                    new Dictionary<int, CacheResult>
                    {
                        {1, GraphCacheResponse.SuccessfulProxyTargetResult() }
                    });
 
                yield return new GraphCacheResponse(
                    new Dictionary<int, int[]?>
                    {
                        {1, null}
                    },
                    new Dictionary<int, CacheResult>
                    {
                        {1, GraphCacheResponse.SuccessfulTargetResult(1, "1.proj") }
                    });
 
                yield return new GraphCacheResponse(
                    new Dictionary<int, int[]?>
                    {
                        {1, new[] {2}}
                    });
 
                yield return new GraphCacheResponse(
                    new Dictionary<int, int[]?>
                    {
                        {1, new[] {2}}
                    },
                    new Dictionary<int, CacheResult>
                    {
                        {2, GraphCacheResponse.SuccessfulProxyTargetResult() }
                    });
 
                yield return new GraphCacheResponse(
                    new Dictionary<int, int[]?>
                    {
                        {1, new[] {2}}
                    },
                    new Dictionary<int, CacheResult>
                    {
                        {2, GraphCacheResponse.SuccessfulTargetResult(2, "2.proj") }
                    });
 
                yield return new GraphCacheResponse(
                    new Dictionary<int, int[]?>
                    {
                        {1, new[] {2}}
                    },
                    new Dictionary<int, CacheResult>
                    {
                        {1, GraphCacheResponse.SuccessfulProxyTargetResult() },
                        {2, GraphCacheResponse.SuccessfulTargetResult(2, "2.proj") }
                    });
 
                yield return new GraphCacheResponse(
                    new Dictionary<int, int[]?>
                    {
                        {1, new[] {2, 3, 7}},
                        {2, new[] {4}},
                        {3, new[] {4}},
                        {4, new[] {5, 6, 7}}
                    });
            }
        }
 
        public static IEnumerable<object[]> MultiProcWithAndWithoutInProcNode
        {
            get
            {
                yield return new object[]
                {
                    new BuildParameters
                    {
                        DisableInProcNode = false,
                        MaxNodeCount = Environment.ProcessorCount
                    }
                };
 
                yield return new object[]
                {
                    new BuildParameters
                    {
                        DisableInProcNode = true,
                        MaxNodeCount = Environment.ProcessorCount
                    }
                };
            }
        }
 
        public static IEnumerable<object[]> SuccessfulGraphsWithBuildParameters
        {
            get
            {
                foreach (var graph in SuccessfulGraphs)
                {
                    foreach (var buildParameters in MultiProcWithAndWithoutInProcNode)
                    {
                        yield return new object[]
                        {
                            graph,
                            buildParameters[0]
                        };
                    }
                }
            }
        }
 
        [Theory]
        [MemberData(nameof(SuccessfulGraphsWithBuildParameters))]
        public void ProjectCacheByBuildParametersAndGraphBuildWorks(GraphCacheResponse testData, BuildParameters buildParameters)
        {
            _output.WriteLine(testData.ToString());
            var graph = testData.CreateGraph(_env);
            var mockCache = new InstanceMockCache(testData);
 
            // Reset the environment variables stored in the build params to take into account TestEnvironmentChanges.
            buildParameters = new BuildParameters(buildParameters, resetEnvironment: true)
            {
                ProjectCacheDescriptor = ProjectCacheDescriptor.FromInstance(mockCache)
            };
 
            MockLogger logger;
            GraphBuildResult graphResult;
            using (var buildSession = new Helpers.BuildManagerSession(_env, buildParameters))
            {
                logger = buildSession.Logger;
                graphResult = buildSession.BuildGraph(graph);
            }
 
            graphResult.ShouldHaveSucceeded();
 
            AssertCacheBuild(graph, testData, mockCache, logger, graphResult.ResultsByNode, targets: "Build");
        }
 
        [Theory]
        [MemberData(nameof(SuccessfulGraphsWithBuildParameters))]
        public void ProjectCacheByBuildParametersAndBottomUpBuildWorks(GraphCacheResponse testData, BuildParameters buildParameters)
        {
            var graph = testData.CreateGraph(_env);
            var mockCache = new InstanceMockCache(testData);
 
            var projectCacheDescriptor = ProjectCacheDescriptor.FromInstance(mockCache);
 
            // Reset the environment variables stored in the build params to take into account TestEnvironmentChanges.
            buildParameters = new BuildParameters(buildParameters, resetEnvironment: true)
            {
                ProjectCacheDescriptor = projectCacheDescriptor
            };
 
            MockLogger logger;
            var nodesToBuildResults = new Dictionary<ProjectGraphNode, BuildResult>();
            using (var buildSession = new Helpers.BuildManagerSession(_env, buildParameters))
            {
                logger = buildSession.Logger;
 
                foreach (var node in graph.ProjectNodesTopologicallySorted)
                {
                    var buildResult = buildSession.BuildProjectFile(node.ProjectInstance.FullPath);
 
                    buildResult.ShouldHaveSucceeded();
 
                    nodesToBuildResults[node] = buildResult;
                }
            }
 
 
            AssertCacheBuild(graph, testData, mockCache, logger, nodesToBuildResults, targets: null);
        }
 
        [Theory]
        [MemberData(nameof(SuccessfulGraphsWithBuildParameters))]
        public void ProjectCacheByVsScenarioWorks(GraphCacheResponse testData, BuildParameters buildParameters)
        {
            (MockLogger logger, ProjectGraph graph, Dictionary<ProjectGraphNode, BuildResult> nodesToBuildResults) = BuildGraphVsScenario(testData, buildParameters);
 
            AssertCacheBuild(graph, testData, null, logger, nodesToBuildResults, targets: null);
        }
 
        [Fact]
        public void ProjectCacheByVsScenarioIgnoresSlnDisabledProjects()
        {
            var testData = new GraphCacheResponse(
                new Dictionary<int, int[]?>
                {
                    { 1, new[] { 2 } },
                },
                extraContentPerProjectNumber: new Dictionary<int, string>()
                {
                    { 1, "<PropertyGroup> <BuildProjectInSolution>false</BuildProjectInSolution> </PropertyGroup>" },
                });
 
            (MockLogger logger, ProjectGraph graph, _) = BuildGraphVsScenario(testData, assertBuildResults: false);
 
            logger.FullLog.ShouldNotContain($"EntryPoint: {graph.GraphRoots.First().ProjectInstance.FullPath}");
            logger.FullLog.ShouldContain($"EntryPoint: {graph.GraphRoots.First().ProjectReferences.First().ProjectInstance.FullPath}");
        }
 
        private (MockLogger logger, ProjectGraph projectGraph, Dictionary<ProjectGraphNode, BuildResult> nodesToBuildResults) BuildGraphVsScenario(
            GraphCacheResponse testData,
            BuildParameters? buildParameters = null,
            bool assertBuildResults = true)
        {
            var nodesToBuildResults = new Dictionary<ProjectGraphNode, BuildResult>();
            MockLogger logger;
            ProjectGraph graph;
 
            var currentBuildEnvironment = BuildEnvironmentHelper.Instance;
 
            try
            {
                BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(
                    new BuildEnvironment(
                        currentBuildEnvironment.Mode,
                        currentBuildEnvironment.CurrentMSBuildExePath,
                        currentBuildEnvironment.RunningTests,
                        currentBuildEnvironment.RunningInMSBuildExe,
                        runningInVisualStudio: true,
                        visualStudioPath: currentBuildEnvironment.VisualStudioInstallRootDirectory));
 
                // Reset the environment variables stored in the build params to take into account TestEnvironmentChanges.
                buildParameters = buildParameters is null
                    ? new BuildParameters()
                    : new BuildParameters(buildParameters, resetEnvironment: true);
 
                BuildManager.ProjectCacheDescriptors.ShouldBeEmpty();
 
                graph = testData.CreateGraph(_env);
 
                BuildManager.ProjectCacheDescriptors.ShouldHaveSingleItem();
 
                // VS sets this global property on every project it builds.
                string solutionConfigurationGlobalProperty = CreateSolutionConfigurationProperty(graph.ProjectNodes);
 
                using (var buildSession = new Helpers.BuildManagerSession(_env, buildParameters))
                {
                    logger = buildSession.Logger;
 
                    foreach (var node in graph.ProjectNodesTopologicallySorted)
                    {
                        BuildResult buildResult = buildSession.BuildProjectFile(
                            node.ProjectInstance.FullPath,
                            globalProperties:
                                new Dictionary<string, string>
                                {
                                    { SolutionProjectGenerator.CurrentSolutionConfigurationContents, solutionConfigurationGlobalProperty },
                                    { "TargetFramework", "net472"},
                                });
 
                        if (assertBuildResults)
                        {
                            buildResult.ShouldHaveSucceeded();
                        }
 
                        nodesToBuildResults[node] = buildResult;
                    }
                }
 
                if (assertBuildResults)
                {
                    foreach (var node in graph.ProjectNodes)
                    {
                        var projectPath = node.ProjectInstance.FullPath;
                        var projectName = Path.GetFileNameWithoutExtension(projectPath);
 
                        // Ensure MSBuild passes config / platform information set by VS.
                        logger.FullLog.ShouldContain($"EntryPoint: {projectPath}");
                        logger.FullLog.ShouldContain($"Configuration:{projectName}Debug");
                        logger.FullLog.ShouldContain($"Platform:{projectName}x64");
 
                        // Ensure MSBuild removes the target framework if present.
                        logger.FullLog.ShouldNotContain("TargetFramework:net472");
                    }
                }
            }
            finally
            {
                BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(currentBuildEnvironment);
                BuildManager.ProjectCacheDescriptors.Clear();
            }
 
            return (logger, graph, nodesToBuildResults);
        }
 
        private static string CreateSolutionConfigurationProperty(IReadOnlyCollection<ProjectGraphNode> projectNodes)
        {
            var sb = new StringBuilder();
 
            sb.AppendLine("<SolutionConfiguration>");
 
            foreach (var node in projectNodes)
            {
                var projectPath = node.ProjectInstance.FullPath;
                var projectName = Path.GetFileNameWithoutExtension(projectPath);
 
                var buildProjectInSolutionValue = node.ProjectInstance.GetPropertyValue("BuildProjectInSolution");
                var buildProjectInSolutionAttribute = string.IsNullOrWhiteSpace(buildProjectInSolutionValue)
                    ? string.Empty
                    : $"BuildProjectInSolution=\"{buildProjectInSolutionValue}\"";
 
                var projectDependencyValue = node.ProjectInstance.GetPropertyValue("ProjectDependency");
                var projectDependencyElement = string.IsNullOrWhiteSpace(projectDependencyValue)
                    ? string.Empty
                    : $"<ProjectDependency Project=\"{projectDependencyValue}\" />";
 
                sb.AppendLine($"<ProjectConfiguration Project=\"{Guid.NewGuid()}\" AbsolutePath=\"{projectPath}\" {buildProjectInSolutionAttribute}>{projectName}Debug|{projectName}x64{projectDependencyElement}</ProjectConfiguration>");
            }
 
            sb.AppendLine("</SolutionConfiguration>");
 
            return sb.ToString();
        }
 
        [Fact]
        public void DesignTimeBuildsDuringVsScenarioShouldDisableTheCache()
        {
            var currentBuildEnvironment = BuildEnvironmentHelper.Instance;
 
            // Use a few references to stress test the design time build workaround logic.
            var referenceNumbers = Enumerable.Range(2, NativeMethodsShared.GetLogicalCoreCount()).ToArray();
 
            var testData = new GraphCacheResponse(
                graphEdges: new Dictionary<int, int[]?>
                {
                    {1, referenceNumbers}
                });
 
            try
            {
                BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(
                    new BuildEnvironment(
                        currentBuildEnvironment.Mode,
                        currentBuildEnvironment.CurrentMSBuildExePath,
                        currentBuildEnvironment.RunningTests,
                        currentBuildEnvironment.RunningInMSBuildExe,
                        runningInVisualStudio: true,
                        visualStudioPath: currentBuildEnvironment.VisualStudioInstallRootDirectory));
 
                var graph = testData.CreateGraph(_env);
 
                var rootNode = graph.GraphRoots.First();
 
                var globalProperties = new Dictionary<string, string>
                {
                    { DesignTimeProperties.DesignTimeBuild, "true" },
                };
 
                MockLogger logger;
                using (var buildSession = new Helpers.BuildManagerSession(_env))
                {
                    logger = buildSession.Logger;
 
                    // Build references in parallel.
                    var referenceBuildTasks = rootNode.ProjectReferences.Select(
                        r => buildSession.BuildProjectFileAsync(r.ProjectInstance.FullPath, globalProperties: globalProperties));
 
                    foreach (var task in referenceBuildTasks)
                    {
                        var buildResult = task.Result;
                        buildResult.ShouldHaveSucceeded();
                    }
 
                    buildSession
                        .BuildProjectFile(rootNode.ProjectInstance.FullPath, globalProperties: globalProperties)
                        .ShouldHaveSucceeded();
                }
 
                // Cache doesn't get initialized, queried, or disposed.
                logger.FullLog.ShouldNotContain("BeginBuildAsync");
                logger.FullLog.ShouldNotContain("GetCacheResultAsync for");
                logger.FullLog.ShouldNotContain("Querying project cache for project");
                logger.FullLog.ShouldNotContain("EndBuildAsync");
            }
            finally
            {
                BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(currentBuildEnvironment);
                BuildManager.ProjectCacheDescriptors.Clear();
            }
        }
 
        [Theory]
        [InlineData(true)]
        [InlineData(false)]
        public void RunningProxyBuildsOnOutOfProcNodesShouldIssueWarning(bool disableInprocNodeViaEnvironmentVariable)
        {
            var testData = new GraphCacheResponse(
                new Dictionary<int, int[]?>
                {
                    {1, new[] {2}}
                },
                new Dictionary<int, CacheResult>
                {
                    {1, GraphCacheResponse.SuccessfulProxyTargetResult() },
                    {2, GraphCacheResponse.SuccessfulProxyTargetResult() }
                });
 
            var graph = testData.CreateGraph(_env);
            var mockCache = new InstanceMockCache(testData);
 
            var buildParameters = new BuildParameters
            {
                MaxNodeCount = Environment.ProcessorCount,
                ProjectCacheDescriptor = ProjectCacheDescriptor.FromInstance(mockCache)
            };
 
            if (disableInprocNodeViaEnvironmentVariable)
            {
                _env.SetEnvironmentVariable("MSBUILDNOINPROCNODE", "1");
            }
            else
            {
                buildParameters.DisableInProcNode = true;
            }
 
            MockLogger logger;
            GraphBuildResult graphResult;
            using (var buildSession = new Helpers.BuildManagerSession(_env, buildParameters))
            {
                logger = buildSession.Logger;
                graphResult = buildSession.BuildGraph(graph);
            }
 
            graphResult.ShouldHaveSucceeded();
 
            logger.AssertMessageCount("MSB4274", 1);
        }
 
        private void AssertCacheBuild(
            ProjectGraph graph,
            GraphCacheResponse testData,
            InstanceMockCache? instanceMockCache,
            MockLogger mockLogger,
            IReadOnlyDictionary<ProjectGraphNode, BuildResult> projectPathToBuildResults,
            string? targets)
        {
            if (instanceMockCache != null)
            {
                instanceMockCache.BeginBuildCalled.ShouldBeTrue();
                instanceMockCache.Requests.Count.ShouldBe(graph.ProjectNodes.Count);
                instanceMockCache.EndBuildCalled.ShouldBeTrue();
            }
            else
            {
                mockLogger.FullLog.ShouldContain($"{AssemblyMockCache}: BeginBuildAsync");
                Regex.Matches(mockLogger.FullLog, $"{AssemblyMockCache}: GetCacheResultAsync for").Count.ShouldBe(graph.ProjectNodes.Count);
                mockLogger.FullLog.ShouldContain($"{AssemblyMockCache}: EndBuildAsync");
            }
 
            foreach (var node in graph.ProjectNodes)
            {
                if (string.IsNullOrEmpty(targets))
                {
                    mockLogger.FullLog.ShouldContain(string.Format(ResourceUtilities.GetResourceString("ProjectCacheQueryStartedWithDefaultTargets"), node.ProjectInstance.FullPath));
                }
                else
                {
                    mockLogger.FullLog.ShouldContain(string.Format(ResourceUtilities.GetResourceString("ProjectCacheQueryStartedWithTargetNames"), node.ProjectInstance.FullPath, targets));
                }
 
                if (instanceMockCache != null)
                {
                    instanceMockCache.Requests.ShouldContain(r => r.ProjectFullPath.Equals(node.ProjectInstance.FullPath));
 
                    var expectedCacheResponse = testData.GetExpectedCacheResultForNode(node);
                    switch (expectedCacheResponse.ResultType)
                    {
                        case CacheResultType.CacheHit:
                            AssertBuildResultForCacheHit(node.ProjectInstance.FullPath, projectPathToBuildResults[node], expectedCacheResponse);
                            break;
                        case CacheResultType.CacheMiss:
                            break;
                        case CacheResultType.CacheNotApplicable:
                            break;
                        case CacheResultType.None:
                            break;
                        default:
                            throw new ArgumentOutOfRangeException();
                    }
                }
                else
                {
                    mockLogger.FullLog.ShouldContain($"{AssemblyMockCache}: GetCacheResultAsync for {node.ProjectInstance.FullPath}");
 
                    // Too complicated, not worth it to send expected results to the assembly plugin, so skip checking the build results.
                }
            }
        }
 
        private static int GetProjectNumber(ProjectGraphNode node) => GetProjectNumber(node.ProjectInstance.FullPath);
 
        private static int GetProjectNumber(string projectPath) => int.Parse(Path.GetFileNameWithoutExtension(projectPath));
 
        private void AssertBuildResultForCacheHit(
            string projectPath,
            BuildResult buildResult,
            CacheResult expectedCacheResponse)
        {
            // If the cache hit is via proxy targets then the build result should contain entry for both the real target
            // and the proxy target. Both target results should be the same.
            // If it's not a cache result by proxy targets then the cache constructed the target results by hand and only the real target result
            // exists in the BuildResult.
 
            var targetResult = buildResult.ResultsByTarget!["Build"];
 
            targetResult.Items.ShouldHaveSingleItem();
            var itemResult = targetResult.Items.First();
            string expectedMetadata;
 
            if (expectedCacheResponse.ProxyTargets != null)
            {
                var proxyTargetResult = buildResult.ResultsByTarget["ProxyBuild"];
                SdkUtilities.EngineHelpers.AssertTargetResultsEqual(targetResult, proxyTargetResult);
 
                expectedMetadata = GraphCacheResponse.CacheHitByProxy;
            }
            else
            {
                expectedMetadata = GraphCacheResponse.CacheHitByTargetResult;
            }
 
            itemResult.ItemSpec.ShouldBe(GetProjectNumber(projectPath).ToString());
            itemResult.GetMetadata("File").ShouldBe(Path.GetFileName(projectPath));
            itemResult.GetMetadata(expectedMetadata).ShouldBe("true");
        }
 
        [Theory]
        [MemberData(nameof(MultiProcWithAndWithoutInProcNode))]
        public void CacheShouldNotGetQueriedForNestedBuildRequests(BuildParameters buildParameters)
        {
            var project1 = _env.CreateFile("1.proj", @"
                    <Project>
                        <Target Name=`Build`>
                            <MSBuild Projects=`2.proj` />
                        </Target>
                    </Project>".Cleanup());
 
            _env.CreateFile("2.proj", @"
                    <Project>
                        <Target Name=`Build`>
                            <Message Text=`Hello` Importance=`High` />
                        </Target>
                    </Project>".Cleanup());
 
            var mockCache = new InstanceMockCache();
            buildParameters.ProjectCacheDescriptor = ProjectCacheDescriptor.FromInstance(mockCache);
 
            MockLogger logger;
            using (var buildSession = new Helpers.BuildManagerSession(_env, buildParameters))
            {
                logger = buildSession.Logger;
 
                BuildResult buildResult = buildSession.BuildProjectFile(project1.Path);
 
                buildResult.ShouldHaveSucceeded();
            }
 
            logger.ProjectStartedEvents.Count.ShouldBe(2);
 
            mockCache.Requests.Count.ShouldBe(1);
            mockCache.Requests.First().ProjectFullPath.ShouldEndWith("1.proj");
        }
 
        [Fact]
        public void CacheViaBuildParametersCanDiscoverAndLoadPluginFromAssembly()
        {
            var testData = new GraphCacheResponse(
                new Dictionary<int, int[]?>
                {
                    {1, new[] {2, 3}}
                });
 
            var graph = testData.CreateGraph(_env);
 
            var buildParameters = new BuildParameters
            {
                ProjectCacheDescriptor = ProjectCacheDescriptor.FromAssemblyPath(SamplePluginAssemblyPath.Value)
            };
 
            MockLogger logger;
            GraphBuildResult graphResult;
            using (var buildSession = new Helpers.BuildManagerSession(_env, buildParameters))
            {
                logger = buildSession.Logger;
                graphResult = buildSession.BuildGraph(graph);
            }
 
            graphResult.ShouldHaveSucceeded();
 
            logger.FullLog.ShouldContain($"Loading the following project cache plugin: {AssemblyMockCache}");
 
            AssertCacheBuild(graph, testData, null, logger, graphResult.ResultsByNode, targets: "Build");
        }
 
        [Fact]
        public void GraphBuildCanDiscoverAndLoadPluginFromAssembly()
        {
            var testData = new GraphCacheResponse(
                new Dictionary<int, int[]?>
                {
                    {1, new[] {2, 3}}
                });
 
            var graph = testData.CreateGraph(_env);
 
            MockLogger logger;
            GraphBuildResult graphResult;
            using (var buildSession = new Helpers.BuildManagerSession(_env))
            {
                logger = buildSession.Logger;
                graphResult = buildSession.BuildGraph(graph);
            }
 
            graphResult.ShouldHaveSucceeded();
 
            AssertCacheBuild(graph, testData, null, logger, graphResult.ResultsByNode, targets: "Build");
        }
 
        [Fact]
        public void BuildFailsWhenCacheBuildResultIsWrong()
        {
            var testData = new GraphCacheResponse(
                new Dictionary<int, int[]?>
                {
                    {1, new[] {2}}
                },
                new Dictionary<int, CacheResult>
                {
                    {
                        2, CacheResult.IndicateCacheHit(
                            new[]
                            {
                                new PluginTargetResult(
                                    "Build",
                                    new ITaskItem2[]
                                    {
                                        new TaskItem(
                                            "NA",
                                            new Dictionary<string, string>
                                            {
                                                {"File", "Invalid file"}
                                            })
                                    },
                                    BuildResultCode.Success)
                            })
                    }
                });
 
            var graph = testData.CreateGraph(_env);
            var mockCache = new InstanceMockCache(testData);
 
            var buildParameters = new BuildParameters
            {
                ProjectCacheDescriptor = ProjectCacheDescriptor.FromInstance(mockCache)
            };
 
            MockLogger logger;
            GraphBuildResult graphResult;
            using (var buildSession = new Helpers.BuildManagerSession(_env, buildParameters))
            {
                logger = buildSession.Logger;
                graphResult = buildSession.BuildGraph(graph);
            }
 
            mockCache.Requests.Count.ShouldBe(2);
 
            graphResult.ResultsByNode.First(r => GetProjectNumber(r.Key) == 2).Value.ShouldHaveSucceeded();
            graphResult.ResultsByNode.First(r => GetProjectNumber(r.Key) == 1).Value.ShouldHaveFailed();
 
            graphResult.ShouldHaveFailed();
 
            logger.FullLog.ShouldContain("Reference file [Invalid file] does not exist");
        }
 
        [Fact]
        public void MultiplePlugins()
        {
            // One from the project, one from BuildParameters.
            var graph = Helpers.CreateProjectGraph(
                _env,
                new Dictionary<int, int[]?>
                {
                    { 1, new[] { 2 } },
                },
                extraContentForAllNodes: @$"
<ItemGroup>
   <{ItemTypeNames.ProjectCachePlugin} Include='{SamplePluginAssemblyPath.Value}' />
</ItemGroup>
");
            var mockCache = new InstanceMockCache();
 
            var buildParameters = new BuildParameters
            {
                ProjectCacheDescriptor = ProjectCacheDescriptor.FromInstance(mockCache),
            };
 
            MockLogger logger;
            GraphBuildResult graphResult;
            using (var buildSession = new Helpers.BuildManagerSession(_env, buildParameters))
            {
                logger = buildSession.Logger;
                graphResult = buildSession.BuildGraph(graph);
            }
 
            graphResult.ShouldHaveSucceeded();
        }
 
        [Fact]
        public void NotAllNodesDefineAPlugin()
        {
            var graph = Helpers.CreateProjectGraph(
                _env,
                dependencyEdges: new Dictionary<int, int[]?>
                {
                    { 1, new[] { 2 } },
                },
                extraContentPerProjectNumber: new Dictionary<int, string>
                {
                    {
                        2,
                        @$"
<ItemGroup>
   <{ItemTypeNames.ProjectCachePlugin} Include='{SamplePluginAssemblyPath.Value}' />
</ItemGroup>
"
                    }
                });
 
            MockLogger logger;
            GraphBuildResult graphResult;
            using (var buildSession = new Helpers.BuildManagerSession(_env))
            {
                logger = buildSession.Logger;
                graphResult = buildSession.BuildGraph(graph);
            }
 
            graphResult.ShouldHaveSucceeded();
        }
 
        public static IEnumerable<object[]> CacheExceptionLocationsTestData
        {
            get
            {
                // Plugin constructors cannot log errors, they can only throw exceptions.
                yield return new object[] { ErrorLocations.Constructor, ErrorKind.Exception };
 
                foreach (var errorKind in new[] { ErrorKind.Exception, ErrorKind.LoggedError })
                {
                    yield return new object[] { ErrorLocations.BeginBuildAsync, errorKind };
                    yield return new object[] { ErrorLocations.BeginBuildAsync | ErrorLocations.GetCacheResultAsync, errorKind };
                    yield return new object[] { ErrorLocations.BeginBuildAsync | ErrorLocations.GetCacheResultAsync | ErrorLocations.EndBuildAsync, errorKind };
                    yield return new object[] { ErrorLocations.BeginBuildAsync | ErrorLocations.EndBuildAsync, errorKind };
 
                    yield return new object[] { ErrorLocations.GetCacheResultAsync, errorKind };
                    yield return new object[] { ErrorLocations.GetCacheResultAsync | ErrorLocations.EndBuildAsync, errorKind };
 
                    yield return new object[] { ErrorLocations.EndBuildAsync, errorKind };
                }
            }
        }
 
        [Theory]
        [MemberData(nameof(CacheExceptionLocationsTestData))]
        public void EngineShouldHandleExceptionsFromCachePluginViaBuildParameters(ErrorLocations errorLocations, ErrorKind errorKind)
        {
            SetEnvironmentForErrorLocations(errorLocations, errorKind);
 
            var project = _env.CreateFile("1.proj", @$"
                    <Project>
                        <Target Name=`Build`>
                            <Message Text=`Hello World` Importance=`High` />
                        </Target>
                    </Project>".Cleanup());
 
            Helpers.BuildManagerSession? buildSession = null;
            MockLogger logger;
 
            try
            {
                buildSession = new Helpers.BuildManagerSession(
                    _env,
                    new BuildParameters
                    {
                        UseSynchronousLogging = true,
                        ProjectCacheDescriptor = ProjectCacheDescriptor.FromAssemblyPath(SamplePluginAssemblyPath.Value)
                    });
 
                logger = buildSession.Logger;
                var buildResult = buildSession.BuildProjectFile(project.Path);
 
                // Plugin construction, initialization, and query all end up throwing in BuildManager.ExecuteSubmission and thus
                // mark the submission as failed with exception.
                var exceptionsThatEndUpInBuildResult =
                    ErrorLocations.Constructor | ErrorLocations.BeginBuildAsync | ErrorLocations.GetCacheResultAsync;
 
                if ((exceptionsThatEndUpInBuildResult & errorLocations) != 0)
                {
                    buildResult.Exception.ShouldNotBeNull();
                    buildResult.Exception.ShouldBeOfType<ProjectCacheException>();
 
                    if (errorKind == ErrorKind.Exception)
                    {
                        buildResult.Exception.InnerException!.ShouldNotBeNull();
                        buildResult.Exception.InnerException!.Message.ShouldContain("Cache plugin exception from");
                    }
                    else
                    {
                        buildResult.Exception.InnerException.ShouldBeNull();
                    }
                }
 
                // BuildManager.EndBuild calls plugin.EndBuild, so if only plugin.EndBuild fails it means everything else passed,
                // so the build submission should be successful.
                if (errorLocations == ErrorLocations.EndBuildAsync)
                {
                    buildResult.ShouldHaveSucceeded();
                }
                else
                {
                    buildResult.ShouldHaveFailed();
                }
            }
            finally
            {
                // These exceptions prevent the creation of a plugin so there's no plugin to shutdown.
                var exceptionsThatPreventEndBuildFromThrowing = ErrorLocations.Constructor |
                                                                ErrorLocations.BeginBuildAsync;
 
                if ((errorLocations & exceptionsThatPreventEndBuildFromThrowing) != 0 ||
                    !errorLocations.HasFlag(ErrorLocations.EndBuildAsync))
                {
                    Should.NotThrow(() => buildSession!.Dispose());
                }
                else if (errorLocations.HasFlag(ErrorLocations.EndBuildAsync))
                {
                    var e = Should.Throw<ProjectCacheException>(() => buildSession!.Dispose());
 
                    if (errorKind == ErrorKind.Exception)
                    {
                        e.InnerException!.ShouldNotBeNull();
                        e.InnerException!.Message.ShouldContain("Cache plugin exception from EndBuildAsync");
                    }
                    else
                    {
                        e.InnerException.ShouldBeNull();
                    }
                }
                else
                {
                    throw new NotImplementedException();
                }
                buildSession?.Dispose();
            }
 
            logger.BuildFinishedEvents.First().Succeeded.ShouldBeFalse();
 
            // Plugin query must happen after plugin init. So if plugin init fails, then the plugin should not get queried.
            var exceptionsThatShouldPreventCacheQueryAndEndBuildAsync = ErrorLocations.Constructor | ErrorLocations.BeginBuildAsync;
 
            if ((exceptionsThatShouldPreventCacheQueryAndEndBuildAsync & errorLocations) != 0)
            {
                logger.FullLog.ShouldNotContain($"{AssemblyMockCache}: GetCacheResultAsync for");
                logger.FullLog.ShouldNotContain($"{AssemblyMockCache}: EndBuildAsync");
            }
            else
            {
                StringShouldContainSubstring(logger.FullLog, $"{AssemblyMockCache}: GetCacheResultAsync for", expectedOccurrences: 1);
                StringShouldContainSubstring(logger.FullLog, $"{AssemblyMockCache}: EndBuildAsync", expectedOccurrences: 1);
            }
 
            logger.FullLog.ShouldNotContain("Cache plugin exception from");
 
            if (errorKind == ErrorKind.LoggedError)
            {
                logger.FullLog.ShouldContain("Cache plugin logged error from");
            }
        }
 
        [Theory]
        [MemberData(nameof(CacheExceptionLocationsTestData))]
        public void EngineShouldHandleExceptionsFromCachePluginViaGraphBuild(ErrorLocations errorLocations, ErrorKind errorKind)
        {
            const ErrorLocations exceptionsThatShouldPreventCacheQueryAndEndBuildAsync = ErrorLocations.Constructor | ErrorLocations.BeginBuildAsync;
 
            SetEnvironmentForErrorLocations(errorLocations, errorKind);
 
            var graph = Helpers.CreateProjectGraph(
                _env,
                new Dictionary<int, int[]?>
                {
                    {1, new []{2}}
                },
                extraContentForAllNodes: @$"
<ItemGroup>
    <{ItemTypeNames.ProjectCachePlugin} Include=`{SamplePluginAssemblyPath.Value}` />
    <{ItemTypeNames.ProjectReferenceTargets} Include=`Build` Targets=`Build` />
</ItemGroup>
<Target Name=`Build`>
    <Message Text=`Hello World` Importance=`High` />
</Target>
");
 
            using var buildSession = new Helpers.BuildManagerSession(
                _env,
                new BuildParameters
                {
                    UseSynchronousLogging = true,
                    MaxNodeCount = 1
                });
 
            var logger = buildSession.Logger;
 
            GraphBuildResult? buildResult = null;
 
            try
            {
                buildResult = buildSession.BuildGraph(graph);
 
                if (!errorLocations.HasFlag(ErrorLocations.Constructor))
                {
                    logger.FullLog.ShouldContain("Loading the following project cache plugin:");
                }
 
                // EndBuildAsync isn't until the build manager is shut down, so the build result itself is successful if that's the only error.
                if (errorLocations == ErrorLocations.EndBuildAsync)
                {
                    buildResult.ShouldHaveSucceeded();
                }
                else
                {
                    buildResult.ShouldHaveFailed();
 
                    buildResult.Exception.ShouldBeOfType<ProjectCacheException>();
 
                    if (errorKind == ErrorKind.Exception)
                    {
                        buildResult.Exception.InnerException!.ShouldNotBeNull();
                        buildResult.Exception.InnerException!.Message.ShouldContain("Cache plugin exception from");
                    }
 
                    logger.FullLog.ShouldNotContain("Cache plugin exception from");
 
                    if (errorKind == ErrorKind.LoggedError)
                    {
                        logger.FullLog.ShouldContain("Cache plugin logged error from");
                    }
                }
            }
            finally
            {
                if (errorLocations.HasFlag(ErrorLocations.EndBuildAsync)
                    && (exceptionsThatShouldPreventCacheQueryAndEndBuildAsync & errorLocations) == 0)
                {
                    Should.Throw<ProjectCacheException>(() => buildSession.Dispose());
                }
                else
                {
                    Should.NotThrow(() => buildSession.Dispose());
                }
            }
 
            logger.BuildFinishedEvents.First().Succeeded.ShouldBeFalse();
 
            if ((exceptionsThatShouldPreventCacheQueryAndEndBuildAsync & errorLocations) != 0)
            {
                logger.FullLog.ShouldNotContain($"{AssemblyMockCache}: GetCacheResultAsync for");
                logger.FullLog.ShouldNotContain($"{AssemblyMockCache}: EndBuildAsync");
            }
            else
            {
                // There's two projects, so there should be two cache queries logged ... unless a cache queries throws an exception. That ends the build.
                var expectedQueryOccurrences = errorLocations.HasFlag(ErrorLocations.GetCacheResultAsync)
                    ? 1
                    : 2;
 
                StringShouldContainSubstring(logger.FullLog, $"{AssemblyMockCache}: GetCacheResultAsync for", expectedQueryOccurrences);
 
                StringShouldContainSubstring(logger.FullLog, $"{AssemblyMockCache}: EndBuildAsync", expectedOccurrences: 1);
            }
        }
 
        [Fact]
        public void EndBuildShouldGetCalledOnceWhenItThrowsExceptionsFromGraphBuilds()
        {
            var project = _env.CreateFile(
                "1.proj",
                @$"
                    <Project>
                        <ItemGroup>
                            <{ItemTypeNames.ProjectCachePlugin} Include=`{SamplePluginAssemblyPath.Value}` />
                        </ItemGroup>
                        <Target Name=`Build`>
                            <Message Text=`Hello EngineShouldHandleExceptionsFromCachePlugin` Importance=`High` />
                        </Target>
                    </Project>".Cleanup());
 
            SetEnvironmentForErrorLocations(ErrorLocations.EndBuildAsync, ErrorKind.Exception);
 
            var buildParameters = new BuildParameters
            {
                UseSynchronousLogging = true
            };
 
            using var buildSession = new Helpers.BuildManagerSession(_env, buildParameters);
            GraphBuildResult graphResult = buildSession.BuildGraph(new ProjectGraph(project.Path));
 
            Should.Throw<ProjectCacheException>(() => buildSession.Dispose()).InnerException!.Message.ShouldContain("Cache plugin exception from EndBuildAsync");
 
            StringShouldContainSubstring(buildSession.Logger.FullLog, $"{nameof(AssemblyMockCache)}: EndBuildAsync", expectedOccurrences: 1);
        }
 
        [Theory]
        [InlineData(false, false)]
        [InlineData(true, true)]
        public void CacheShouldBeQueriedInParallelDuringGraphBuilds(bool useSynchronousLogging, bool disableInprocNode)
        {
            var referenceNumbers = new[] { 2, 3, 4 };
 
            var testData = new GraphCacheResponse(
                new Dictionary<int, int[]?>
                {
                    {1, referenceNumbers}
                },
                referenceNumbers.ToDictionary(k => k, k => GraphCacheResponse.SuccessfulProxyTargetResult()));
 
            var graph = testData.CreateGraph(_env);
 
            var completedCacheRequests = new ConcurrentBag<int>();
            var task2Completion = new TaskCompletionSource<bool>();
            task2Completion.Task.IsCompleted.ShouldBeFalse();
 
            var cache = new DelegatingMockCache(
                async (buildRequest, _, _) =>
                {
                    var projectNumber = GetProjectNumber(buildRequest.ProjectFullPath);
 
                    try
                    {
                        if (projectNumber == 2)
                        {
                            await task2Completion.Task;
                        }
 
                        return testData.GetExpectedCacheResultForProjectNumber(projectNumber);
                    }
                    finally
                    {
                        completedCacheRequests.Add(projectNumber);
                    }
                });
 
            var buildParameters = new BuildParameters()
            {
                MaxNodeCount = NativeMethodsShared.GetLogicalCoreCount(),
                ProjectCacheDescriptor = ProjectCacheDescriptor.FromInstance(cache),
                UseSynchronousLogging = useSynchronousLogging,
                DisableInProcNode = disableInprocNode
            };
 
            using var buildSession = new Helpers.BuildManagerSession(_env, buildParameters);
 
            var task2 = BuildProjectFileAsync(2);
            var task3 = BuildProjectFileAsync(3);
            var task4 = BuildProjectFileAsync(4);
 
            task3.Result.ShouldHaveSucceeded();
            completedCacheRequests.ShouldContain(3);
            task4.Result.ShouldHaveSucceeded();
            completedCacheRequests.ShouldContain(4);
 
            // task 2 hasn't been instructed to finish yet
            task2.IsCompleted.ShouldBeFalse();
            completedCacheRequests.ShouldNotContain(2);
 
            task2Completion.SetResult(true);
 
            task2.Result.ShouldHaveSucceeded();
            completedCacheRequests.ShouldContain(2);
 
            var task1 = BuildProjectFileAsync(1);
            task1.Result.ShouldHaveSucceeded();
            completedCacheRequests.ShouldContain(1);
 
            Task<BuildResult> BuildProjectFileAsync(int projectNumber)
            {
                return buildSession.BuildProjectFileAsync(graph.ProjectNodes.First(n => GetProjectNumber(n) == projectNumber).ProjectInstance.FullPath);
            }
        }
 
        [Theory]
        [InlineData(false, false)]
        [InlineData(true, true)]
        public void ParallelStressTestForVsScenario(bool useSynchronousLogging, bool disableInprocNode)
        {
            var currentBuildEnvironment = BuildEnvironmentHelper.Instance;
 
            try
            {
                BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(
                    new BuildEnvironment(
                        currentBuildEnvironment.Mode,
                        currentBuildEnvironment.CurrentMSBuildExePath,
                        currentBuildEnvironment.RunningTests,
                        currentBuildEnvironment.RunningInMSBuildExe,
                        runningInVisualStudio: true,
                        visualStudioPath: currentBuildEnvironment.VisualStudioInstallRootDirectory));
 
                BuildManager.ProjectCacheDescriptors.ShouldBeEmpty();
 
                var referenceNumbers = Enumerable.Range(2, NativeMethodsShared.GetLogicalCoreCount() * 2).ToArray();
 
                var testData = new GraphCacheResponse(
                    new Dictionary<int, int[]?>
                    {
                        {1, referenceNumbers}
                    },
                    referenceNumbers.ToDictionary(k => k, k => GraphCacheResponse.SuccessfulProxyTargetResult()));
 
                var graph = testData.CreateGraph(_env);
 
                BuildManager.ProjectCacheDescriptors.ShouldHaveSingleItem();
 
                var solutionConfigurationGlobalProperty = CreateSolutionConfigurationProperty(graph.ProjectNodes);
 
                var buildParameters = new BuildParameters
                {
                    MaxNodeCount = NativeMethodsShared.GetLogicalCoreCount(),
                    UseSynchronousLogging = useSynchronousLogging,
                    DisableInProcNode = disableInprocNode
                };
 
                MockLogger logger;
                var buildResultTasks = new List<Task<BuildResult>>();
                using (var buildSession = new Helpers.BuildManagerSession(_env, buildParameters))
                {
                    logger = buildSession.Logger;
 
                    foreach (var node in graph.ProjectNodes.Where(n => referenceNumbers.Contains(GetProjectNumber(n))))
                    {
                        Task<BuildResult> buildResultTask = buildSession.BuildProjectFileAsync(
                            node.ProjectInstance.FullPath,
                            globalProperties:
                            new Dictionary<string, string>
                            {
                                { SolutionProjectGenerator.CurrentSolutionConfigurationContents, solutionConfigurationGlobalProperty }
                            });
 
                        buildResultTasks.Add(buildResultTask);
                    }
 
                    foreach (var buildResultTask in buildResultTasks)
                    {
                        buildResultTask.Result.ShouldHaveSucceeded();
                    }
 
                    buildSession.BuildProjectFile(graph.GraphRoots.First().ProjectInstance.FullPath).ShouldHaveSucceeded();
                }
 
                StringShouldContainSubstring(logger.FullLog, $"{AssemblyMockCache}: GetCacheResultAsync for", graph.ProjectNodes.Count);
            }
            finally
            {
                BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(currentBuildEnvironment);
                BuildManager.ProjectCacheDescriptors.Clear();
            }
        }
 
        [Theory]
        [InlineData(false, false)]
        [InlineData(true, true)]
        public void ParallelStressTest(bool useSynchronousLogging, bool disableInprocNode)
        {
            var referenceNumbers = Enumerable.Range(2, NativeMethodsShared.GetLogicalCoreCount() * 2).ToArray();
 
            var testData = new GraphCacheResponse(
                new Dictionary<int, int[]?>
                {
                    {1, referenceNumbers}
                },
                referenceNumbers.ToDictionary(k => k, k => GraphCacheResponse.SuccessfulProxyTargetResult()));
 
            var graph = testData.CreateGraph(_env);
            var cache = new InstanceMockCache(testData, TimeSpan.FromMilliseconds(50));
 
            var buildParameters = new BuildParameters()
            {
                MaxNodeCount = NativeMethodsShared.GetLogicalCoreCount(),
                ProjectCacheDescriptor = ProjectCacheDescriptor.FromInstance(cache),
                UseSynchronousLogging = useSynchronousLogging,
                DisableInProcNode = disableInprocNode
            };
 
            MockLogger logger;
            GraphBuildResult graphResult;
            using (var buildSession = new Helpers.BuildManagerSession(_env, buildParameters))
            {
                logger = buildSession.Logger;
                graphResult = buildSession.BuildGraph(graph);
            }
 
            graphResult.ShouldHaveSucceeded();
            cache.QueryStartStops.Count.ShouldBe(graph.ProjectNodes.Count * 2);
        }
 
        [Fact]
        // Schedules different requests for the same BuildRequestConfiguration in parallel.
        // The first batch of the requests are cache misses, the second batch are cache hits via proxy builds.
        // The first batch is delayed so it starts intermingling with the second batch.
        // This test ensures that scheduling proxy builds on the inproc node works nicely within the Scheduler
        // if the BuildRequestConfigurations for those proxy builds have built before (or are still building) on
        // the out of proc node.
        // More details: https://github.com/dotnet/msbuild/pull/6635
        public void ProxyCacheHitsOnPreviousCacheMissesShouldWork()
        {
            var cacheNotApplicableTarget = "NATarget";
            var cacheHitTarget = "CacheHitTarget";
            var proxyTarget = "ProxyTarget";
 
            var project =
@$"
<Project>
    <Target Name='{cacheNotApplicableTarget}'>
        <Exec Command=`{Helpers.GetSleepCommand(TimeSpan.FromMilliseconds(200))}` />
        <Message Text='{cacheNotApplicableTarget} in $(MSBuildThisFile)' />
    </Target>
 
    <Target Name='{cacheHitTarget}'>
        <Message Text='{cacheHitTarget} in $(MSBuildThisFile)' />
    </Target>
 
    <Target Name='{proxyTarget}'>
        <Message Text='{proxyTarget} in $(MSBuildThisFile)' />
    </Target>
</Project>
".Cleanup();
 
            var projectPaths = Enumerable.Range(0, NativeMethodsShared.GetLogicalCoreCount())
                .Select(i => _env.CreateFile($"project{i}.proj", project).Path)
                .ToArray();
 
            var cacheHitCount = 0;
            var nonCacheHitCount = 0;
 
            var buildParameters = new BuildParameters
            {
                ProjectCacheDescriptor = ProjectCacheDescriptor.FromInstance(
                    new ConfigurableMockCache
                    {
                        GetCacheResultImplementation = (request, _, _) =>
                        {
                            var projectFile = request.ProjectFullPath;
 
                            if (request.TargetNames.Contains(cacheNotApplicableTarget))
                            {
                                Interlocked.Increment(ref nonCacheHitCount);
                                return Task.FromResult(CacheResult.IndicateNonCacheHit(CacheResultType.CacheNotApplicable));
                            }
                            else
                            {
                                Interlocked.Increment(ref cacheHitCount);
                                return Task.FromResult(
                                    CacheResult.IndicateCacheHit(
                                        new ProxyTargets(new Dictionary<string, string> { { proxyTarget, cacheHitTarget } })));
                            }
                        }
                    }),
                MaxNodeCount = NativeMethodsShared.GetLogicalCoreCount()
            };
 
            MockLogger logger;
            using (var buildSession = new Helpers.BuildManagerSession(_env, buildParameters))
            {
                logger = buildSession.Logger;
 
                var buildRequests = new List<(string, string)>();
                buildRequests.AddRange(projectPaths.Select(r => (r, cacheNotApplicableTarget)));
                buildRequests.AddRange(projectPaths.Select(r => (r, cacheHitTarget)));
 
                var buildTasks = new List<Task<BuildResult>>();
                foreach (var (projectPath, target) in buildRequests)
                {
                    buildTasks.Add(buildSession.BuildProjectFileAsync(projectPath, new[] { target }));
                }
 
                foreach (var buildResult in buildTasks.Select(buildTask => buildTask.Result))
                {
                    buildResult.Exception.ShouldBeNull();
                    buildResult.ShouldHaveSucceeded();
                }
            }
 
            logger.ProjectStartedEvents.Count.ShouldBe(2 * projectPaths.Length);
 
            cacheHitCount.ShouldBe(projectPaths.Length);
            nonCacheHitCount.ShouldBe(projectPaths.Length);
        }
 
        private static void StringShouldContainSubstring(string aString, string substring, int expectedOccurrences)
        {
            aString.ShouldContain(substring);
            Regex.Matches(aString, substring).Count.ShouldBe(expectedOccurrences);
        }
 
        private void SetEnvironmentForErrorLocations(ErrorLocations errorLocations, ErrorKind errorKind)
        {
            foreach (var enumValue in Enum.GetValues(typeof(ErrorLocations)))
            {
                var typedValue = (ErrorLocations)enumValue;
                if (errorLocations.HasFlag(typedValue))
                {
                    var exceptionLocation = typedValue.ToString();
                    _env.SetEnvironmentVariable(exceptionLocation, errorKind.ToString());
                    _output.WriteLine($"Set exception location: {exceptionLocation}");
                }
            }
        }
    }
}