diff --git a/src/Build.UnitTests/BackEnd/RequestedProjectState_Tests.cs b/src/Build.UnitTests/BackEnd/RequestedProjectState_Tests.cs new file mode 100644 index 00000000000..cc0b1f39faf --- /dev/null +++ b/src/Build.UnitTests/BackEnd/RequestedProjectState_Tests.cs @@ -0,0 +1,208 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using FluentAssertions; +using Microsoft.Build.Execution; +using Shouldly; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + public class RequestedProjectState_Tests + { + [Fact] + public void DeepCloneEmpty() + { + RequestedProjectState state = new(); + RequestedProjectState clone = state.DeepClone(); + + clone.PropertyFilters.Should().BeNull(); + clone.ItemFilters.Should().BeNull(); + } + + [Fact] + public void DeepCloneProperties() + { + List properties = ["prop1", "prop2"]; + RequestedProjectState state = new() + { + PropertyFilters = properties, + }; + RequestedProjectState clone = state.DeepClone(); + + clone.PropertyFilters.Should().BeEquivalentTo(properties); + clone.ItemFilters.Should().BeNull(); + + // Mutating the original instance is not reflected in the clone. + properties.Add("prop3"); + clone.PropertyFilters.Count.Should().NotBe(properties.Count); + } + + [Fact] + public void DeepCloneItemsNoMetadata() + { + Dictionary> items = new() + { + { "item1", null! }, + { "item2", null! }, + }; + RequestedProjectState state = new() + { + ItemFilters = items, + }; + RequestedProjectState clone = state.DeepClone(); + + clone.PropertyFilters.Should().BeNull(); + clone.ItemFilters.Should().BeEquivalentTo(items); + + // Mutating the original instance is not reflected in the clone. + items.Add("item3", null!); + clone.ItemFilters.Count.Should().NotBe(items.Count); + } + + [Fact] + public void DeepCloneItemsWithMetadata() + { + Dictionary> items = new() + { + { "item1", ["metadatum1", "metadatum2"] }, + { "item2", ["metadatum3"] }, + }; + RequestedProjectState state = new() + { + ItemFilters = items, + }; + RequestedProjectState clone = state.DeepClone(); + + clone.PropertyFilters.Should().BeNull(); + clone.ItemFilters.Should().BeEquivalentTo(items); + + // Mutating the original instance is not reflected in the clone. + items.Add("item3", ["metadatum4"]); + clone.ItemFilters.Count.Should().NotBe(items.Count); + } + + [Fact] + public void IsSubsetOfEmpty() + { + RequestedProjectState state1 = new(); + RequestedProjectState state2 = new(); + + // Empty instances are subsets of each other. + state1.IsSubsetOf(state2).Should().BeTrue(); + state2.IsSubsetOf(state1).Should().BeTrue(); + + state1.PropertyFilters = ["prop1"]; + state1.ItemFilters = new Dictionary>() + { + { "item1", null! }, + }; + + // Non-empty instance is a subset of empty instance but not the other way round. + state1.IsSubsetOf(state2).Should().BeTrue(); + state2.IsSubsetOf(state1).Should().BeFalse(); + } + + [Fact] + public void IsSubsetOfProperties() + { + RequestedProjectState state1 = new() + { + PropertyFilters = ["prop1"], + }; + RequestedProjectState state2 = new() + { + PropertyFilters = ["prop1", "prop2"], + }; + + // "prop1" is a subset of "prop1", "prop2". + state1.IsSubsetOf(state2).Should().BeTrue(); + state2.IsSubsetOf(state1).Should().BeFalse(); + + state1.PropertyFilters.Add("prop3"); + + // Disjoint sets are not subsets of each other. + state1.IsSubsetOf(state2).Should().BeFalse(); + state2.IsSubsetOf(state1).Should().BeFalse(); + + state1.PropertyFilters.Clear(); + + // Empty props is a subset of anything. + state1.IsSubsetOf(state2).Should().BeTrue(); + state2.IsSubsetOf(state1).Should().BeFalse(); + } + + [Fact] + public void IsSubsetOfItemsNoMetadata() + { + RequestedProjectState state1 = new() + { + ItemFilters = new Dictionary>() + { + { "item1", null! }, + }, + }; + RequestedProjectState state2 = new() + { + ItemFilters = new Dictionary>() + { + { "item1", null! }, + { "item2", null! }, + }, + }; + + // "item1" is a subset of "item1", "item2". + state1.IsSubsetOf(state2).Should().BeTrue(); + state2.IsSubsetOf(state1).Should().BeFalse(); + + state1.ItemFilters.Add("item3", null!); + + // Disjoint sets are not subsets of each other. + state1.IsSubsetOf(state2).Should().BeFalse(); + state2.IsSubsetOf(state1).Should().BeFalse(); + + state1.ItemFilters.Clear(); + + // Empty items is a subset of anything. + state1.IsSubsetOf(state2).Should().BeTrue(); + state2.IsSubsetOf(state1).Should().BeFalse(); + } + + [Fact] + public void IsSubsetOfItemsWithMetadata() + { + RequestedProjectState state1 = new() + { + ItemFilters = new Dictionary>() + { + { "item1", ["metadatum1"] }, + }, + }; + RequestedProjectState state2 = new() + { + ItemFilters = new Dictionary>() + { + { "item1", null! }, + }, + }; + + // "item1" with "metadatum1" is a subset of "item1" with no metadata filter. + state1.IsSubsetOf(state2).Should().BeTrue(); + state2.IsSubsetOf(state1).Should().BeFalse(); + + state2.ItemFilters["item1"] = ["metadatum2"]; + + // Disjoint metadata filters are not subsets of each other. + state1.IsSubsetOf(state2).Should().BeFalse(); + state2.IsSubsetOf(state1).Should().BeFalse(); + + state1.ItemFilters["item1"] = []; + + // Empty metadata filter is a subset of any other metadata filter. + state1.IsSubsetOf(state2).Should().BeTrue(); + state2.IsSubsetOf(state1).Should().BeFalse(); + } + } +} diff --git a/src/Build.UnitTests/BackEnd/ResultsCache_Tests.cs b/src/Build.UnitTests/BackEnd/ResultsCache_Tests.cs index da24e32046c..8448538e7c7 100644 --- a/src/Build.UnitTests/BackEnd/ResultsCache_Tests.cs +++ b/src/Build.UnitTests/BackEnd/ResultsCache_Tests.cs @@ -3,8 +3,12 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Xml; using Microsoft.Build.BackEnd; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; using Microsoft.Build.Execution; using Microsoft.Build.Framework; using Microsoft.Build.Internal; @@ -265,6 +269,10 @@ public void TestCacheOnDifferentBuildFlagsPerRequest_ProvideSubsetOfStateAfterBu int nodeRequestId = 0; int configurationId = 1; + RequestedProjectState requestedProjectState1 = new() + { + PropertyFilters = ["property1", "property2"], + }; BuildRequest requestWithSubsetFlag1 = new BuildRequest( submissionId, nodeRequestId, @@ -273,8 +281,13 @@ public void TestCacheOnDifferentBuildFlagsPerRequest_ProvideSubsetOfStateAfterBu null /* hostServices */, BuildEventContext.Invalid /* parentBuildEventContext */, null /* parentRequest */, - BuildRequestDataFlags.ProvideSubsetOfStateAfterBuild); + BuildRequestDataFlags.ProvideSubsetOfStateAfterBuild, + requestedProjectState1); + RequestedProjectState requestedProjectState2 = new() + { + PropertyFilters = ["property1"], + }; BuildRequest requestWithSubsetFlag2 = new BuildRequest( submissionId, nodeRequestId, @@ -283,18 +296,31 @@ public void TestCacheOnDifferentBuildFlagsPerRequest_ProvideSubsetOfStateAfterBu null /* hostServices */, BuildEventContext.Invalid /* parentBuildEventContext */, null /* parentRequest */, - BuildRequestDataFlags.ProvideSubsetOfStateAfterBuild); + BuildRequestDataFlags.ProvideSubsetOfStateAfterBuild, + requestedProjectState2); BuildResult resultForRequestWithSubsetFlag1 = new(requestWithSubsetFlag1); resultForRequestWithSubsetFlag1.AddResultsForTarget(targetName, BuildResultUtilities.GetEmptySucceedingTargetResult()); + + using TextReader textReader = new StringReader(@" + + + Value1 + Value2 + + + "); + using XmlReader xmlReader = XmlReader.Create(textReader); + resultForRequestWithSubsetFlag1.ProjectStateAfterBuild = new ProjectInstance(ProjectRootElement.Create(xmlReader)).FilteredCopy(requestedProjectState1); + ResultsCache cache = new(); cache.AddResult(resultForRequestWithSubsetFlag1); ResultsCacheResponse cachedResponseWithSubsetFlag1 = cache.SatisfyRequest( - requestWithSubsetFlag1, - new List(), - new List(new string[] { targetName }), - skippedResultsDoNotCauseCacheMiss: false); + requestWithSubsetFlag1, + new List(), + new List(new string[] { targetName }), + skippedResultsDoNotCauseCacheMiss: false); ResultsCacheResponse cachedResponseWithSubsetFlag2 = cache.SatisfyRequest( requestWithSubsetFlag2, @@ -302,11 +328,13 @@ public void TestCacheOnDifferentBuildFlagsPerRequest_ProvideSubsetOfStateAfterBu new List(new string[] { targetName }), skippedResultsDoNotCauseCacheMiss: false); - // It was agreed not to return cache results if ProvideSubsetOfStateAfterBuild is passed, - // because RequestedProjectState may have different user filters defined. - // It is more reliable to ignore the cached value. - Assert.Equal(ResultsCacheResponseType.NotSatisfied, cachedResponseWithSubsetFlag1.Type); - Assert.Equal(ResultsCacheResponseType.NotSatisfied, cachedResponseWithSubsetFlag2.Type); + Assert.Equal(ResultsCacheResponseType.Satisfied, cachedResponseWithSubsetFlag1.Type); + Assert.Equal("Value1", cachedResponseWithSubsetFlag1.Results.ProjectStateAfterBuild.GetPropertyValue("property1")); + Assert.Equal("Value2", cachedResponseWithSubsetFlag1.Results.ProjectStateAfterBuild.GetPropertyValue("property2")); + + Assert.Equal(ResultsCacheResponseType.Satisfied, cachedResponseWithSubsetFlag2.Type); + Assert.Equal("Value1", cachedResponseWithSubsetFlag2.Results.ProjectStateAfterBuild.GetPropertyValue("property1")); + Assert.Equal("", cachedResponseWithSubsetFlag2.Results.ProjectStateAfterBuild.GetPropertyValue("property2")); } [Fact]