diff --git a/MSBuild.sln b/MSBuild.sln
index 966817afd12..f58cad8b0d6 100644
--- a/MSBuild.sln
+++ b/MSBuild.sln
@@ -67,6 +67,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MSBuild.Bootstrap", "src\MS
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Build.UnGAC", "src\Package\Microsoft.Build.UnGAC\Microsoft.Build.UnGAC.csproj", "{B60173F0-F9F0-4688-9DF8-9ADDD57BD45F}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProjectCachePlugin", "src\Samples\ProjectCachePlugin\ProjectCachePlugin.csproj", "{F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -866,6 +868,36 @@ Global
{B60173F0-F9F0-4688-9DF8-9ADDD57BD45F}.Release-MONO|x64.Build.0 = Release-MONO|x64
{B60173F0-F9F0-4688-9DF8-9ADDD57BD45F}.Release-MONO|x86.ActiveCfg = Release-MONO|Any CPU
{B60173F0-F9F0-4688-9DF8-9ADDD57BD45F}.Release-MONO|x86.Build.0 = Release-MONO|Any CPU
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Debug|x64.ActiveCfg = Debug|x64
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Debug|x64.Build.0 = Debug|x64
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Debug|x86.Build.0 = Debug|Any CPU
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Debug-MONO|Any CPU.ActiveCfg = Debug-MONO|Any CPU
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Debug-MONO|Any CPU.Build.0 = Debug-MONO|Any CPU
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Debug-MONO|x64.ActiveCfg = Debug-MONO|x64
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Debug-MONO|x64.Build.0 = Debug-MONO|x64
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Debug-MONO|x86.ActiveCfg = Debug-MONO|Any CPU
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Debug-MONO|x86.Build.0 = Debug-MONO|Any CPU
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.MachineIndependent|Any CPU.ActiveCfg = MachineIndependent|Any CPU
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.MachineIndependent|Any CPU.Build.0 = MachineIndependent|Any CPU
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.MachineIndependent|x64.ActiveCfg = MachineIndependent|x64
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.MachineIndependent|x64.Build.0 = MachineIndependent|x64
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.MachineIndependent|x86.ActiveCfg = MachineIndependent|Any CPU
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.MachineIndependent|x86.Build.0 = MachineIndependent|Any CPU
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Release|x64.ActiveCfg = Release|x64
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Release|x64.Build.0 = Release|x64
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Release|x86.ActiveCfg = Release|Any CPU
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Release|x86.Build.0 = Release|Any CPU
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Release-MONO|Any CPU.ActiveCfg = Release-MONO|Any CPU
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Release-MONO|Any CPU.Build.0 = Release-MONO|Any CPU
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Release-MONO|x64.ActiveCfg = Release-MONO|x64
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Release-MONO|x64.Build.0 = Release-MONO|x64
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Release-MONO|x86.ActiveCfg = Release-MONO|Any CPU
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943}.Release-MONO|x86.Build.0 = Release-MONO|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -883,6 +915,7 @@ Global
{EDBFE32E-F264-4F01-97C3-B58F8B9165C9} = {9BAD9352-DEFB-45E5-B8A4-4816B9B22A33}
{3D67E4FF-6EC6-4FE7-82F1-0DACE1E399A7} = {9BAD9352-DEFB-45E5-B8A4-4816B9B22A33}
{B60173F0-F9F0-4688-9DF8-9ADDD57BD45F} = {9BAD9352-DEFB-45E5-B8A4-4816B9B22A33}
+ {F47E1A0A-7D81-40CF-B8B3-A0F4B5ADE943} = {760FF85D-8BEB-4992-8095-A9678F88FD47}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F948D667-14E3-4F98-BA50-3F3C948BF4C2}
diff --git a/src/Build.UnitTests/Graph/IsolateProjects_Tests.cs b/src/Build.UnitTests/Graph/IsolateProjects_Tests.cs
index 7cc2060da0f..62cc2016a58 100644
--- a/src/Build.UnitTests/Graph/IsolateProjects_Tests.cs
+++ b/src/Build.UnitTests/Graph/IsolateProjects_Tests.cs
@@ -473,8 +473,9 @@ public void SkippedTargetsShouldNotTriggerCacheMissEnforcement()
".Cleanup()).Path;
_buildParametersPrototype.IsolateProjects.ShouldBeTrue();
+ var buildParameters = _buildParametersPrototype.Clone();
- using (var buildManagerSession = new Helpers.BuildManagerSession(_env, _buildParametersPrototype))
+ using (var buildManagerSession = new Helpers.BuildManagerSession(_env, buildParameters))
{
// seed caches with results from the reference
buildManagerSession.BuildProjectFile(referenceFile).OverallResult.ShouldBe(BuildResultCode.Success);
diff --git a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj
index fdeaf730a1b..9b45fb61b7e 100644
--- a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj
+++ b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj
@@ -40,6 +40,12 @@
+
+ TargetFramework=$(FullFrameworkTFM)
+ TargetFramework=$(FullFrameworkTFM)
+ TargetFramework=netcoreapp2.1
+
+
diff --git a/src/Build.UnitTests/ProjectCache/ProjectCacheTests.cs b/src/Build.UnitTests/ProjectCache/ProjectCacheTests.cs
new file mode 100644
index 00000000000..c7baf563774
--- /dev/null
+++ b/src/Build.UnitTests/ProjectCache/ProjectCacheTests.cs
@@ -0,0 +1,882 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+#nullable enable
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+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.ProjectCacheItems.ShouldBeEmpty();
+ _env.WithInvariant(new CustomConditionInvariant(() => BuildManager.ProjectCacheItems.Count == 0));
+ }
+
+ public void Dispose()
+ {
+ _env.Dispose();
+ }
+
+ private static readonly Lazy SamplePluginAssemblyPath =
+ new Lazy(
+ () =>
+ {
+ return Directory.EnumerateFiles(
+ Path.GetFullPath(
+ Path.Combine(
+ BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory,
+ "..",
+ "..",
+ "..",
+ "Samples",
+ "ProjectCachePlugin")),
+ "ProjectCachePlugin.dll",
+ SearchOption.AllDirectories).First();
+ });
+
+ public class GraphCacheResponse
+ {
+ public const string CacheHitByProxy = nameof(CacheHitByProxy);
+ public const string CacheHitByTargetResult = nameof(CacheHitByTargetResult);
+
+ private static readonly string P2PTargets =
+ @$"
+
+
+ <{ItemTypeNames.ProjectCachePlugin} Include=`{SamplePluginAssemblyPath.Value}` />
+
+
+
+
+
+
+
+
+ Count()) != 0 and !Exists(%(ReferenceReturns.File))` />
+
+
+
+
+
+
+
+
+
+
+ ";
+
+ private Dictionary GraphEdges { get; }
+
+ public Dictionary NonCacheMissResults { get; }
+
+ public GraphCacheResponse(Dictionary graphEdges, Dictionary? nonCacheMissResults = null)
+ {
+ GraphEdges = graphEdges;
+ NonCacheMissResults = nonCacheMissResults ?? new Dictionary();
+ }
+
+ public ProjectGraph CreateGraph(TestEnvironment env)
+ {
+ return Helpers.CreateProjectGraph(
+ env,
+ GraphEdges,
+ null,
+ P2PTargets);
+ }
+
+ public static CacheResult SuccessfulProxyTargetResult()
+ {
+ return CacheResult.IndicateCacheHit(
+ new ProxyTargets(
+ new Dictionary
+ {
+ {"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
+ {
+ {"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 base.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.CacheError => 'E',
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ }
+ }
+ }
+
+ [Flags]
+ public enum ExceptionLocations
+ {
+ Constructor = 1 << 0,
+ BeginBuildAsync = 1 << 1,
+ GetCacheResultAsync = 1 << 2,
+ EndBuildAsync = 1 << 3
+ }
+
+ public class MockProjectCache : ProjectCacheBase
+ {
+ private readonly GraphCacheResponse? _testData;
+ public ConcurrentQueue Requests { get; } = new ConcurrentQueue();
+
+ public bool BeginBuildCalled { get; set; }
+ public bool EndBuildCalled { get; set; }
+
+ public MockProjectCache(GraphCacheResponse? testData = null)
+ {
+ _testData = testData;
+ }
+
+ public override Task BeginBuildAsync(CacheContext context, PluginLoggerBase logger, CancellationToken cancellationToken)
+ {
+ logger.LogMessage("MockCache: BeginBuildAsync", MessageImportance.High);
+
+ BeginBuildCalled = true;
+
+ return Task.CompletedTask;
+ }
+
+ public override Task GetCacheResultAsync(
+ BuildRequestData buildRequest,
+ PluginLoggerBase logger,
+ CancellationToken cancellationToken)
+ {
+ Requests.Enqueue(buildRequest);
+ logger.LogMessage($"MockCache: GetCacheResultAsync for {buildRequest.ProjectFullPath}", MessageImportance.High);
+
+ return
+ Task.FromResult(
+ _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 SuccessfulGraphs
+ {
+ get
+ {
+ yield return new GraphCacheResponse(
+ new Dictionary
+ {
+ {1, null!}
+ });
+
+ yield return new GraphCacheResponse(
+ new Dictionary
+ {
+ {1, null!}
+ },
+ new Dictionary
+ {
+ {1, GraphCacheResponse.SuccessfulProxyTargetResult()}
+ });
+
+ yield return new GraphCacheResponse(
+ new Dictionary
+ {
+ {1, null!}
+ },
+ new Dictionary
+ {
+ {1, GraphCacheResponse.SuccessfulTargetResult(1, "1.proj")}
+ });
+
+ yield return new GraphCacheResponse(
+ new Dictionary
+ {
+ {1, new[] {2}}
+ });
+
+ yield return new GraphCacheResponse(
+ new Dictionary
+ {
+ {1, new[] {2}}
+ },
+ new Dictionary
+ {
+ {2, GraphCacheResponse.SuccessfulProxyTargetResult()}
+ });
+
+ yield return new GraphCacheResponse(
+ new Dictionary
+ {
+ {1, new[] {2}}
+ },
+ new Dictionary
+ {
+ {2, GraphCacheResponse.SuccessfulTargetResult(2, "2.proj")}
+ });
+
+ yield return new GraphCacheResponse(
+ new Dictionary
+ {
+ {1, new[] {2}}
+ },
+ new Dictionary
+ {
+ {1, GraphCacheResponse.SuccessfulProxyTargetResult()},
+ {2, GraphCacheResponse.SuccessfulTargetResult(2, "2.proj")}
+ });
+
+ yield return new GraphCacheResponse(
+ new Dictionary
+ {
+ {1, new[] {2, 3, 7}},
+ {2, new[] {4}},
+ {3, new[] {4}},
+ {4, new[] {5, 6, 7}}
+ });
+ }
+ }
+
+ public static IEnumerable