File: Definition\ProjectEvaluationContext_Tests.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.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using Microsoft.Build.BackEnd.SdkResolution;
using Microsoft.Build.Construction;
using Microsoft.Build.Definition;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Evaluation.Context;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Unittest;
using Shouldly;
using Xunit;
using SdkResult = Microsoft.Build.BackEnd.SdkResolution.SdkResult;
 
#nullable disable
 
namespace Microsoft.Build.UnitTests.Definition
{
    /// <summary>
    ///     Tests some manipulations of Project and ProjectCollection that require dealing with internal data.
    /// </summary>
    public class ProjectEvaluationContext_Tests : IDisposable
    {
        public ProjectEvaluationContext_Tests()
        {
            _env = TestEnvironment.Create();
 
            _resolver = new SdkUtilities.ConfigurableMockSdkResolver(
                new Dictionary<string, SdkResult>
                {
                    {"foo", new SdkResult(new SdkReference("foo", "1.0.0", null), "path", "1.0.0", null) },
                    {"bar", new SdkResult(new SdkReference("bar", "1.0.0", null), "path", "1.0.0", null) }
                });
        }
 
        public void Dispose()
        {
            _env.Dispose();
        }
 
        private readonly SdkUtilities.ConfigurableMockSdkResolver _resolver;
        private readonly TestEnvironment _env;
 
        private static void SetResolverForContext(EvaluationContext context, SdkResolver resolver)
        {
            var sdkService = (SdkResolverService)context.SdkResolverService;
 
            sdkService.InitializeForTests(null, new List<SdkResolver> { resolver });
        }
 
        [Theory]
        [InlineData(EvaluationContext.SharingPolicy.Shared)]
        [InlineData(EvaluationContext.SharingPolicy.SharedSDKCache)]
        [InlineData(EvaluationContext.SharingPolicy.Isolated)]
        public void SharedContextShouldGetReusedWhereasIsolatedContextShouldNot(EvaluationContext.SharingPolicy policy)
        {
            var previousContext = EvaluationContext.Create(policy);
 
            for (var i = 0; i < 10; i++)
            {
                var currentContext = previousContext.ContextForNewProject();
 
                if (i == 0)
                {
                    currentContext.ShouldBeSameAs(previousContext, "first usage context was not the same as the initial context");
                }
                else
                {
                    switch (policy)
                    {
                        case EvaluationContext.SharingPolicy.Shared:
                            currentContext.ShouldBeSameAs(previousContext, $"Shared policy: usage {i} was not the same as usage {i - 1}");
                            break;
                        case EvaluationContext.SharingPolicy.SharedSDKCache:
                            currentContext.ShouldNotBeSameAs(previousContext, $"SharedSDKCache policy: usage {i} was the same as usage {i - 1}");
                            break;
                        case EvaluationContext.SharingPolicy.Isolated:
                            currentContext.ShouldNotBeSameAs(previousContext, $"Isolated policy: usage {i} was the same as usage {i - 1}");
                            break;
                        default:
                            throw new ArgumentOutOfRangeException(nameof(policy), policy, null);
                    }
                }
 
                previousContext = currentContext;
            }
        }
 
        [Fact]
        public void PassedInFileSystemShouldBeReusedInSharedContext()
        {
            var projectFiles = new[]
            {
                _env.CreateFile("1.proj", @"<Project> <PropertyGroup Condition=`Exists('1.file')`></PropertyGroup> </Project>".Cleanup()).Path,
                _env.CreateFile("2.proj", @"<Project> <PropertyGroup Condition=`Exists('2.file')`></PropertyGroup> </Project>".Cleanup()).Path
            };
 
            var projectCollection = _env.CreateProjectCollection().Collection;
            var fileSystem = new Helpers.LoggingFileSystem();
            var evaluationContext = EvaluationContext.Create(EvaluationContext.SharingPolicy.Shared, fileSystem);
 
            foreach (var projectFile in projectFiles)
            {
                Project.FromFile(
                    projectFile,
                    new ProjectOptions
                    {
                        ProjectCollection = projectCollection,
                        EvaluationContext = evaluationContext
                    });
            }
 
            fileSystem.ExistenceChecks.OrderBy(kvp => kvp.Key)
                .ShouldBe(
                    new Dictionary<string, int>
                    {
                        {Path.Combine(_env.DefaultTestDirectory.Path, "1.file"), 1},
                        {Path.Combine(_env.DefaultTestDirectory.Path, "2.file"), 1}
                    }.OrderBy(kvp => kvp.Key));
 
            fileSystem.FileOrDirectoryExistsCalls.ShouldBe(2);
        }
 
        [Theory]
        [InlineData(EvaluationContext.SharingPolicy.SharedSDKCache)]
        [InlineData(EvaluationContext.SharingPolicy.Isolated)]
        public void NonSharedContextShouldNotSupportBeingPassedAFileSystem(EvaluationContext.SharingPolicy policy)
        {
            var fileSystem = new Helpers.LoggingFileSystem();
            Should.Throw<ArgumentException>(() => EvaluationContext.Create(policy, fileSystem));
        }
 
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void EvaluationShouldUseDirectoryCache(bool useProjectInstance)
        {
            var projectFile = _env.CreateFile("1.proj", @"<Project> <Import Project='1.file' Condition=`Exists('1.file')`/> <ItemGroup><Compile Include='*.cs'/></ItemGroup> </Project>".Cleanup()).Path;
 
            var projectCollection = _env.CreateProjectCollection().Collection;
            var directoryCacheFactory = new Helpers.LoggingDirectoryCacheFactory();
 
            int expectedEvaluationId;
            if (useProjectInstance)
            {
                var projectInstance = ProjectInstance.FromFile(
                    projectFile,
                    new ProjectOptions
                    {
                        ProjectCollection = projectCollection,
                        DirectoryCacheFactory = directoryCacheFactory,
                    });
                expectedEvaluationId = projectInstance.EvaluationId;
            }
            else
            {
                var project = Project.FromFile(
                    projectFile,
                    new ProjectOptions
                    {
                        ProjectCollection = projectCollection,
                        DirectoryCacheFactory = directoryCacheFactory,
                    });
                expectedEvaluationId = project.LastEvaluationId;
            }
 
            directoryCacheFactory.DirectoryCaches.Count.ShouldBe(1);
            var directoryCache = directoryCacheFactory.DirectoryCaches[0];
 
            directoryCache.EvaluationId.ShouldBe(expectedEvaluationId);
 
            directoryCache.ExistenceChecks.OrderBy(kvp => kvp.Key).ShouldBe(
                new Dictionary<string, int>
                {
                    { _env.DefaultTestDirectory.Path, 1},
                    { Path.Combine(_env.DefaultTestDirectory.Path, "1.file"), 2 }
                }.OrderBy(kvp => kvp.Key));
            directoryCache.Enumerations.ShouldBe(
                new Dictionary<string, int>
                {
                    { _env.DefaultTestDirectory.Path, 1 }
                });
        }
 
        [Theory]
        [InlineData(EvaluationContext.SharingPolicy.Shared)]
        [InlineData(EvaluationContext.SharingPolicy.SharedSDKCache)]
        [InlineData(EvaluationContext.SharingPolicy.Isolated)]
        public void ReevaluationShouldNotReuseInitialContext(EvaluationContext.SharingPolicy policy)
        {
            try
            {
                EvaluationContext.TestOnlyHookOnCreate = c => SetResolverForContext(c, _resolver);
 
                var collection = _env.CreateProjectCollection().Collection;
 
                var context = EvaluationContext.Create(policy);
 
                using var xmlReader = XmlReader.Create(new StringReader("<Project Sdk=\"foo\"></Project>"));
                var project = Project.FromXmlReader(
                    xmlReader,
                    new ProjectOptions
                    {
                        ProjectCollection = collection,
                        EvaluationContext = context,
                        LoadSettings = ProjectLoadSettings.IgnoreMissingImports
                    });
 
                _resolver.ResolvedCalls["foo"].ShouldBe(1);
 
                project.AddItem("a", "b");
 
                project.ReevaluateIfNecessary();
 
                _resolver.ResolvedCalls["foo"].ShouldBe(2);
            }
            finally
            {
                EvaluationContext.TestOnlyHookOnCreate = null;
            }
        }
 
        [Theory]
        [InlineData(EvaluationContext.SharingPolicy.Shared)]
        [InlineData(EvaluationContext.SharingPolicy.SharedSDKCache)]
        [InlineData(EvaluationContext.SharingPolicy.Isolated)]
        public void ProjectInstanceShouldRespectSharingPolicy(EvaluationContext.SharingPolicy policy)
        {
            try
            {
                var seenContexts = new HashSet<EvaluationContext>();
 
                EvaluationContext.TestOnlyHookOnCreate = c => seenContexts.Add(c);
 
                var collection = _env.CreateProjectCollection().Collection;
 
                var context = EvaluationContext.Create(policy);
 
                const int numIterations = 10;
                for (int i = 0; i < numIterations; i++)
                {
                    ProjectInstance.FromProjectRootElement(
                        ProjectRootElement.Create(),
                        new ProjectOptions
                        {
                            ProjectCollection = collection,
                            EvaluationContext = context,
                            LoadSettings = ProjectLoadSettings.IgnoreMissingImports
                        });
                }
 
                int expectedNumContexts = policy == EvaluationContext.SharingPolicy.Shared ? 1 : numIterations;
 
                seenContexts.Count.ShouldBe(expectedNumContexts);
                seenContexts.ShouldAllBe(c => c.Policy == policy);
            }
            finally
            {
                EvaluationContext.TestOnlyHookOnCreate = null;
            }
        }
 
        private static string[] _sdkResolutionProjects =
        {
            "<Project Sdk=\"foo\"></Project>",
            "<Project Sdk=\"bar\"></Project>",
            "<Project Sdk=\"foo\"></Project>",
            "<Project Sdk=\"bar\"></Project>"
        };
 
        [Theory]
        [InlineData(EvaluationContext.SharingPolicy.Shared, 1, 1)]
        [InlineData(EvaluationContext.SharingPolicy.SharedSDKCache, 1, 1)]
        [InlineData(EvaluationContext.SharingPolicy.Isolated, 4, 4)]
        public void ContextPinsSdkResolverCache(EvaluationContext.SharingPolicy policy, int sdkLookupsForFoo, int sdkLookupsForBar)
        {
            try
            {
                EvaluationContext.TestOnlyHookOnCreate = c => SetResolverForContext(c, _resolver);
 
                var context = EvaluationContext.Create(policy);
                EvaluateProjects(_sdkResolutionProjects, context, null);
 
                _resolver.ResolvedCalls.Count.ShouldBe(2);
                _resolver.ResolvedCalls["foo"].ShouldBe(sdkLookupsForFoo);
                _resolver.ResolvedCalls["bar"].ShouldBe(sdkLookupsForBar);
            }
            finally
            {
                EvaluationContext.TestOnlyHookOnCreate = null;
            }
        }
 
        [Fact]
        public void DefaultContextIsIsolatedContext()
        {
            try
            {
                var seenContexts = new HashSet<EvaluationContext>();
 
                EvaluationContext.TestOnlyHookOnCreate = c => seenContexts.Add(c);
 
                EvaluateProjects(_sdkResolutionProjects, null, null);
 
                seenContexts.Count.ShouldBe(8); // 4 evaluations and 4 reevaluations
                seenContexts.ShouldAllBe(c => c.Policy == EvaluationContext.SharingPolicy.Isolated);
            }
            finally
            {
                EvaluationContext.TestOnlyHookOnCreate = null;
            }
        }
 
        public static IEnumerable<object[]> ContextPinsGlobExpansionCacheData
        {
            get
            {
                yield return new object[]
                {
                    EvaluationContext.SharingPolicy.Shared,
                    new[]
                    {
                        new[] {"0.cs"},
                        new[] {"0.cs"},
                        new[] {"0.cs"},
                        new[] {"0.cs"}
                    }
                };
 
                foreach (var policy in new[] { EvaluationContext.SharingPolicy.SharedSDKCache, EvaluationContext.SharingPolicy.Isolated })
                {
                    yield return new object[]
                    {
                        policy,
                        new[]
                        {
                            new[] {"0.cs"},
                            new[] {"0.cs", "1.cs"},
                            new[] {"0.cs", "1.cs", "2.cs"},
                            new[] {"0.cs", "1.cs", "2.cs", "3.cs"},
                        }
                    };
                }
            }
        }
 
        private static string[] _projectsWithGlobs =
        {
            @"<Project>
                <ItemGroup>
                    <i Include=`**/*.cs` />
                </ItemGroup>
            </Project>",
 
            @"<Project>
                <ItemGroup>
                    <i Include=`**/*.cs` />
                </ItemGroup>
            </Project>",
        };
 
        [Theory]
        [MemberData(nameof(ContextPinsGlobExpansionCacheData))]
        public void ContextCachesItemElementGlobExpansions(EvaluationContext.SharingPolicy policy, string[][] expectedGlobExpansions)
        {
            var projectDirectory = _env.DefaultTestDirectory.Path;
 
            var context = EvaluationContext.Create(policy);
 
            var evaluationCount = 0;
 
            File.WriteAllText(Path.Combine(projectDirectory, $"{evaluationCount}.cs"), "");
 
            EvaluateProjects(
                _projectsWithGlobs,
                context,
                project =>
                {
                    var expectedGlobExpansion = expectedGlobExpansions[evaluationCount];
                    evaluationCount++;
 
                    File.WriteAllText(Path.Combine(projectDirectory, $"{evaluationCount}.cs"), "");
 
                    ObjectModelHelpers.AssertItems(expectedGlobExpansion, project.GetItems("i"));
                });
        }
 
        public static IEnumerable<object[]> ContextDisambiguatesRelativeGlobsData
        {
            get
            {
                yield return new object[]
                {
                    EvaluationContext.SharingPolicy.Shared,
                    new[]
                    {
                        new[] {"0.cs"}, // first project
                        new[] {"0.cs", "1.cs"}, // second project
                        new[] {"0.cs"}, // first project reevaluation
                        new[] {"0.cs", "1.cs"}, // second project reevaluation
                    }
                };
 
                foreach (var policy in new[] { EvaluationContext.SharingPolicy.SharedSDKCache, EvaluationContext.SharingPolicy.Isolated })
                {
                    yield return new object[]
                    {
                        policy,
                        new[]
                        {
                            new[] {"0.cs"},
                            new[] {"0.cs", "1.cs"},
                            new[] {"0.cs", "1.cs", "2.cs"},
                            new[] {"0.cs", "1.cs", "2.cs", "3.cs"},
                        }
                    };
                }
            }
        }
 
        [Theory]
        [MemberData(nameof(ContextDisambiguatesRelativeGlobsData))]
        public void ContextDisambiguatesSameRelativeGlobsPointingInsideDifferentProjectCones(EvaluationContext.SharingPolicy policy, string[][] expectedGlobExpansions)
        {
            var projectDirectory1 = _env.DefaultTestDirectory.CreateDirectory("1").Path;
            var projectDirectory2 = _env.DefaultTestDirectory.CreateDirectory("2").Path;
 
            var context = EvaluationContext.Create(policy);
 
            var evaluationCount = 0;
 
            File.WriteAllText(Path.Combine(projectDirectory1, $"1.{evaluationCount}.cs"), "");
            File.WriteAllText(Path.Combine(projectDirectory2, $"2.{evaluationCount}.cs"), "");
 
            EvaluateProjects(
                new[]
                {
                    new ProjectSpecification(
                        Path.Combine(projectDirectory1, "1"),
                        $@"<Project>
                            <ItemGroup>
                                <i Include=`{Path.Combine("**", "*.cs")}` />
                            </ItemGroup>
                        </Project>"),
                    new ProjectSpecification(
                        Path.Combine(projectDirectory2, "2"),
                        $@"<Project>
                            <ItemGroup>
                                <i Include=`{Path.Combine("**", "*.cs")}` />
                            </ItemGroup>
                        </Project>"),
                },
                context,
                project =>
                {
                    var projectName = Path.GetFileNameWithoutExtension(project.FullPath);
 
                    var expectedGlobExpansion = expectedGlobExpansions[evaluationCount]
                        .Select(i => $"{projectName}.{i}")
                        .ToArray();
 
                    ObjectModelHelpers.AssertItems(expectedGlobExpansion, project.GetItems("i"));
 
                    evaluationCount++;
 
                    File.WriteAllText(Path.Combine(projectDirectory1, $"1.{evaluationCount}.cs"), "");
                    File.WriteAllText(Path.Combine(projectDirectory2, $"2.{evaluationCount}.cs"), "");
                });
        }
 
        [Theory]
        [MemberData(nameof(ContextDisambiguatesRelativeGlobsData))]
        public void ContextDisambiguatesSameRelativeGlobsPointingOutsideDifferentProjectCones(EvaluationContext.SharingPolicy policy, string[][] expectedGlobExpansions)
        {
            var project1Root = _env.DefaultTestDirectory.CreateDirectory("Project1");
            var project1Directory = project1Root.CreateDirectory("1").Path;
            var project1GlobDirectory = project1Root.CreateDirectory("Glob").CreateDirectory("1").Path;
 
            var project2Root = _env.DefaultTestDirectory.CreateDirectory("Project2");
            var project2Directory = project2Root.CreateDirectory("2").Path;
            var project2GlobDirectory = project2Root.CreateDirectory("Glob").CreateDirectory("2").Path;
 
            var context = EvaluationContext.Create(policy);
 
            var evaluationCount = 0;
 
            File.WriteAllText(Path.Combine(project1GlobDirectory, $"1.{evaluationCount}.cs"), "");
            File.WriteAllText(Path.Combine(project2GlobDirectory, $"2.{evaluationCount}.cs"), "");
 
            EvaluateProjects(
                new[]
                {
                    new ProjectSpecification(
                        Path.Combine(project1Directory, "1"),
                        $@"<Project>
                            <ItemGroup>
                                <i Include=`{Path.Combine("..", "Glob", "**", "*.cs")}`/>
                            </ItemGroup>
                        </Project>"),
                    new ProjectSpecification(
                        Path.Combine(project2Directory, "2"),
                        $@"<Project>
                            <ItemGroup>
                                <i Include=`{Path.Combine("..", "Glob", "**", "*.cs")}`/>
                            </ItemGroup>
                        </Project>")
                },
                context,
                project =>
                {
                    var projectName = Path.GetFileNameWithoutExtension(project.FullPath);
 
                    // globs have the fixed directory part prepended, so add it to the expected results
                    var expectedGlobExpansion = expectedGlobExpansions[evaluationCount]
                        .Select(i => Path.Combine("..", "Glob", projectName, $"{projectName}.{i}"))
                        .ToArray();
 
                    var actualGlobExpansion = project.GetItems("i");
                    ObjectModelHelpers.AssertItems(expectedGlobExpansion, actualGlobExpansion);
 
                    evaluationCount++;
 
                    File.WriteAllText(Path.Combine(project1GlobDirectory, $"1.{evaluationCount}.cs"), "");
                    File.WriteAllText(Path.Combine(project2GlobDirectory, $"2.{evaluationCount}.cs"), "");
                });
        }
 
        [Theory]
        [MemberData(nameof(ContextDisambiguatesRelativeGlobsData))]
        public void ContextDisambiguatesAFullyQualifiedGlobPointingInAnotherRelativeGlobsCone(EvaluationContext.SharingPolicy policy, string[][] expectedGlobExpansions)
        {
            if (policy == EvaluationContext.SharingPolicy.Shared)
            {
                // This test case has a dependency on our glob expansion caching policy. If the evaluation context is reused
                // between evaluations and files are added to the filesystem between evaluations, the cache may be returning
                // stale results. Run only the Isolated variant.
                return;
            }
 
            var project1Directory = _env.DefaultTestDirectory.CreateDirectory("Project1");
            var project1GlobDirectory = project1Directory.CreateDirectory("Glob").CreateDirectory("1").Path;
 
            var project2Directory = _env.DefaultTestDirectory.CreateDirectory("Project2");
 
            var context = EvaluationContext.Create(policy);
 
            var evaluationCount = 0;
 
            File.WriteAllText(Path.Combine(project1GlobDirectory, $"{evaluationCount}.cs"), "");
 
            EvaluateProjects(
                new[]
                {
                    // first project uses a relative path
                    new ProjectSpecification(
                        Path.Combine(project1Directory.Path, "1"),
                        $@"<Project>
                            <ItemGroup>
                                <i Include=`{Path.Combine("Glob", "**", "*.cs")}` />
                            </ItemGroup>
                        </Project>"),
                    // second project reaches out into first project's cone via a fully qualified path
                    new ProjectSpecification(
                        Path.Combine(project2Directory.Path, "2"),
                        $@"<Project>
                            <ItemGroup>
                                <i Include=`{Path.Combine(project1Directory.Path, "Glob", "**", "*.cs")}` />
                            </ItemGroup>
                        </Project>")
                },
                context,
                project =>
                {
                    var projectName = Path.GetFileNameWithoutExtension(project.FullPath);
 
                    // globs have the fixed directory part prepended, so add it to the expected results
                    var expectedGlobExpansion = expectedGlobExpansions[evaluationCount]
                        .Select(i => Path.Combine("Glob", "1", i))
                        .ToArray();
 
                    // project 2 has fully qualified directory parts, so make the results for 2 fully qualified
                    if (projectName.Equals("2"))
                    {
                        expectedGlobExpansion = expectedGlobExpansion
                            .Select(i => Path.Combine(project1Directory.Path, i))
                            .ToArray();
                    }
 
                    var actualGlobExpansion = project.GetItems("i");
                    ObjectModelHelpers.AssertItems(expectedGlobExpansion, actualGlobExpansion);
 
                    evaluationCount++;
 
                    File.WriteAllText(Path.Combine(project1GlobDirectory, $"{evaluationCount}.cs"), "");
                });
        }
 
        [Theory]
        [MemberData(nameof(ContextDisambiguatesRelativeGlobsData))]
        public void ContextDisambiguatesDistinctRelativeGlobsPointingOutsideOfSameProjectCone(EvaluationContext.SharingPolicy policy, string[][] expectedGlobExpansions)
        {
            var globDirectory = _env.DefaultTestDirectory.CreateDirectory("glob");
 
            var projectRoot = _env.DefaultTestDirectory.CreateDirectory("proj");
 
            var project1Directory = projectRoot.CreateDirectory("Project1");
 
            var project2SubDir = projectRoot.CreateDirectory("subdirectory");
 
            var project2Directory = project2SubDir.CreateDirectory("Project2");
 
            var context = EvaluationContext.Create(policy);
 
            var evaluationCount = 0;
 
            File.WriteAllText(Path.Combine(globDirectory.Path, $"{evaluationCount}.cs"), "");
 
            EvaluateProjects(
                new[]
                {
                    new ProjectSpecification(
                        Path.Combine(project1Directory.Path, "1"),
                        @"<Project>
                            <ItemGroup>
                                <i Include=`../../glob/*.cs` />
                            </ItemGroup>
                        </Project>"),
                    new ProjectSpecification(
                        Path.Combine(project2Directory.Path, "2"),
                        @"<Project>
                            <ItemGroup>
                                <i Include=`../../../glob/*.cs` />
                            </ItemGroup>
                        </Project>")
                },
                context,
                project =>
                {
                    var projectName = Path.GetFileNameWithoutExtension(project.FullPath);
                    var globFixedDirectoryPart = projectName.EndsWith("1")
                        ? Path.Combine("..", "..", "glob")
                        : Path.Combine("..", "..", "..", "glob");
 
                    // globs have the fixed directory part prepended, so add it to the expected results
                    var expectedGlobExpansion = expectedGlobExpansions[evaluationCount]
                        .Select(i => Path.Combine(globFixedDirectoryPart, i))
                        .ToArray();
 
                    var actualGlobExpansion = project.GetItems("i");
                    ObjectModelHelpers.AssertItems(expectedGlobExpansion, actualGlobExpansion);
 
                    evaluationCount++;
 
                    File.WriteAllText(Path.Combine(globDirectory.Path, $"{evaluationCount}.cs"), "");
                });
        }
 
        [Theory]
        [MemberData(nameof(ContextPinsGlobExpansionCacheData))]
        // projects should cache glob expansions when the __fully qualified__ glob is shared between projects and points outside of project cone
        public void ContextCachesCommonOutOfProjectConeFullyQualifiedGlob(EvaluationContext.SharingPolicy policy, string[][] expectedGlobExpansions)
        {
            ContextCachesCommonOutOfProjectCone(itemSpecPathIsRelative: false, policy: policy, expectedGlobExpansions: expectedGlobExpansions);
        }
 
        [Theory(Skip = "https://github.com/dotnet/msbuild/issues/3889")]
        [MemberData(nameof(ContextPinsGlobExpansionCacheData))]
        // projects should cache glob expansions when the __relative__ glob is shared between projects and points outside of project cone
        public void ContextCachesCommonOutOfProjectConeRelativeGlob(EvaluationContext.SharingPolicy policy, string[][] expectedGlobExpansions)
        {
            ContextCachesCommonOutOfProjectCone(itemSpecPathIsRelative: true, policy: policy, expectedGlobExpansions: expectedGlobExpansions);
        }
 
        private void ContextCachesCommonOutOfProjectCone(bool itemSpecPathIsRelative, EvaluationContext.SharingPolicy policy, string[][] expectedGlobExpansions)
        {
            var testDirectory = _env.DefaultTestDirectory;
            var globDirectory = testDirectory.CreateDirectory("GlobDirectory");
 
            var itemSpecDirectoryPart = itemSpecPathIsRelative
                ? Path.Combine("..", "GlobDirectory")
                : globDirectory.Path;
 
            Directory.CreateDirectory(globDirectory.Path);
 
            // Globs with a directory part will produce items prepended with that directory part.
            // Make a deep copy of the argument to avoid writing to global variables.
            string[][] prependedExpectedGlobExpansions = new string[expectedGlobExpansions.Length][];
            for (int expIndex = 0; expIndex < expectedGlobExpansions.Length; expIndex++)
            {
                string[] globExpansion = expectedGlobExpansions[expIndex];
                string[] prependedGlobExpansion = new string[globExpansion.Length];
 
                prependedExpectedGlobExpansions[expIndex] = prependedGlobExpansion;
                for (var i = 0; i < globExpansion.Length; i++)
                {
                    prependedGlobExpansion[i] = Path.Combine(itemSpecDirectoryPart, globExpansion[i]);
                }
            }
 
            var projectSpecs = new[]
            {
                $@"<Project>
                <ItemGroup>
                    <i Include=`{Path.Combine("{0}", "**", "*.cs")}`/>
                </ItemGroup>
            </Project>",
                $@"<Project>
                <ItemGroup>
                    <i Include=`{Path.Combine("{0}", "**", "*.cs")}`/>
                </ItemGroup>
            </Project>"
            }
                .Select(p => string.Format(p, itemSpecDirectoryPart))
                .Select((p, i) => new ProjectSpecification(Path.Combine(testDirectory.Path, $"ProjectDirectory{i}", $"Project{i}.proj"), p));
 
            var context = EvaluationContext.Create(policy);
 
            var evaluationCount = 0;
 
            File.WriteAllText(Path.Combine(globDirectory.Path, $"{evaluationCount}.cs"), "");
 
            EvaluateProjects(
                projectSpecs,
                context,
                project =>
                {
                    var expectedGlobExpansion = prependedExpectedGlobExpansions[evaluationCount];
                    evaluationCount++;
 
                    File.WriteAllText(Path.Combine(globDirectory.Path, $"{evaluationCount}.cs"), "");
 
                    ObjectModelHelpers.AssertItems(expectedGlobExpansion, project.GetItems("i"));
                });
        }
 
        private static string[] _projectsWithGlobImports =
        {
            @"<Project>
                <Import Project=`*.props` />
            </Project>",
 
            @"<Project>
                <Import Project=`*.props` />
            </Project>",
        };
 
        [Theory]
        [MemberData(nameof(ContextPinsGlobExpansionCacheData))]
        public void ContextCachesImportGlobExpansions(EvaluationContext.SharingPolicy policy, string[][] expectedGlobExpansions)
        {
            var projectDirectory = _env.DefaultTestDirectory.Path;
 
            var context = EvaluationContext.Create(policy);
 
            var evaluationCount = 0;
 
            File.WriteAllText(Path.Combine(projectDirectory, $"{evaluationCount}.props"), $"<Project><ItemGroup><i Include=`{evaluationCount}.cs`/></ItemGroup></Project>".Cleanup());
 
            EvaluateProjects(
                _projectsWithGlobImports,
                context,
                project =>
                {
                    var expectedGlobExpansion = expectedGlobExpansions[evaluationCount];
                    evaluationCount++;
 
                    File.WriteAllText(Path.Combine(projectDirectory, $"{evaluationCount}.props"), $"<Project><ItemGroup><i Include=`{evaluationCount}.cs`/></ItemGroup></Project>".Cleanup());
 
                    ObjectModelHelpers.AssertItems(expectedGlobExpansion, project.GetItems("i"));
                });
        }
 
        private static string[] _projectsWithConditions =
        {
            @"<Project>
                <PropertyGroup Condition=`Exists('0.cs')`>
                    <p>val</p>
                </PropertyGroup>
            </Project>",
 
            @"<Project>
                <PropertyGroup Condition=`Exists('0.cs')`>
                    <p>val</p>
                </PropertyGroup>
            </Project>",
        };
 
        [Theory]
        [InlineData(EvaluationContext.SharingPolicy.Isolated)]
        [InlineData(EvaluationContext.SharingPolicy.SharedSDKCache)]
        [InlineData(EvaluationContext.SharingPolicy.Shared)]
        public void ContextCachesExistenceChecksInConditions(EvaluationContext.SharingPolicy policy)
        {
            var projectDirectory = _env.DefaultTestDirectory.Path;
 
            var context = EvaluationContext.Create(policy);
 
            var theFile = Path.Combine(projectDirectory, "0.cs");
            File.WriteAllText(theFile, "");
 
            var evaluationCount = 0;
 
            EvaluateProjects(
                _projectsWithConditions,
                context,
                project =>
                {
                    evaluationCount++;
 
                    if (File.Exists(theFile))
                    {
                        File.Delete(theFile);
                    }
 
                    if (evaluationCount == 1)
                    {
                        project.GetPropertyValue("p").ShouldBe("val");
                    }
                    else
                    {
                        switch (policy)
                        {
                            case EvaluationContext.SharingPolicy.Shared:
                                project.GetPropertyValue("p").ShouldBe("val");
                                break;
                            case EvaluationContext.SharingPolicy.SharedSDKCache:
                            case EvaluationContext.SharingPolicy.Isolated:
                                project.GetPropertyValue("p").ShouldBeEmpty();
                                break;
                            default:
                                throw new ArgumentOutOfRangeException(nameof(policy), policy, null);
                        }
                    }
                });
        }
 
        [Theory]
        [InlineData(EvaluationContext.SharingPolicy.Isolated)]
        [InlineData(EvaluationContext.SharingPolicy.SharedSDKCache)]
        [InlineData(EvaluationContext.SharingPolicy.Shared)]
        public void ContextCachesExistenceChecksInGetDirectoryNameOfFileAbove(EvaluationContext.SharingPolicy policy)
        {
            var context = EvaluationContext.Create(policy);
 
            var subdirectory = _env.DefaultTestDirectory.CreateDirectory("subDirectory");
            var subdirectoryFile = subdirectory.CreateFile("a");
            _env.DefaultTestDirectory.CreateFile("a");
 
            int evaluationCount = 0;
 
            EvaluateProjects(
                new[]
                {
                    $@"<Project>
                      <PropertyGroup>
                        <SearchedPath>$([MSBuild]::GetDirectoryNameOfFileAbove('{subdirectory.Path}', 'a'))</SearchedPath>
                      </PropertyGroup>
                    </Project>"
                },
                context,
                project =>
                {
                    evaluationCount++;
 
                    var searchedPath = project.GetProperty("SearchedPath");
 
                    switch (policy)
                    {
                        case EvaluationContext.SharingPolicy.Shared:
                            searchedPath.EvaluatedValue.ShouldBe(subdirectory.Path);
                            break;
                        case EvaluationContext.SharingPolicy.SharedSDKCache:
                        case EvaluationContext.SharingPolicy.Isolated:
                            searchedPath.EvaluatedValue.ShouldBe(
                                evaluationCount == 1
                                    ? subdirectory.Path
                                    : _env.DefaultTestDirectory.Path);
                            break;
                        default:
                            throw new ArgumentOutOfRangeException(nameof(policy), policy, null);
                    }
 
                    if (evaluationCount == 1)
                    {
                        // this will cause the upper file to get picked up in the Isolated policy
                        subdirectoryFile.Delete();
                    }
                });
 
            evaluationCount.ShouldBe(2);
        }
 
        [Theory]
        [InlineData(EvaluationContext.SharingPolicy.Isolated)]
        [InlineData(EvaluationContext.SharingPolicy.SharedSDKCache)]
        [InlineData(EvaluationContext.SharingPolicy.Shared)]
        public void ContextCachesExistenceChecksInGetPathOfFileAbove(EvaluationContext.SharingPolicy policy)
        {
            var context = EvaluationContext.Create(policy);
 
            var subdirectory = _env.DefaultTestDirectory.CreateDirectory("subDirectory");
            var subdirectoryFile = subdirectory.CreateFile("a");
            var rootFile = _env.DefaultTestDirectory.CreateFile("a");
 
            int evaluationCount = 0;
 
            EvaluateProjects(
                new[]
                {
                    $@"<Project>
                      <PropertyGroup>
                        <SearchedPath>$([MSBuild]::GetPathOfFileAbove('a', '{subdirectory.Path}'))</SearchedPath>
                      </PropertyGroup>
                    </Project>"
                },
                context,
                project =>
                {
                    evaluationCount++;
 
                    var searchedPath = project.GetProperty("SearchedPath");
 
                    switch (policy)
                    {
                        case EvaluationContext.SharingPolicy.Shared:
                            searchedPath.EvaluatedValue.ShouldBe(subdirectoryFile.Path);
                            break;
                        case EvaluationContext.SharingPolicy.SharedSDKCache:
                        case EvaluationContext.SharingPolicy.Isolated:
                            searchedPath.EvaluatedValue.ShouldBe(
                                evaluationCount == 1
                                    ? subdirectoryFile.Path
                                    : rootFile.Path);
                            break;
                        default:
                            throw new ArgumentOutOfRangeException(nameof(policy), policy, null);
                    }
 
                    if (evaluationCount == 1)
                    {
                        // this will cause the upper file to get picked up in the Isolated policy
                        subdirectoryFile.Delete();
                    }
                });
 
            evaluationCount.ShouldBe(2);
        }
 
        private void EvaluateProjects(IEnumerable<string> projectContents, EvaluationContext context, Action<Project> afterEvaluationAction)
        {
            EvaluateProjects(
                projectContents.Select((p, i) => new ProjectSpecification(Path.Combine(_env.DefaultTestDirectory.Path, $"Project{i}.proj"), p)),
                context,
                afterEvaluationAction);
        }
 
        private struct ProjectSpecification
        {
            public string ProjectFilePath { get; }
            public string ProjectContents { get; }
 
            public ProjectSpecification(string projectFilePath, string projectContents)
            {
                ProjectFilePath = projectFilePath;
                ProjectContents = projectContents;
            }
 
            public void Deconstruct(out string projectPath, out string projectContents)
            {
                projectPath = this.ProjectFilePath;
                projectContents = this.ProjectContents;
            }
        }
 
        /// <summary>
        /// Should be at least two test projects to test cache visibility between projects
        /// </summary>
        private void EvaluateProjects(IEnumerable<ProjectSpecification> projectSpecs, EvaluationContext context, Action<Project> afterEvaluationAction)
        {
            var collection = _env.CreateProjectCollection().Collection;
 
            var projects = new List<Project>();
 
            foreach (var (projectFilePath, projectContents) in projectSpecs)
            {
                Directory.CreateDirectory(Path.GetDirectoryName(projectFilePath));
                File.WriteAllText(projectFilePath, projectContents.Cleanup());
 
                var project = Project.FromFile(
                    projectFilePath,
                    new ProjectOptions
                    {
                        ProjectCollection = collection,
                        EvaluationContext = context,
                        LoadSettings = ProjectLoadSettings.IgnoreMissingImports
                    });
 
                afterEvaluationAction?.Invoke(project);
 
                projects.Add(project);
            }
 
            foreach (var project in projects)
            {
                project.AddItem("a", "b");
                project.ReevaluateIfNecessary(context);
 
                afterEvaluationAction?.Invoke(project);
            }
        }
    }
}