File: Graph\ProjectGraph_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.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.Build.Construction;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Evaluation.Context;
using Microsoft.Build.Exceptions;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.UnitTests;
using Shouldly;
using Xunit;
using Xunit.Abstractions;
using static Microsoft.Build.Graph.UnitTests.GraphTestingUtilities;
 
#nullable disable
 
namespace Microsoft.Build.Graph.UnitTests
{
    [ActiveIssue("https://github.com/dotnet/msbuild/issues/4368")]
    public class ProjectGraphTests : IDisposable
    {
        private TestEnvironment _env;
 
        private const string ProjectReferenceTargetsWithMultitargeting = @"<ItemGroup>
                                                                               <!-- Item order is important to ensure outer build targets are put in front of inner build ones -->
                                                                               <ProjectReferenceTargets Include='A' Targets='AHelperInner;A' />
                                                                               <ProjectReferenceTargets Include='A' Targets='AHelperOuter' OuterBuild='true' />
                                                                           </ItemGroup>";
        private static string[] NonOuterBuildTargets = { "AHelperOuter", "AHelperInner", "A" };
        private static string[] OuterBuildTargets = { "AHelperOuter" };
 
        private const string OuterBuildSpecificationWithProjectReferenceTargets = MultitargetingSpecificationPropertyGroup + ProjectReferenceTargetsWithMultitargeting;
 
        public ProjectGraphTests(ITestOutputHelper outputHelper)
        {
            _env = TestEnvironment.Create(outputHelper);
        }
 
        [Fact]
        public void ConstructWithNoNodes()
        {
            var projectGraph = new ProjectGraph(Enumerable.Empty<ProjectGraphEntryPoint>());
 
            projectGraph.ProjectNodes.ShouldBeEmpty();
            projectGraph.EntryPointNodes.ShouldBeEmpty();
            projectGraph.GraphRoots.ShouldBeEmpty();
            projectGraph.ProjectNodesTopologicallySorted.ShouldBeEmpty();
            projectGraph.GetTargetLists(new[] { "restore", "build" }).ShouldBeEmpty();
        }
 
        [Fact]
        public void ConstructWithSingleNode()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = CreateProjectFile(env, 1);
                var projectGraph = new ProjectGraph(entryProject.Path);
                projectGraph.ProjectNodes.Count.ShouldBe(1);
                projectGraph.ProjectNodes.First().ProjectInstance.FullPath.ShouldBe(entryProject.Path);
            }
        }
 
        [Fact]
        public void ConstructionMetricsAreAvailable()
        {
            var graph = Helpers.CreateProjectGraph(
                _env,
                new Dictionary<int, int[]>()
                {
                    {1, new[] {2, 3}}
                });
 
            graph.ConstructionMetrics.ConstructionTime.Ticks.ShouldBeGreaterThan(0);
            graph.ConstructionMetrics.NodeCount.ShouldBe(3);
            graph.ConstructionMetrics.EdgeCount.ShouldBe(2);
        }
 
        [Fact]
        public void CycleInGraphDoesNotThrowStackOverflowException()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = CreateProjectFile(env, 1, new[] { 2 });
                CreateProjectFile(env, 2, new[] { 2 }, extraContent: @"<PropertyGroup><UsingMicrosoftNETSdk>true</UsingMicrosoftNETSdk></PropertyGroup>");
                Should.Throw<CircularDependencyException>(() => new ProjectGraph(entryProject.Path));
            }
        }
 
        [Fact]
        public void ConstructWithSingleNodeWithProjectInstanceFactory()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = CreateProjectFile(env, 1);
 
                bool factoryCalled = false;
                var projectGraph = new ProjectGraph(
                    entryProject.Path,
                    ProjectCollection.GlobalProjectCollection,
                    (projectPath, globalProperties, projectCollection) =>
                    {
                        factoryCalled = true;
                        return ProjectGraph.StaticProjectInstanceFactory(
                            projectPath,
                            globalProperties,
                            projectCollection,
                            EvaluationContext.Create(EvaluationContext.SharingPolicy.Isolated));
                    });
                projectGraph.ProjectNodes.Count.ShouldBe(1);
                projectGraph.ProjectNodes.First().ProjectInstance.FullPath.ShouldBe(entryProject.Path);
                factoryCalled.ShouldBeTrue();
            }
        }
 
        [Fact]
        public void ProjectGraphNodeConstructorNoNullArguments()
        {
            Assert.Throws<InternalErrorException>(() => new ProjectGraphNode(null));
        }
 
        [Fact]
        public void UpdatingReferencesIsBidirectional()
        {
            using (var env = TestEnvironment.Create())
            {
                var projectInstance = new Project().CreateProjectInstance();
                var node = new ProjectGraphNode(projectInstance);
                var reference1 = new ProjectGraphNode(projectInstance);
                var referenceItem1 = new ProjectItemInstance(projectInstance, "Ref1", "path1", "file1");
 
                var reference2 = new ProjectGraphNode(projectInstance);
                var referenceItem2 = new ProjectItemInstance(projectInstance, "Ref2", "path2", "file2");
 
                var edges = new GraphBuilder.GraphEdges();
 
                node.AddProjectReference(reference1, referenceItem1, edges);
                node.AddProjectReference(reference2, referenceItem2, edges);
 
                node.ProjectReferences.ShouldBeSameIgnoringOrder(new[] { reference1, reference2 });
                node.ReferencingProjects.ShouldBeEmpty();
 
                reference1.ReferencingProjects.ShouldBeSameIgnoringOrder(new[] { node });
                reference1.ProjectReferences.ShouldBeEmpty();
 
                reference2.ReferencingProjects.ShouldBeSameIgnoringOrder(new[] { node });
                reference2.ProjectReferences.ShouldBeEmpty();
 
                edges[(node, reference1)].ShouldBe(referenceItem1);
                edges[(node, reference2)].ShouldBe(referenceItem2);
 
                edges.Count.ShouldBe(2);
 
                node.RemoveReferences(edges);
 
                node.ProjectReferences.ShouldBeEmpty();
                node.ReferencingProjects.ShouldBeEmpty();
 
                reference1.ProjectReferences.ShouldBeEmpty();
                reference1.ReferencingProjects.ShouldBeEmpty();
 
                reference2.ProjectReferences.ShouldBeEmpty();
                reference2.ReferencingProjects.ShouldBeEmpty();
 
                edges.Count.ShouldBe(0);
            }
        }
 
        [Fact]
        public void FirstEdgeWinsWhenMultipleEdgesPointToSameReference()
        {
            using (var env = TestEnvironment.Create())
            {
                var projectInstance = new Project().CreateProjectInstance();
                var node = new ProjectGraphNode(projectInstance);
                var reference1 = new ProjectGraphNode(projectInstance);
                var referenceItem1 = new ProjectItemInstance(projectInstance, "Ref1", "path1", "file1");
                var referenceItem2 = new ProjectItemInstance(projectInstance, "Ref2", "path1", "file1");
 
                var edges = new GraphBuilder.GraphEdges();
 
                node.AddProjectReference(reference1, referenceItem1, edges);
 
                // add same reference but via a different edge
                node.AddProjectReference(reference1, referenceItem2, edges);
 
                edges.Count.ShouldBe(1);
 
                edges[(node, reference1)].ShouldBe(referenceItem1);
            }
        }
 
        [Fact]
        public void ConstructWithProjectInstanceFactory_FactoryReturnsNull_Throws()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = CreateProjectFile(env, 1);
 
                var aggException = Should.Throw<AggregateException>(() => new ProjectGraph(
                    entryProject.Path,
                    ProjectCollection.GlobalProjectCollection,
                    (projectPath, globalProperties, projectCollection) => null));
                aggException.InnerExceptions.ShouldHaveSingleItem();
 
                aggException.InnerExceptions[0].ShouldBeOfType<InvalidOperationException>();
            }
        }
 
        /// <summary>
        ///   1
        ///  / \
        /// 2   3
        /// </summary>
        [Fact]
        public void ConstructWithThreeNodes()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = CreateProjectFile(env, 1, new[] { 2, 3 });
                CreateProjectFile(env, 2);
                CreateProjectFile(env, 3);
                ProjectGraph graph = new ProjectGraph(entryProject.Path);
 
                graph.ProjectNodes.Count.ShouldBe(3);
                GetFirstNodeWithProjectNumber(graph, 1).ProjectReferences.Count.ShouldBe(2);
                GetFirstNodeWithProjectNumber(graph, 2).ProjectReferences.Count.ShouldBe(0);
                GetFirstNodeWithProjectNumber(graph, 3).ProjectReferences.Count.ShouldBe(0);
            }
        }
 
        /// <summary>
        /// Test the following graph with entry project 2
        /// 2 depends on 3,5,6
        /// 6 depends on 1
        /// 5 depends on 7
        /// 1 depends on 4,5
        /// </summary>
        [Fact]
        public void ConstructWithMultipleNodes()
        {
            using (var env = TestEnvironment.Create())
            {
                CreateProjectFile(env, 1, new[] { 4, 5 });
                TransientTestFile entryProject = CreateProjectFile(env, 2, new[] { 3, 5, 6 });
                CreateProjectFile(env, 3);
                CreateProjectFile(env, 4);
                CreateProjectFile(env, 5, new[] { 7 });
                CreateProjectFile(env, 6, new[] { 1 });
                CreateProjectFile(env, 7);
 
                ProjectGraph graph = new ProjectGraph(entryProject.Path);
 
                graph.ProjectNodes.Count.ShouldBe(7);
                ProjectGraphNode node1 = GetFirstNodeWithProjectNumber(graph, 1);
                ProjectGraphNode node2 = GetFirstNodeWithProjectNumber(graph, 2);
                ProjectGraphNode node3 = GetFirstNodeWithProjectNumber(graph, 3);
                ProjectGraphNode node4 = GetFirstNodeWithProjectNumber(graph, 4);
                ProjectGraphNode node5 = GetFirstNodeWithProjectNumber(graph, 5);
                ProjectGraphNode node6 = GetFirstNodeWithProjectNumber(graph, 6);
                ProjectGraphNode node7 = GetFirstNodeWithProjectNumber(graph, 7);
 
                node1.ProjectReferences.Count.ShouldBe(2);
                node2.ProjectReferences.Count.ShouldBe(3);
                node3.ProjectReferences.Count.ShouldBe(0);
                node4.ProjectReferences.Count.ShouldBe(0);
                node5.ProjectReferences.Count.ShouldBe(1);
                node6.ProjectReferences.Count.ShouldBe(1);
                node7.ProjectReferences.Count.ShouldBe(0);
 
                node1.ReferencingProjects.Count.ShouldBe(1);
                node2.ReferencingProjects.Count.ShouldBe(0);
                node3.ReferencingProjects.Count.ShouldBe(1);
                node4.ReferencingProjects.Count.ShouldBe(1);
                node5.ReferencingProjects.Count.ShouldBe(2);
                node6.ReferencingProjects.Count.ShouldBe(1);
                node7.ReferencingProjects.Count.ShouldBe(1);
 
                // confirm that there is a path from 2 -> 6 -> 1 -> 5 -> 7
                node2.ProjectReferences.ShouldContain(node6);
                node6.ProjectReferences.ShouldContain(node1);
                node1.ProjectReferences.ShouldContain(node5);
                node5.ProjectReferences.ShouldContain(node7);
 
                // confirm that there is a path from 7 -> 5 -> 1 -> 6 -> 2 using ReferencingProjects
                node7.ReferencingProjects.ShouldContain(node5);
                node5.ReferencingProjects.ShouldContain(node1);
                node1.ReferencingProjects.ShouldContain(node6);
                node6.ReferencingProjects.ShouldContain(node2);
            }
        }
 
        [Fact]
        public void ConstructWithCycle()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = CreateProjectFile(env, 1, new[] { 2 });
                var proj2 = CreateProjectFile(env, 2, new[] { 3 });
                var proj3 = CreateProjectFile(env, 3, new[] { 1 });
                var projectsInCycle = new List<string> { entryProject.Path, proj3.Path, proj2.Path, entryProject.Path };
                string expectedErrorMessage = GraphBuilder.FormatCircularDependencyError(projectsInCycle);
                Should.Throw<CircularDependencyException>(() => new ProjectGraph(entryProject.Path)).Message.ShouldContain(expectedErrorMessage);
            }
        }
 
        [Fact]
        public void ConstructWithSelfLoop()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = CreateProjectFile(env, 1, new[] { 2, 3 });
                CreateProjectFile(env, 2, new[] { 2 });
                CreateProjectFile(env, 3);
                Should.Throw<CircularDependencyException>(() => new ProjectGraph(entryProject.Path));
            }
        }
 
        [Fact]
        // graph with a cycle between 2->6->7->3->2
        public void ConstructBigGraphWithCycle()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = CreateProjectFile(env, 1, new[] { 2, 3, 4 });
                var proj2 = CreateProjectFile(env, 2, new[] { 5, 6 });
                var proj3 = CreateProjectFile(env, 3, new[] { 2, 8 });
                CreateProjectFile(env, 4);
                CreateProjectFile(env, 5, new[] { 9, 10 });
                var proj6 = CreateProjectFile(env, 6, new[] { 7 });
                var proj7 = CreateProjectFile(env, 7, new[] { 3 });
                CreateProjectFile(env, 8);
                CreateProjectFile(env, 9);
                CreateProjectFile(env, 10);
                var projectsInCycle = new List<string> { proj2.Path, proj3.Path, proj7.Path, proj6.Path, proj2.Path };
                var errorMessage = GraphBuilder.FormatCircularDependencyError(projectsInCycle);
                Should.Throw<CircularDependencyException>(() => new ProjectGraph(entryProject.Path)).Message.ShouldContain(errorMessage);
            }
        }
 
        [Fact]
        public void ProjectCollectionShouldNotInfluenceGlobalProperties()
        {
            var entryFile1 = CreateProjectFile(_env, 1, new[] { 3, 4 });
            var entryFile2 = CreateProjectFile(_env, 2, new[] { 4, 5 });
            CreateProjectFile(_env, 3);
            CreateProjectFile(_env, 4);
            CreateProjectFile(_env, 5);
 
            var entryPoint1 = new ProjectGraphEntryPoint(entryFile1.Path, new Dictionary<string, string> { ["B"] = "EntryPointB", ["C"] = "EntryPointC" });
            var entryPoint2 = new ProjectGraphEntryPoint(entryFile2.Path, null);
 
            var collection = _env.CreateProjectCollection().Collection;
            collection.SetGlobalProperty("A", "CollectionA");
            collection.SetGlobalProperty("B", "CollectionB");
 
            var graph = new ProjectGraph(
                entryPoints: new[] { entryPoint1, entryPoint2 },
                projectCollection: collection,
                projectInstanceFactory: null);
 
            var root1 = GetFirstNodeWithProjectNumber(graph, 1);
            var globalPropertiesFor1 = new Dictionary<string, string> { ["B"] = "EntryPointB", ["C"] = "EntryPointC", ["IsGraphBuild"] = "true" };
 
            root1.ProjectInstance.GlobalProperties.ShouldBeSameIgnoringOrder(globalPropertiesFor1);
            root1.ProjectReferences.First(r => GetProjectNumber(r) == 3).ProjectInstance.GlobalProperties.ShouldBeSameIgnoringOrder(globalPropertiesFor1);
            root1.ProjectReferences.First(r => GetProjectNumber(r) == 4).ProjectInstance.GlobalProperties.ShouldBeSameIgnoringOrder(globalPropertiesFor1);
 
            var root2 = GetFirstNodeWithProjectNumber(graph, 2);
            var globalPropertiesFor2 = new Dictionary<string, string> { ["IsGraphBuild"] = "true" };
 
            root2.ProjectInstance.GlobalProperties.ShouldBeSameIgnoringOrder(globalPropertiesFor2);
            root2.ProjectReferences.First(r => GetProjectNumber(r) == 4).ProjectInstance.GlobalProperties.ShouldBeSameIgnoringOrder(globalPropertiesFor2);
            root2.ProjectReferences.First(r => GetProjectNumber(r) == 5).ProjectInstance.GlobalProperties.ShouldBeSameIgnoringOrder(globalPropertiesFor2);
        }
 
        [Fact]
        public void ConstructWithDifferentGlobalProperties()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = CreateProjectFile(env, 1, new[] { 2, 3 });
                env.CreateFile("2.proj", @"
<Project>
  <ItemGroup>
    <ProjectReference Include=""4.proj"" />
  </ItemGroup>
</Project>");
                env.CreateFile("3.proj", @"
<Project>
  <ItemGroup>
    <ProjectReference Include=""4.proj"" AdditionalProperties=""A=B"" />
  </ItemGroup>
</Project>");
                CreateProjectFile(env, 4);
                ProjectGraph graph = new ProjectGraph(entryProject.Path);
 
                // Project 4 requires 2 nodes
                graph.ProjectNodes.Count.ShouldBe(5);
 
                // Projects 2 and 3 both reference project 4, but with different properties, so they should not point to the same node.
                GetFirstNodeWithProjectNumber(graph, 2).ProjectReferences.First().ShouldNotBe(GetFirstNodeWithProjectNumber(graph, 3).ProjectReferences.First());
                GetFirstNodeWithProjectNumber(graph, 2).ProjectReferences.First().ProjectInstance.FullPath.ShouldEndWith("4.proj");
                GetFirstNodeWithProjectNumber(graph, 2).ProjectReferences.First().ProjectInstance.GlobalProperties.ShouldBeSameIgnoringOrder(EmptyGlobalProperties);
                GetFirstNodeWithProjectNumber(graph, 3).ProjectReferences.First().ProjectInstance.FullPath.ShouldEndWith("4.proj");
                GetFirstNodeWithProjectNumber(graph, 3).ProjectReferences.First().ProjectInstance.GlobalProperties.Count.ShouldBeGreaterThan(1);
            }
        }
 
        [Fact]
        public void TestGlobalPropertiesInProjectReferences()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = env.CreateFile("1.proj", @"
<Project>
  <ItemGroup>
    <ProjectReference Include=""2.proj"" AdditionalProperties=""A=B""/>
  </ItemGroup>
</Project>");
                CreateProjectFile(env, 2, new[] { 3 });
                CreateProjectFile(env, 3);
                ProjectGraph graph = new ProjectGraph(entryProject.Path);
                graph.ProjectNodes.Count.ShouldBe(3);
                GetFirstNodeWithProjectNumber(graph, 3).ProjectInstance.GlobalProperties["A"].ShouldBe("B");
            }
        }
 
        [Fact]
        public void ConstructWithConvergingProperties()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = CreateProjectFile(env, 1, new[] { 2, 3 });
                env.CreateFile("2.proj", @"
<Project>
  <ItemGroup>
    <ProjectReference Include=""4.proj"" AdditionalProperties=""Foo=A"" />
  </ItemGroup>
</Project>");
                env.CreateFile("3.proj", @"
<Project>
  <ItemGroup>
    <ProjectReference Include=""4.proj"" AdditionalProperties=""Foo=B"" />
  </ItemGroup>
</Project>");
                env.CreateFile("4.proj", @"
<Project>
  <ItemGroup>
    <ProjectReference Include=""5.proj"" GlobalPropertiesToRemove=""Foo"" />
  </ItemGroup>
</Project>");
                CreateProjectFile(env, 5);
                ProjectGraph graph = new ProjectGraph(entryProject.Path);
 
                // Project 4 requires 2 nodes, but project 5 does not
                graph.ProjectNodes.Count.ShouldBe(6);
 
                var node4A = GetFirstNodeWithProjectNumber(graph, 2).ProjectReferences.First();
                var node4B = GetFirstNodeWithProjectNumber(graph, 3).ProjectReferences.First();
                node4A.ShouldNotBe(node4B);
 
                node4A.ProjectReferences.Count.ShouldBe(1);
                node4B.ProjectReferences.Count.ShouldBe(1);
                node4A.ProjectReferences.First().ShouldBe(node4B.ProjectReferences.First());
            }
        }
 
        [Fact]
        public void ConstructWithSameEffectiveProperties()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = CreateProjectFile(env, 1, new[] { 2, 3 });
                env.CreateFile("2.proj", @"
<Project>
  <ItemGroup>
    <ProjectReference Include=""4.proj"" AdditionalProperties=""Foo=Bar"" />
  </ItemGroup>
</Project>");
                env.CreateFile("3.proj", @"
<Project>
  <ItemGroup>
    <ProjectReference Include=""4.proj"" GlobalPropertiesToRemove=""DoesNotExist"" />
  </ItemGroup>
</Project>");
                CreateProjectFile(env, 4);
                ProjectGraph graph = new ProjectGraph(
                    entryProject.Path,
                    new Dictionary<string, string> { { "Foo", "Bar" } });
 
                // Project 4 does not require 2 nodes
                graph.ProjectNodes.Count.ShouldBe(4);
 
                // The project references end up using the same effective properties
                GetFirstNodeWithProjectNumber(graph, 2).ProjectReferences.First().ShouldBe(GetFirstNodeWithProjectNumber(graph, 3).ProjectReferences.First());
            }
        }
 
        [Fact]
        public void ConstructWithCaseDifferences()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = CreateProjectFile(env, 1, new[] { 2, 3, 4 });
                env.CreateFile("2.proj", @"
<Project>
  <ItemGroup>
    <ProjectReference Include=""5.proj"" AdditionalProperties=""foo=bar"" />
  </ItemGroup>
</Project>");
                env.CreateFile("3.proj", @"
<Project>
  <ItemGroup>
    <ProjectReference Include=""5.proj"" AdditionalProperties=""FOO=bar"" />
  </ItemGroup>
</Project>");
                env.CreateFile("4.proj", @"
<Project>
  <ItemGroup>
    <ProjectReference Include=""5.proj"" AdditionalProperties=""foo=BAR"" />
  </ItemGroup>
</Project>");
                CreateProjectFile(env, 5);
                ProjectGraph graph = new ProjectGraph(entryProject.Path);
 
                // Project 5 requires 2 nodes
                graph.ProjectNodes.Count.ShouldBe(6);
 
                // Property names are case-insensitive, so projects 2 and 3 point to the same project 5 node.
                GetFirstNodeWithProjectNumber(graph, 2).ProjectReferences.First().ShouldBe(GetFirstNodeWithProjectNumber(graph, 3).ProjectReferences.First());
                GetFirstNodeWithProjectNumber(graph, 2).ProjectReferences.First().ProjectInstance.FullPath.ShouldEndWith("5.proj");
                GetFirstNodeWithProjectNumber(graph, 2).ProjectReferences.First().ProjectInstance.GlobalProperties["FoO"].ShouldBe("bar");
 
                // Property values are case-sensitive, so project 4 points to a different project 5 node than proejcts 2 and 3
                GetFirstNodeWithProjectNumber(graph, 4).ProjectReferences.First().ShouldNotBe(GetFirstNodeWithProjectNumber(graph, 2).ProjectReferences.First());
                GetFirstNodeWithProjectNumber(graph, 4).ProjectReferences.First().ProjectInstance.FullPath.ShouldEndWith("5.proj");
                GetFirstNodeWithProjectNumber(graph, 4).ProjectReferences.First().ProjectInstance.GlobalProperties["FoO"].ShouldBe("BAR");
            }
        }
 
        [Fact]
        public void ConstructWithInvalidProperties()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = CreateProjectFile(env, 1, new[] { 2 });
                env.CreateFile("2.proj", @"
<Project>
  <ItemGroup>
    <ProjectReference Include=""3.proj"" AdditionalProperties=""ThisIsntValid"" />
  </ItemGroup>
</Project>");
                CreateProjectFile(env, 3);
 
                var aggException = Should.Throw<AggregateException>(() => new ProjectGraph(entryProject.Path));
                aggException.InnerExceptions.ShouldHaveSingleItem();
 
                aggException.InnerExceptions[0].ShouldBeOfType<InvalidProjectFileException>();
            }
        }
 
        [Fact]
        public void ConstructWithMultipleEntryPoints()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject1 = CreateProjectFile(env, 1, new[] { 3 });
                TransientTestFile entryProject2 = CreateProjectFile(env, 2, new[] { 3 });
                CreateProjectFile(env, 3);
                var projectGraph = new ProjectGraph(new[] { entryProject1.Path, entryProject2.Path });
                projectGraph.ProjectNodes.Count.ShouldBe(3);
 
                var node1 = GetFirstNodeWithProjectNumber(projectGraph, 1);
                var node2 = GetFirstNodeWithProjectNumber(projectGraph, 2);
                var node3 = GetFirstNodeWithProjectNumber(projectGraph, 3);
                node1.ProjectReferences.Count.ShouldBe(1);
                node1.ProjectReferences.First().ShouldBe(node3);
                node2.ProjectReferences.Count.ShouldBe(1);
                node2.ProjectReferences.First().ShouldBe(node3);
            }
        }
 
        [Fact]
        public void ConstructWithMultipleEntryPointsWithDifferentGlobalProperties()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = CreateProjectFile(env, 1, new[] { 2 });
                CreateProjectFile(env, 2);
                var entryPoint1 = new ProjectGraphEntryPoint(entryProject.Path, new Dictionary<string, string> { { "Platform", "x86" } });
                var entryPoint2 = new ProjectGraphEntryPoint(entryProject.Path, new Dictionary<string, string> { { "Platform", "x64" } });
 
                var projectGraph = new ProjectGraph(new[] { entryPoint1, entryPoint2 });
                projectGraph.ProjectNodes.Count.ShouldBe(4);
 
                projectGraph.EntryPointNodes.Count.ShouldBe(2);
 
                var entryPointNode1 = projectGraph.EntryPointNodes.First();
                var entryPointNode2 = projectGraph.EntryPointNodes.Last();
 
                // The entry points should not be the same node, but should point to the same project
                entryPointNode1.ShouldNotBe(entryPointNode2);
                entryPointNode1.ProjectInstance.FullPath.ShouldBe(entryPointNode2.ProjectInstance.FullPath);
                entryPointNode1.ProjectInstance.GlobalProperties["Platform"].ShouldBe("x86");
                entryPointNode2.ProjectInstance.GlobalProperties["Platform"].ShouldBe("x64");
 
                // The entry points should not have the same project reference, but should point to the same project reference file
                entryPointNode1.ProjectReferences.Count.ShouldBe(1);
                entryPointNode2.ProjectReferences.Count.ShouldBe(1);
                entryPointNode1.ProjectReferences.First().ShouldNotBe(entryPointNode2.ProjectReferences.First());
                entryPointNode1.ProjectReferences.First().ProjectInstance.FullPath.ShouldBe(entryPointNode2.ProjectReferences.First().ProjectInstance.FullPath);
                entryPointNode1.ProjectReferences.First().ProjectInstance.GlobalProperties["Platform"].ShouldBe("x86");
                entryPointNode2.ProjectReferences.First().ProjectInstance.GlobalProperties["Platform"].ShouldBe("x64");
            }
        }
 
        [Fact]
        public void ConstructWithMultipleEntryPointsWithDifferentGlobalPropertiesConverging()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = env.CreateFile("1.proj", @"
<Project>
  <ItemGroup>
    <ProjectReference Include=""2.proj"" GlobalPropertiesToRemove=""Platform"" />
  </ItemGroup>
</Project>");
                CreateProjectFile(env, 2);
                var entryPoint1 = new ProjectGraphEntryPoint(entryProject.Path, new Dictionary<string, string> { { "Platform", "x86" } });
                var entryPoint2 = new ProjectGraphEntryPoint(entryProject.Path, new Dictionary<string, string> { { "Platform", "x64" } });
 
                var projectGraph = new ProjectGraph(new[] { entryPoint1, entryPoint2 });
                projectGraph.ProjectNodes.Count.ShouldBe(3);
 
                projectGraph.EntryPointNodes.Count.ShouldBe(2);
 
                var entryPointNode1 = projectGraph.EntryPointNodes.First();
                var entryPointNode2 = projectGraph.EntryPointNodes.Last();
 
                // The entry points should not be the same node, but should point to the same project
                entryPointNode1.ShouldNotBe(entryPointNode2);
                entryPointNode1.ProjectInstance.FullPath.ShouldBe(entryPointNode2.ProjectInstance.FullPath);
                entryPointNode1.ProjectInstance.GlobalProperties["Platform"].ShouldBe("x86");
                entryPointNode2.ProjectInstance.GlobalProperties["Platform"].ShouldBe("x64");
 
                // The entry points should have the same project reference since they're platform-agnostic
                entryPointNode1.ProjectReferences.Count.ShouldBe(1);
                entryPointNode2.ProjectReferences.Count.ShouldBe(1);
                entryPointNode1.ProjectReferences.First().ShouldBe(entryPointNode2.ProjectReferences.First());
                entryPointNode1.ProjectReferences.First().ProjectInstance.GlobalProperties.ContainsKey("Platform").ShouldBeFalse();
            }
        }
 
        [Fact]
        public void ConstructGraphWithDifferentEntryPointsAndGraphRoots()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject1 = CreateProjectFile(env, 1, new[] { 4 });
                TransientTestFile entryProject2 = CreateProjectFile(env, 2, new[] { 4, 5 });
                TransientTestFile entryProject3 = CreateProjectFile(env, 3, new[] { 2, 6 });
                CreateProjectFile(env, 4);
                CreateProjectFile(env, 5);
                CreateProjectFile(env, 6);
                var projectGraph = new ProjectGraph(new[] { entryProject1.Path, entryProject2.Path, entryProject3.Path });
                projectGraph.EntryPointNodes.Count.ShouldBe(3);
                projectGraph.GraphRoots.Count.ShouldBe(2);
                projectGraph.GraphRoots.ShouldNotContain(GetFirstNodeWithProjectNumber(projectGraph, 2));
            }
        }
 
        [Fact]
        public void ConstructGraphWithSolution()
        {
            /*
             * This test exercises various key features of solution-based builds:
             *      From AssignProjectConfiguration:
             *          Adding synthetic project references (defined both before and after the depending project)
             *          Resolving project configuration based on the sln
             *          Handling unresolved project references with ShouldUnsetParentConfigurationAndPlatform=true
             *          Handling unresolved project references with ShouldUnsetParentConfigurationAndPlatform=false
             *      Project types other than "well-known" MSBuild project types:
             *          Buildable project (wapproj)
             *          Solution folder
             *      Project not included in build (also has solution dependencies as a regression test)
             * 
             */
            using (var env = TestEnvironment.Create())
            {
                const string SolutionFileContents = """
                    Microsoft Visual Studio Solution File, Format Version 12.00
                    # Visual Studio Version 17
                    VisualStudioVersion = 17.0.31903.59
                    MinimumVisualStudioVersion = 17.0.31903.59
                    Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Project8", "Project8.csproj", "{2022C11A-1405-4983-BEC2-3A8B0233108F}"
                    EndProject
                    Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Project1", "Project1.csproj", "{8761499A-7280-43C4-A32F-7F41C47CA6DF}"
                        ProjectSection(ProjectDependencies) = postProject
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98} = {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}
                            {2022C11A-1405-4983-BEC2-3A8B0233108F} = {2022C11A-1405-4983-BEC2-3A8B0233108F}
                        EndProjectSection
                    EndProject
                    Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Project2", "Project2.vcxproj", "{D638A8EF-3A48-45F2-913C-88B29FED03CB}"
                    EndProject
                    Project("{13B669BE-BB05-4DDF-9536-439F39A36129}") = "Project3", "Project3.vcxproj", "{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}"
                    EndProject
                    Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "Project6", "Project6.wapproj", "{CA5CAD1A-224A-4171-B13A-F16E576FDD12}"
                    EndProject
                    Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Project7", "Project7.csproj", "{28C7025E-2AB6-4962-A001-1E5B2271837C}"
                        ProjectSection(ProjectDependencies) = postProject
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98} = {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}
                        EndProjectSection
                    EndProject
                    Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0392E290-973E-4086-A58E-F927AAA65B9A}"
                        ProjectSection(SolutionItems) = preProject
                            SomeSolutionItemsFile = SomeSolutionItemsFile
                        EndProjectSection
                    EndProject
                    Global
                        GlobalSection(SolutionConfigurationPlatforms) = preSolution
                            Debug|Win32 = Debug|Win32
                            Debug|x64 = Debug|x64
                            Debug|x86 = Debug|x86
                            Release|Win32 = Release|Win32
                            Release|x64 = Release|x64
                            Release|x86 = Release|x86
                        EndGlobalSection
                        GlobalSection(ProjectConfigurationPlatforms) = postSolution
                            {2022C11A-1405-4983-BEC2-3A8B0233108F}.Debug|Win32.ActiveCfg = Debug|x86
                            {2022C11A-1405-4983-BEC2-3A8B0233108F}.Debug|Win32.Build.0 = Debug|x86
                            {2022C11A-1405-4983-BEC2-3A8B0233108F}.Debug|x64.ActiveCfg = Debug|x64
                            {2022C11A-1405-4983-BEC2-3A8B0233108F}.Debug|x64.Build.0 = Debug|x64
                            {2022C11A-1405-4983-BEC2-3A8B0233108F}.Debug|x86.ActiveCfg = Debug|x86
                            {2022C11A-1405-4983-BEC2-3A8B0233108F}.Debug|x86.Build.0 = Debug|x86
                            {2022C11A-1405-4983-BEC2-3A8B0233108F}.Release|Win32.ActiveCfg = Release|x86
                            {2022C11A-1405-4983-BEC2-3A8B0233108F}.Release|Win32.Build.0 = Release|x86
                            {2022C11A-1405-4983-BEC2-3A8B0233108F}.Release|x64.ActiveCfg = Release|x64
                            {2022C11A-1405-4983-BEC2-3A8B0233108F}.Release|x64.Build.0 = Release|x64
                            {2022C11A-1405-4983-BEC2-3A8B0233108F}.Release|x86.ActiveCfg = Release|x86
                            {2022C11A-1405-4983-BEC2-3A8B0233108F}.Release|x86.Build.0 = Release|x86
                            {8761499A-7280-43C4-A32F-7F41C47CA6DF}.Debug|Win32.ActiveCfg = Debug|x86
                            {8761499A-7280-43C4-A32F-7F41C47CA6DF}.Debug|Win32.Build.0 = Debug|x86
                            {8761499A-7280-43C4-A32F-7F41C47CA6DF}.Debug|x64.ActiveCfg = Debug|x64
                            {8761499A-7280-43C4-A32F-7F41C47CA6DF}.Debug|x64.Build.0 = Debug|x64
                            {8761499A-7280-43C4-A32F-7F41C47CA6DF}.Debug|x86.ActiveCfg = Debug|x86
                            {8761499A-7280-43C4-A32F-7F41C47CA6DF}.Debug|x86.Build.0 = Debug|x86
                            {8761499A-7280-43C4-A32F-7F41C47CA6DF}.Release|Win32.ActiveCfg = Release|x86
                            {8761499A-7280-43C4-A32F-7F41C47CA6DF}.Release|Win32.Build.0 = Release|x86
                            {8761499A-7280-43C4-A32F-7F41C47CA6DF}.Release|x64.ActiveCfg = Release|x64
                            {8761499A-7280-43C4-A32F-7F41C47CA6DF}.Release|x64.Build.0 = Release|x64
                            {8761499A-7280-43C4-A32F-7F41C47CA6DF}.Release|x86.ActiveCfg = Release|x86
                            {8761499A-7280-43C4-A32F-7F41C47CA6DF}.Release|x86.Build.0 = Release|x86
                            {D638A8EF-3A48-45F2-913C-88B29FED03CB}.Debug|Win32.ActiveCfg = Debug|Win32
                            {D638A8EF-3A48-45F2-913C-88B29FED03CB}.Debug|Win32.Build.0 = Debug|Win32
                            {D638A8EF-3A48-45F2-913C-88B29FED03CB}.Debug|x64.ActiveCfg = Debug|x64
                            {D638A8EF-3A48-45F2-913C-88B29FED03CB}.Debug|x64.Build.0 = Debug|x64
                            {D638A8EF-3A48-45F2-913C-88B29FED03CB}.Release|Win32.ActiveCfg = Release|Win32
                            {D638A8EF-3A48-45F2-913C-88B29FED03CB}.Release|Win32.Build.0 = Release|Win32
                            {D638A8EF-3A48-45F2-913C-88B29FED03CB}.Release|x64.ActiveCfg = Release|x64
                            {D638A8EF-3A48-45F2-913C-88B29FED03CB}.Release|x64.Build.0 = Release|x64
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Debug|Win32.ActiveCfg = Debug|Win32
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Debug|Win32.Build.0 = Debug|Win32
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Debug|x64.ActiveCfg = Debug|x64
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Debug|x64.Build.0 = Debug|x64
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Debug|x86.ActiveCfg = Debug|Win32
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Debug|x86.Build.0 = Debug|Win32
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Release|Win32.ActiveCfg = Release|Win32
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Release|Win32.Build.0 = Release|Win32
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Release|x64.ActiveCfg = Release|x64
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Release|x64.Build.0 = Release|x64
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Release|x86.ActiveCfg = Release|Win32
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Release|x86.Build.0 = Release|Win32
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Debug|Win32.ActiveCfg = Debug|Win32
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Debug|Win32.Build.0 = Debug|Win32
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Debug|x64.ActiveCfg = Debug|x64
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Debug|x64.Build.0 = Debug|x64
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Debug|x86.ActiveCfg = Debug|Win32
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Debug|x86.Build.0 = Debug|Win32
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Release|Win32.ActiveCfg = Release|Win32
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Release|Win32.Build.0 = Release|Win32
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Release|x64.ActiveCfg = Release|x64
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Release|x64.Build.0 = Release|x64
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Release|x86.ActiveCfg = Release|Win32
                            {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Release|x86.Build.0 = Release|Win32
                            {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|Win32.ActiveCfg = Debug|x86
                            {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|Win32.Build.0 = Debug|x86
                            {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|Win32.Deploy.0 = Debug|x86
                            {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|x64.ActiveCfg = Debug|x64
                            {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|x64.Build.0 = Debug|x64
                            {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|x64.Deploy.0 = Debug|x64
                            {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|x86.ActiveCfg = Debug|x86
                            {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|x86.Build.0 = Debug|x86
                            {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|x86.Deploy.0 = Debug|x86
                            {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Release|Win32.ActiveCfg = Release|x86
                            {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Release|x64.ActiveCfg = Release|x64
                            {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Release|x86.ActiveCfg = Release|x86
                            {28C7025E-2AB6-4962-A001-1E5B2271837C}.Debug|Win32.ActiveCfg = Debug|x86
                            {28C7025E-2AB6-4962-A001-1E5B2271837C}.Debug|x64.ActiveCfg = Debug|x64
                            {28C7025E-2AB6-4962-A001-1E5B2271837C}.Debug|x86.ActiveCfg = Debug|x86
                            {28C7025E-2AB6-4962-A001-1E5B2271837C}.Release|Win32.ActiveCfg = Release|x86
                            {28C7025E-2AB6-4962-A001-1E5B2271837C}.Release|x64.ActiveCfg = Release|x64
                            {28C7025E-2AB6-4962-A001-1E5B2271837C}.Release|x86.ActiveCfg = Release|x86
                        EndGlobalSection
                        GlobalSection(SolutionProperties) = preSolution
                            HideSolutionNode = FALSE
                        EndGlobalSection
                    EndGlobal
                    """;
                TransientTestFile slnFile = env.CreateFile(@"Solution.sln", SolutionFileContents);
                SolutionFile solutionFile = SolutionFile.Parse(slnFile.Path);
 
                ProjectRootElement project1Xml = ProjectRootElement.Create();
 
                // Project 1 depends on Project 2 using ProjectReference but there is a sln-based dependency defined on Project 3 and 8 as well.
                project1Xml.AddItem("ProjectReference", "Project2.vcxproj");
 
                ProjectRootElement project2Xml = ProjectRootElement.Create();
 
                // Project 2 depends on Project 4, which is not in the solution and uses ShouldUnsetParentConfigurationAndPlatform=true (the default)
                project2Xml.AddItem("ProjectReference", "Project4.vcxproj");
                project2Xml.AddProperty("ShouldUnsetParentConfigurationAndPlatform", "true");
 
                ProjectRootElement project3Xml = ProjectRootElement.Create();
 
                // Project 3 depends on Project 5, which is not in the solution and uses ShouldUnsetParentConfigurationAndPlatform=false
                project3Xml.AddItem("ProjectReference", "Project5.vcxproj");
                project3Xml.AddProperty("ShouldUnsetParentConfigurationAndPlatform", "false");
 
                ProjectRootElement project4Xml = ProjectRootElement.Create();
                ProjectRootElement project5Xml = ProjectRootElement.Create();
                ProjectRootElement project6Xml = ProjectRootElement.Create();
                ProjectRootElement project7Xml = ProjectRootElement.Create();
                ProjectRootElement project8Xml = ProjectRootElement.Create();
 
                string project1Path = Path.Combine(env.DefaultTestDirectory.Path, "Project1.csproj");
                string project2Path = Path.Combine(env.DefaultTestDirectory.Path, "Project2.vcxproj");
                string project3Path = Path.Combine(env.DefaultTestDirectory.Path, "Project3.vcxproj");
                string project4Path = Path.Combine(env.DefaultTestDirectory.Path, "Project4.vcxproj");
                string project5Path = Path.Combine(env.DefaultTestDirectory.Path, "Project5.vcxproj");
                string project6Path = Path.Combine(env.DefaultTestDirectory.Path, "Project6.wapproj");
                string project7Path = Path.Combine(env.DefaultTestDirectory.Path, "Project7.csproj");
                string project8Path = Path.Combine(env.DefaultTestDirectory.Path, "Project8.csproj");
 
                project1Xml.Save(project1Path);
                project2Xml.Save(project2Path);
                project3Xml.Save(project3Path);
                project4Xml.Save(project4Path);
                project5Xml.Save(project5Path);
                project6Xml.Save(project6Path);
                project7Xml.Save(project7Path);
                project8Xml.Save(project8Path);
 
                var projectGraph = new ProjectGraph(slnFile.Path);
                projectGraph.EntryPointNodes.Count.ShouldBe(5);
                projectGraph.EntryPointNodes.Select(node => node.ProjectInstance.FullPath).ShouldBe(new[] { project1Path, project2Path, project3Path, project6Path, project8Path }, ignoreOrder: true);
                projectGraph.GraphRoots.Count.ShouldBe(2);
                projectGraph.GraphRoots.Select(node => node.ProjectInstance.FullPath).ShouldBe(new[] { project1Path, project6Path }, ignoreOrder: true);
                projectGraph.ProjectNodes.Count.ShouldBe(7);
 
                ProjectGraphNode project1Node = projectGraph.ProjectNodes.Single(node => node.ProjectInstance.FullPath == project1Path);
                project1Node.ProjectInstance.GlobalProperties["Configuration"].ShouldBe("Debug");
                project1Node.ProjectInstance.GlobalProperties["Platform"].ShouldBe("x86");
                project1Node.ProjectReferences.Count.ShouldBe(3);
                project1Node.ProjectReferences.Select(node => node.ProjectInstance.FullPath).ShouldBe(new[] { project2Path, project3Path, project8Path }, ignoreOrder: true);
 
                ProjectGraphNode project2Node = projectGraph.ProjectNodes.Single(node => node.ProjectInstance.FullPath == project2Path);
                project2Node.ProjectInstance.GlobalProperties["Configuration"].ShouldBe("Debug");
                project2Node.ProjectInstance.GlobalProperties["Platform"].ShouldBe("Win32");
                project2Node.ProjectReferences.Count.ShouldBe(1);
                project2Node.ProjectReferences.Select(node => node.ProjectInstance.FullPath).ShouldBe(new[] { project4Path }, ignoreOrder: true);
 
                ProjectGraphNode project3Node = projectGraph.ProjectNodes.Single(node => node.ProjectInstance.FullPath == project3Path);
                project3Node.ProjectInstance.GlobalProperties["Configuration"].ShouldBe("Debug");
                project3Node.ProjectInstance.GlobalProperties["Platform"].ShouldBe("Win32");
                project3Node.ProjectReferences.Count.ShouldBe(1);
                project3Node.ProjectReferences.Select(node => node.ProjectInstance.FullPath).ShouldBe(new[] { project5Path }, ignoreOrder: true);
 
                // Configuration and Platform get unset
                ProjectGraphNode project4Node = projectGraph.ProjectNodes.Single(node => node.ProjectInstance.FullPath == project4Path);
                project4Node.ProjectInstance.GlobalProperties.ContainsKey("Configuration").ShouldBeFalse();
                project4Node.ProjectInstance.GlobalProperties.ContainsKey("Platform").ShouldBeFalse();
                project4Node.ProjectReferences.Count.ShouldBe(0);
 
                // Configuration and Platform are inherited from the referencing project
                ProjectGraphNode project5Node = projectGraph.ProjectNodes.Single(node => node.ProjectInstance.FullPath == project5Path);
                project5Node.ProjectInstance.GlobalProperties["Configuration"].ShouldBe("Debug");
                project5Node.ProjectInstance.GlobalProperties["Platform"].ShouldBe("Win32");
                project5Node.ProjectReferences.Count.ShouldBe(0);
 
                // Project type other than "well-known" MSBuild project types.
                ProjectGraphNode project6Node = projectGraph.ProjectNodes.Single(node => node.ProjectInstance.FullPath == project6Path);
                project6Node.ProjectInstance.GlobalProperties["Configuration"].ShouldBe("Debug");
                project6Node.ProjectInstance.GlobalProperties["Platform"].ShouldBe("x86");
                project6Node.ProjectReferences.Count.ShouldBe(0);
 
                // Project not included in the build
                Assert.DoesNotContain(projectGraph.ProjectNodes, node => node.ProjectInstance.FullPath == project7Path);
 
                ProjectGraphNode project8Node = projectGraph.ProjectNodes.Single(node => node.ProjectInstance.FullPath == project8Path);
                project8Node.ProjectInstance.GlobalProperties["Configuration"].ShouldBe("Debug");
                project8Node.ProjectInstance.GlobalProperties["Platform"].ShouldBe("x86");
                project8Node.ProjectReferences.Count.ShouldBe(0);
            }
        }
 
        [Fact]
        public void GetTargetListsAggregatesFromMultipleEdges()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = CreateProjectFile(env: env, projectNumber: 1, projectReferences: new[] { 2, 3 }, projectReferenceTargets: new Dictionary<string, string[]> { { "A", new[] { "B" } } });
                CreateProjectFile(env: env, projectNumber: 2, projectReferences: new[] { 4 }, projectReferenceTargets: new Dictionary<string, string[]> { { "B", new[] { "C" } } });
                CreateProjectFile(env: env, projectNumber: 3, projectReferences: new[] { 4 }, projectReferenceTargets: new Dictionary<string, string[]> { { "B", new[] { "D" } } });
                CreateProjectFile(env: env, projectNumber: 4);
 
                var projectGraph = new ProjectGraph(entryProject.Path);
                projectGraph.ProjectNodes.Count.ShouldBe(4);
 
                IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = projectGraph.GetTargetLists(new[] { "A" });
                targetLists.Count.ShouldBe(projectGraph.ProjectNodes.Count);
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 1)].ShouldBe(new[] { "A" });
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 2)].ShouldBe(new[] { "B" });
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 3)].ShouldBe(new[] { "B" });
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 4)].ShouldBe(new[] { "C", "D" }); // From B => C and B => D
            }
        }
 
        [Fact]
        public void GetTargetListsDedupesTargets()
        {
            var projectReferenceTargets = new Dictionary<string, string[]>
            {
                { "A" , new[] { "B", "X", "C" } },
                { "B" , new[] { "X", "Y" } },
                { "C" , new[] { "X", "Z" } },
            };
 
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = CreateProjectFile(env: env, projectNumber: 1, projectReferences: new[] { 2 }, projectReferenceTargets: projectReferenceTargets);
                CreateProjectFile(env: env, projectNumber: 2, projectReferences: new[] { 3 }, projectReferenceTargets: projectReferenceTargets);
                CreateProjectFile(env: env, projectNumber: 3, projectReferences: Array.Empty<int>(), projectReferenceTargets: projectReferenceTargets);
 
                var projectGraph = new ProjectGraph(entryProject.Path);
                projectGraph.ProjectNodes.Count.ShouldBe(3);
 
                IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = projectGraph.GetTargetLists(new[] { "A" });
                targetLists.Count.ShouldBe(projectGraph.ProjectNodes.Count);
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 1)].ShouldBe(new[] { "A" });
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 2)].ShouldBe(new[] { "B", "X", "C" });
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 3)].ShouldBe(new[] { "X", "Y", "Z" }); // Simplified from X, Y, X, Z
            }
        }
 
        [Fact]
        public void GetTargetListsForComplexGraph()
        {
            var projectReferenceTargets = new Dictionary<string, string[]>
            {
                { "A" , new[] { "B" } },
                { "B" , new[] { "C" } },
                { "C" , new[] { "D" } },
                { "D" , new[] { "E" } },
            };
 
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = CreateProjectFile(env: env, projectNumber: 1, projectReferences: new[] { 2, 3, 5 }, projectReferenceTargets: projectReferenceTargets);
                CreateProjectFile(env: env, projectNumber: 2, projectReferences: new[] { 4, 5 }, projectReferenceTargets: projectReferenceTargets);
                CreateProjectFile(env: env, projectNumber: 3, projectReferences: new[] { 5, 6 }, projectReferenceTargets: projectReferenceTargets);
                CreateProjectFile(env: env, projectNumber: 4, projectReferences: new[] { 5 }, projectReferenceTargets: projectReferenceTargets);
                CreateProjectFile(env: env, projectNumber: 5, projectReferences: new[] { 6 }, projectReferenceTargets: projectReferenceTargets);
                CreateProjectFile(env: env, projectNumber: 6, projectReferences: Array.Empty<int>(), projectReferenceTargets: projectReferenceTargets);
 
                var projectGraph = new ProjectGraph(entryProject.Path);
                projectGraph.ProjectNodes.Count.ShouldBe(6);
 
                IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = projectGraph.GetTargetLists(new[] { "A" });
                targetLists.Count.ShouldBe(projectGraph.ProjectNodes.Count);
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 1)].ShouldBe(new[] { "A" });
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 2)].ShouldBe(new[] { "B" });
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 3)].ShouldBe(new[] { "B" });
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 4)].ShouldBe(new[] { "C" });
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 5)].ShouldBe(new[] { "B", "C", "D" });
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 6)].ShouldBe(new[] { "C", "D", "E" });
            }
        }
 
        [Fact]
        public void GetTargetListsNullEntryTargets()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = CreateProjectFile(env: env, projectNumber: 1, projectReferences: new[] { 2 }, projectReferenceTargets: new Dictionary<string, string[]> { { "A", new[] { "B" } } }, defaultTargets: "A");
                CreateProjectFile(env: env, projectNumber: 2);
 
                var projectGraph = new ProjectGraph(entryProject.Path);
                projectGraph.ProjectNodes.Count.ShouldBe(2);
 
                IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = projectGraph.GetTargetLists(null);
                targetLists.Count.ShouldBe(projectGraph.ProjectNodes.Count);
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 1)].ShouldBe(new[] { "A" });
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 2)].ShouldBe(new[] { "B" });
            }
        }
 
        [Fact]
        public void GetTargetsListReturnsEmptyTargetsForNodeIfNoTargetsPropagatedToIt()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = CreateProjectFile(env: env, projectNumber: 1, projectReferences: new[] { 2 }, projectReferenceTargets: new Dictionary<string, string[]> { { "A", new[] { "B" } } }, defaultTargets: "A");
                CreateProjectFile(env: env, projectNumber: 2);
 
                var projectGraph = new ProjectGraph(entryProject.Path);
                projectGraph.ProjectNodes.Count.ShouldBe(2);
 
                IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = projectGraph.GetTargetLists(new[] { "Foo" });
                targetLists.Count.ShouldBe(projectGraph.ProjectNodes.Count);
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 1)].ShouldBe(new[] { "Foo" });
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 2)].ShouldBeEmpty();
            }
        }
 
        [Fact]
        public void GetTargetListsReturnsEmptyTargetsForAllNodesWhenDefaultTargetsAreRequestedAndThereAreNoDefaultTargets()
        {
            using (var env = TestEnvironment.Create())
            {
                // Root project has no default targets.
                // The project file does not contain any targets
                TransientTestFile entryProject = CreateProjectFile(env: env, projectNumber: 1, projectReferences: new[] { 2 }, projectReferenceTargets: new Dictionary<string, string[]> { { "A", new[] { "B" } } }, defaultTargets: string.Empty);
 
                // Dependency has default targets. Even though it gets called with empty targets, B will not get called,
                // because target propagation only equates empty targets to default targets for the root nodes.
                CreateProjectFile(env: env, projectNumber: 2, defaultTargets: "B");
 
                var projectGraph = new ProjectGraph(entryProject.Path);
                projectGraph.ProjectNodes.Count.ShouldBe(2);
 
                IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = projectGraph.GetTargetLists(null);
                targetLists.Count.ShouldBe(projectGraph.ProjectNodes.Count);
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 1)].ShouldBeEmpty();
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 2)].ShouldBeEmpty();
            }
        }
 
        [Fact]
        public void GetTargetListsDoesNotPropagateEmptyTargets()
        {
            using (var env = TestEnvironment.Create())
            {
                // Target protocol produces empty target
                // The project file also does not contain any targets
                TransientTestFile entryProject = CreateProjectFile(env: env, projectNumber: 1, projectReferences: new[] { 2 }, projectReferenceTargets: new Dictionary<string, string[]> { { "A", new[] { " ; ; " } } }, defaultTargets: string.Empty);
 
                // Dependency has default targets. Even though it gets called with empty targets, B will not get called,
                // because target propagation only equates empty targets to default targets for the root nodes.
                CreateProjectFile(env: env, projectNumber: 2, defaultTargets: "B");
 
                var projectGraph = new ProjectGraph(entryProject.Path);
                projectGraph.ProjectNodes.Count.ShouldBe(2);
 
                IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = projectGraph.GetTargetLists(new[] { "A" });
                targetLists.Count.ShouldBe(projectGraph.ProjectNodes.Count);
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 1)].ShouldBe(new[] { "A" });
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 2)].ShouldBeEmpty();
            }
        }
 
        [Fact]
        public void GetTargetListsThrowsOnInvalidTargetNames()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = CreateProjectFile(env: env, projectNumber: 1);
 
                var projectGraph = new ProjectGraph(entryProject.Path);
                projectGraph.ProjectNodes.Count.ShouldBe(1);
 
                Should.Throw<ArgumentException>(() => projectGraph.GetTargetLists(new[] { "   " }));
            }
        }
 
 
        [Fact]
        public void GetTargetListsUsesAllTargetsForNonMultitargetingNodes()
        {
            using (var env = TestEnvironment.Create())
            {
                var root1 = CreateProjectFile(
                    env: env,
                    projectNumber: 1,
                    projectReferences: new[] { 2 },
                    projectReferenceTargets: null,
                    defaultTargets: null,
                    extraContent: ProjectReferenceTargetsWithMultitargeting)
                    .Path;
                CreateProjectFile(env, 2);
 
                var projectGraph = new ProjectGraph(root1);
 
                var dot = projectGraph.ToDot();
 
                projectGraph.ProjectNodes.Count.ShouldBe(2);
 
                IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = projectGraph.GetTargetLists(new List<string> { "A" });
                targetLists.Count.ShouldBe(projectGraph.ProjectNodes.Count);
 
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 1)].ShouldBe(new[] { "A" });
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 2)].ShouldBe(NonOuterBuildTargets);
            }
        }
 
        [Fact]
        public void GetTargetsListInnerBuildToInnerBuild()
        {
            using (var env = TestEnvironment.Create())
            {
                string singleTargetedSpec = OuterBuildSpecificationWithProjectReferenceTargets +
                        $@"<PropertyGroup>
                            <{InnerBuildPropertyName}>a</{InnerBuildPropertyName}>
                          </PropertyGroup>";
 
                var root1 = CreateProjectFile(
                            env: env,
                            projectNumber: 1,
                            projectReferences: new[] { 2 },
                            projectReferenceTargets: null,
                            defaultTargets: null,
                            extraContent: singleTargetedSpec)
                            .Path;
                CreateProjectFile(
                    env: env,
                    projectNumber: 2,
                    projectReferences: null,
                    projectReferenceTargets: null,
                    defaultTargets: null,
                    extraContent: singleTargetedSpec);
 
 
                var projectGraph = new ProjectGraph(root1);
 
                var dot = projectGraph.ToDot();
 
                projectGraph.ProjectNodes.Count.ShouldBe(2);
 
                IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = projectGraph.GetTargetLists(new List<string> { "A" });
                targetLists.Count.ShouldBe(projectGraph.ProjectNodes.Count);
 
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 1)].ShouldBe(new[] { "A" });
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 2)].ShouldBe(NonOuterBuildTargets);
            }
        }
 
        [Fact]
        public void GetTargetListsFiltersTargetsForOuterAndInnerBuilds()
        {
            using (var env = TestEnvironment.Create())
            {
                var root1 = CreateProjectFile(
                    env: env,
                    projectNumber: 1,
                    projectReferences: new[] { 2 },
                    projectReferenceTargets: null,
                    defaultTargets: null,
                    extraContent: ProjectReferenceTargetsWithMultitargeting).Path;
                CreateProjectFile(
                    env: env,
                    projectNumber: 2,
                    projectReferences: null,
                    projectReferenceTargets: null,
                    defaultTargets: null,
                    extraContent: OuterBuildSpecificationWithProjectReferenceTargets);
 
                var projectGraph = new ProjectGraph(root1);
 
                var dot = projectGraph.ToDot();
 
                projectGraph.ProjectNodes.Count.ShouldBe(4);
 
                IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = projectGraph.GetTargetLists(new List<string> { "A" });
 
                targetLists.Count.ShouldBe(projectGraph.ProjectNodes.Count);
                var root = GetFirstNodeWithProjectNumber(projectGraph, 1);
 
                var outerBuild = GetOuterBuild(projectGraph, 2);
                var innerBuilds = GetInnerBuilds(projectGraph, 2).ToArray();
 
                targetLists[root].ShouldBe(new[] { "A" });
                targetLists[outerBuild].ShouldBe(OuterBuildTargets);
 
                foreach (var innerBuild in innerBuilds)
                {
                    targetLists[innerBuild].ShouldBe(NonOuterBuildTargets);
                }
            }
        }
 
        [Fact]
        public void GetTargetListsDoesNotUseTargetsMetadataOnInnerBuildsFromRootOuterBuilds()
        {
            string projectReferenceTargetsProtocol =
$@"<ItemGroup>
     <ProjectReferenceTargets Include='A' Targets='{MSBuildConstants.ProjectReferenceTargetsOrDefaultTargetsMarker};A;AInner' />
     <ProjectReferenceTargets Include='A' Targets='{MSBuildConstants.ProjectReferenceTargetsOrDefaultTargetsMarker};A;AOuter' OuterBuild='true' />
   </ItemGroup>";
 
            string entryProject = CreateProjectFile(
                env: _env,
                projectNumber: 1,
                projectReferences: null,
                projectReferenceTargets: null,
                defaultTargets: "D1",
                extraContent: MultitargetingSpecificationPropertyGroup +
                              projectReferenceTargetsProtocol +
$@"
<ItemGroup>
    <ProjectReference Include='2.proj' Targets='T2' />
</ItemGroup>
")
                .Path;
            CreateProjectFile(
                env: _env,
                projectNumber: 2,
                projectReferences: null,
                projectReferenceTargets: null,
                defaultTargets: "D2",
                extraContent: projectReferenceTargetsProtocol +
$@"
<ItemGroup>
    <ProjectReference Include='3.proj' Targets='T3' />
</ItemGroup>
");
            CreateProjectFile(
                env: _env,
                projectNumber: 3,
                projectReferences: null,
                projectReferenceTargets: null,
                defaultTargets: "D3",
                extraContent: MultitargetingSpecificationPropertyGroup + projectReferenceTargetsProtocol);
 
            var graph = new ProjectGraph(entryProject);
 
            var dot = graph.ToDot();
 
            ProjectGraphNode rootOuterBuild = GetOuterBuild(graph, 1);
            ProjectGraphNode nonRootOuterBuild = GetOuterBuild(graph, 3);
 
            AssertOuterBuild(rootOuterBuild, graph);
            AssertOuterBuild(nonRootOuterBuild, graph);
 
            IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = graph.GetTargetLists(new[] { "A" });
 
            targetLists[rootOuterBuild].ShouldBe(new[] { "A" });
 
            foreach (ProjectGraphNode innerBuild in GetInnerBuilds(graph, 1))
            {
                targetLists[innerBuild].ShouldBe(new[] { "D1", "A", "AOuter", "AInner" });
            }
 
            targetLists[GetFirstNodeWithProjectNumber(graph, 2)].ShouldBe(new[] { "T2", "A", "AOuter", "AInner" });
 
            targetLists[nonRootOuterBuild].ShouldBe(new[] { "T3", "A", "AOuter" });
 
            foreach (ProjectGraphNode innerBuild in GetInnerBuilds(graph, 3))
            {
                targetLists[innerBuild].ShouldBe(new[] { "T3", "A", "AOuter", "AInner", "D3" });
            }
        }
 
        [Fact]
        public void GetTargetListsForComplexMultitargetingGraph()
        {
            using (var env = TestEnvironment.Create())
            {
                var root1 = CreateProjectFile(
                    env: env,
                    projectNumber: 1,
                    projectReferences: null,
                    projectReferenceTargets: null,
                    defaultTargets: null,
                    extraContent: OuterBuildSpecificationWithProjectReferenceTargets +
                    $@"<ItemGroup>
                            <ProjectReference Include=`3.proj` Condition=`'$({InnerBuildPropertyName})'=='a'`/>
 
                            <ProjectReference Include=`4.proj` Condition=`'$({InnerBuildPropertyName})'=='b'`/>
                            <ProjectReference Include=`5.proj` Condition=`'$({InnerBuildPropertyName})'=='b'`/>
                            <ProjectReference Include=`6.proj` Condition=`'$({InnerBuildPropertyName})'=='b'` Properties=`{InnerBuildPropertyName}=a`/>
                       </ItemGroup>".Cleanup())
                    .Path;
 
                var root2 = CreateProjectFile(
                    env: env,
                    projectNumber: 2,
                    projectReferences: null,
                    projectReferenceTargets: null,
                    defaultTargets: null,
                    extraContent: ProjectReferenceTargetsWithMultitargeting +
                    $@"<ItemGroup>
                            <ProjectReference Include=`1.proj` Properties=`{InnerBuildPropertyName}=b`/>
                            <ProjectReference Include=`4.proj`/>
                            <ProjectReference Include=`5.proj`/>
                       </ItemGroup>".Cleanup())
                    .Path;
 
                CreateProjectFile(
                    env: env,
                    projectNumber: 3,
                    projectReferences: null,
                    projectReferenceTargets: null,
                    defaultTargets: null,
                    extraContent: OuterBuildSpecificationWithProjectReferenceTargets);
 
                CreateProjectFile(
                    env: env,
                    projectNumber: 4,
                    projectReferences: new[] { 6 },
                    projectReferenceTargets: null,
                    defaultTargets: null,
                    extraContent: ProjectReferenceTargetsWithMultitargeting);
 
                CreateProjectFile(
                    env: env,
                    projectNumber: 5,
                    projectReferences: null,
                    projectReferenceTargets: null,
                    defaultTargets: null,
                    extraContent: OuterBuildSpecificationWithProjectReferenceTargets +
                    $@"
                       <PropertyGroup>
                            <{InnerBuildPropertyName}>a</{InnerBuildPropertyName}>
                       </PropertyGroup>
 
                       <ItemGroup>
                            <ProjectReference Include=`3.proj` Properties=`{InnerBuildPropertyName}=a`/>
                            <ProjectReference Include=`6.proj`/>
                       </ItemGroup>".Cleanup());
 
                CreateProjectFile(
                    env: env,
                    projectNumber: 6,
                    projectReferences: null,
                    projectReferenceTargets: null,
                    defaultTargets: null,
                    extraContent: OuterBuildSpecificationWithProjectReferenceTargets);
 
                var projectGraph = new ProjectGraph(new[] { root1, root2 });
 
                var dot = projectGraph.ToDot();
 
                projectGraph.ProjectNodes.Count.ShouldBe(12);
 
                IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = projectGraph.GetTargetLists(new List<string> { "A" });
 
                targetLists.Count.ShouldBe(projectGraph.ProjectNodes.Count);
 
                AssertMultitargetingNode(1, projectGraph, targetLists, new[] { "A" }, NonOuterBuildTargets);
                AssertMultitargetingNode(3, projectGraph, targetLists, OuterBuildTargets, NonOuterBuildTargets);
                AssertMultitargetingNode(6, projectGraph, targetLists, OuterBuildTargets, NonOuterBuildTargets);
 
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 2)].ShouldBe(new[] { "A" });
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 4)].ShouldBe(NonOuterBuildTargets);
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 5)].ShouldBe(NonOuterBuildTargets);
            }
 
            void AssertMultitargetingNode(int projectNumber, ProjectGraph projectGraph, IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists, string[] outerBuildTargets, string[] nonOuterBuildTargets)
            {
                targetLists[GetOuterBuild(projectGraph, projectNumber)].ShouldBe(outerBuildTargets);
 
                foreach (var innerBuild in GetInnerBuilds(projectGraph, projectNumber))
                {
                    targetLists[innerBuild].ShouldBe(nonOuterBuildTargets);
                }
            }
        }
 
        [Fact]
        public void GetTargetListsDefaultTargetsAreExpanded()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = CreateProjectFile(env, 1, new[] { 2 }, new Dictionary<string, string[]> { { "A", new[] { ".default" } } }, defaultTargets: "A");
                CreateProjectFile(env: env, projectNumber: 2, defaultTargets: "B");
 
                var projectGraph = new ProjectGraph(entryProject.Path);
                projectGraph.ProjectNodes.Count.ShouldBe(2);
 
                IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = projectGraph.GetTargetLists(null);
                targetLists.Count.ShouldBe(projectGraph.ProjectNodes.Count);
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 1)].ShouldBe(new[] { "A" });
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 2)].ShouldBe(new[] { "B" });
            }
        }
 
        [Fact]
        public void GetTargetListsUnspecifiedTargetsDefaultToBuild()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = CreateProjectFile(
                    env: env,
                    projectNumber: 1,
                    projectReferences: new[] { 2 },
                    projectReferenceTargets: new Dictionary<string, string[]> { { "Build", new[] { "A", ".default" } } });
 
                CreateProjectFile(env: env, projectNumber: 2);
 
                var projectGraph = new ProjectGraph(entryProject.Path);
                projectGraph.ProjectNodes.Count.ShouldBe(2);
 
                IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = projectGraph.GetTargetLists(null);
                targetLists.Count.ShouldBe(projectGraph.ProjectNodes.Count);
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 1)].ShouldBe(new[] { "Build" });
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 2)].ShouldBe(new[] { "A", "Build" });
            }
        }
 
        [Fact]
        public void GetTargetListsDefaultComplexPropagation()
        {
            var projectReferenceTargets = new Dictionary<string, string[]>
            {
                { "Build", new[] { "A", ".default" } },
                { "X", new[] { "B", ".default" } },
                { "Y", new[] { "C", ".default" } },
            };
 
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject = CreateProjectFile(env, 1, new[] { 2, 3, 4 }, projectReferenceTargets);
                CreateProjectFile(env: env, projectNumber: 2, projectReferences: new[] { 5 }, projectReferenceTargets: projectReferenceTargets);
                CreateProjectFile(env: env, projectNumber: 3, projectReferences: new[] { 6 }, projectReferenceTargets: projectReferenceTargets, defaultTargets: "X");
                CreateProjectFile(env: env, projectNumber: 4, projectReferences: new[] { 7 }, projectReferenceTargets: projectReferenceTargets, defaultTargets: "Y");
                CreateProjectFile(env: env, projectNumber: 5);
                CreateProjectFile(env: env, projectNumber: 6);
                CreateProjectFile(env: env, projectNumber: 7, defaultTargets: "Z;W");
 
                var projectGraph = new ProjectGraph(entryProject.Path);
                projectGraph.ProjectNodes.Count.ShouldBe(7);
 
                IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = projectGraph.GetTargetLists(null);
                targetLists.Count.ShouldBe(projectGraph.ProjectNodes.Count);
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 1)].ShouldBe(new[] { "Build" });
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 2)].ShouldBe(new[] { "A", "Build" });
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 3)].ShouldBe(new[] { "A", "X" });
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 4)].ShouldBe(new[] { "A", "Y" });
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 5)].ShouldBe(new[] { "A", "Build" });
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 6)].ShouldBe(new[] { "B", "Build" });
                targetLists[GetFirstNodeWithProjectNumber(projectGraph, 7)].ShouldBe(new[] { "C", "Z", "W" });
            }
        }
 
        [Fact]
        public void GetTargetsListProjectReferenceTargetsOrDefaultComplexPropagation()
        {
            var referenceItem = @"
<ItemGroup>
    <ProjectReference Include='{0}.proj' Targets='{1}' />
</ItemGroup>
";
 
            using (var env = TestEnvironment.Create())
            {
                var entryProject = CreateProjectFile(
                    env: env,
                    projectNumber: 1,
                    projectReferences: new[] { 2, 3, 4 },
                    projectReferenceTargets: new Dictionary<string, string[]> { { "Build", new[] { "Build" } } });
                CreateProjectFile(
                    env: env,
                    projectNumber: 2,
                    projectReferences: null,
                    projectReferenceTargets:
                        new Dictionary<string, string[]> { { "Build", new[] { MSBuildConstants.ProjectReferenceTargetsOrDefaultTargetsMarker, "T2" } } },
                    defaultTargets: null,
                    extraContent: referenceItem.Format("5", "T51"));
                CreateProjectFile(
                    env: env,
                    projectNumber: 3,
                    projectReferences: null,
                    projectReferenceTargets:
                        new Dictionary<string, string[]> { { "Build", new[] { MSBuildConstants.ProjectReferenceTargetsOrDefaultTargetsMarker, "T3" } } },
                    defaultTargets: null,
                    extraContent: referenceItem.Format("5", "T51;T53;T54"));
                CreateProjectFile(
                    env: env,
                    projectNumber: 4,
                    projectReferences: null,
                    projectReferenceTargets:
                        new Dictionary<string, string[]> { { "Build", new[] { MSBuildConstants.ProjectReferenceTargetsOrDefaultTargetsMarker, "T4" } } },
                    defaultTargets: null,
                    extraContent: referenceItem.Format("5", ""));
                CreateProjectFile(env: env, projectNumber: 5, projectReferences: null, projectReferenceTargets: null, defaultTargets: "D51;D52");
 
                var projectGraph = new ProjectGraph(entryProjectFile: entryProject.Path);
                projectGraph.ProjectNodes.Count.ShouldBe(expected: 5);
 
                IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = projectGraph.GetTargetLists(entryProjectTargets: null);
                targetLists.Count.ShouldBe(expected: projectGraph.ProjectNodes.Count);
                targetLists[key: GetFirstNodeWithProjectNumber(graph: projectGraph, projectNum: 1)].ShouldBe(expected: new[] { "Build" });
                targetLists[key: GetFirstNodeWithProjectNumber(graph: projectGraph, projectNum: 2)].ShouldBe(expected: new[] { "Build" });
                targetLists[key: GetFirstNodeWithProjectNumber(graph: projectGraph, projectNum: 3)].ShouldBe(expected: new[] { "Build" });
                targetLists[key: GetFirstNodeWithProjectNumber(graph: projectGraph, projectNum: 4)].ShouldBe(expected: new[] { "Build" });
                targetLists[key: GetFirstNodeWithProjectNumber(graph: projectGraph, projectNum: 5)].ShouldBe(expected: new[] { "T51", "T2", "T53", "T54", "T3", "D51", "D52", "T4" });
            }
        }
 
        [Fact]
        public void GetTargetsListSupportsTargetsMarkedSkipNonexistentTargets()
        {
            ProjectGraph graph = Helpers.CreateProjectGraph(
                _env,
                dependencyEdges: new Dictionary<int, int[]>
                {
                    { 1, new[] { 2 } },
                },
                extraContentPerProjectNumber: new Dictionary<int, string>
                {
                    {
                        1,
                        @"
                          <ItemGroup>
                            <ProjectReferenceTargets Include='Build' Targets='NonskippableTarget1' />
                            <ProjectReferenceTargets Include='Build' Targets='NonskippableTarget2' SkipNonexistentTargets='false' />
                            <ProjectReferenceTargets Include='Build' Targets='SkippableExistingTarget;SkippableNonexistentTarget' SkipNonexistentTargets='true' />
                            <ProjectReference Include='2.proj' />
                          </ItemGroup>
                          <Target Name='Build'>
                            <MSBuild Projects='2.proj' Targets='NonskippableTarget1; NonskippableTarget2' />
                            <MSBuild Projects='2.proj' Targets='SkippableExistingTarget;SkippableNonexistentTarget' SkipNonexistentTargets='true' />
                          </Target>"
                    },
                    {
                        2,
                        @"<Target Name='NonskippableTarget1'>
                          </Target>
                          <Target Name='NonskippableTarget2'>
                          </Target>
                          <Target Name='SkippableExistingTarget'>
                          </Target>"
                    },
                });
            IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = graph.GetTargetLists(entryProjectTargets: new[] { "Build" });
            targetLists[key: GetFirstNodeWithProjectNumber(graph: graph, projectNum: 1)].ShouldBe(expected: new[] { "Build" });
            targetLists[key: GetFirstNodeWithProjectNumber(graph: graph, projectNum: 2)].ShouldBe(expected: new[] { "NonskippableTarget1", "NonskippableTarget2", "SkippableExistingTarget" });
            Dictionary<string, (BuildResult Result, MockLogger Logger)> results = ResultCacheBasedBuilds_Tests.BuildUsingCaches(
                _env,
                topoSortedNodes: graph.ProjectNodesTopologicallySorted,
                generateCacheFiles: true,
                expectedNodeBuildOutput: new Dictionary<ProjectGraphNode, string[]>(),
                outputCaches: new Dictionary<ProjectGraphNode, string>(),
                assertBuildResults: false,
                targetListsPerNode: targetLists);
            foreach (KeyValuePair<string, (BuildResult Result, MockLogger Logger)> result in results)
            {
                result.Value.Result.OverallResult.ShouldBe(BuildResultCode.Success);
            }
        }
 
        [Fact]
        public void SkipNonexistentTargetsDoesNotHideMissedTargetResults()
        {
            ProjectGraph graph = Helpers.CreateProjectGraph(
                env: _env,
                dependencyEdges: new Dictionary<int, int[]>
                {
                    { 1, new[] { 2 } },
                },
                extraContentPerProjectNumber: new Dictionary<int, string>
                {
                    {
                        1,
                        @"
                          <ItemGroup>
                            <!-- NonskippableTarget2 is not specified in the target protocol, which should cause the build to fail -->
                            <ProjectReferenceTargets Include='Build' Targets='NonskippableTarget1;SkippableExistingTarget;SkippableNonexistentTarget' SkipNonexistentTargets='true' />
                            <ProjectReference Include='2.proj' />
                          </ItemGroup>
                          <Target Name='Build'>
                            <MSBuild Projects='2.proj' Targets='NonskippableTarget1;NonskippableTarget2;SkippableExistingTarget;SkippableNonexistentTarget' SkipNonexistentTargets='true' />
                          </Target>"
                    },
                    {
                        2,
                        @"<Target Name='NonskippableTarget1'>
                          </Target>
                          <Target Name='NonskippableTarget2'>
                          </Target>
                          <Target Name='SkippableExistingTarget'>
                          </Target>"
                    },
                });
            IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = graph.GetTargetLists(entryProjectTargets: new[] { "Build" });
            targetLists[key: GetFirstNodeWithProjectNumber(graph: graph, projectNum: 1)].ShouldBe(expected: new[] { "Build" });
            targetLists[key: GetFirstNodeWithProjectNumber(graph: graph, projectNum: 2)].ShouldBe(expected: new[] { "NonskippableTarget1", "SkippableExistingTarget" });
            Dictionary<string, (BuildResult Result, MockLogger Logger)> results = ResultCacheBasedBuilds_Tests.BuildUsingCaches(
                env: _env,
                topoSortedNodes: graph.ProjectNodesTopologicallySorted,
                generateCacheFiles: true,
                expectedNodeBuildOutput: new Dictionary<ProjectGraphNode, string[]>(),
                outputCaches: new Dictionary<ProjectGraphNode, string>(),
                assertBuildResults: false,
                targetListsPerNode: targetLists);
            results["2"].Result.OverallResult.ShouldBe(BuildResultCode.Success);
            BuildResult project1BuildResult = results["1"].Result;
            project1BuildResult.OverallResult.ShouldBe(BuildResultCode.Failure);
            MockLogger project1MockLogger = results["1"].Logger;
            project1MockLogger.ErrorCount.ShouldBe(1);
            string project1ErrorMessage = project1MockLogger.Errors.First().Message;
            project1ErrorMessage.ShouldContain("MSB4252");
            project1ErrorMessage.ShouldContain("1.proj");
            project1ErrorMessage.ShouldContain("2.proj");
            project1ErrorMessage.ShouldContain(" with the (NonskippableTarget1;NonskippableTarget2;SkippableExistingTarget;SkippableNonexistentTarget) target(s) but the build result for the built project is not in the engine cache");
        }
 
        [Fact]
        public void ReferencedMultitargetingEntryPointNodeTargetListContainsDefaultTarget()
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile entryProject1 = CreateProjectFile(env, 1, projectReferences: new[] { 2 }, defaultTargets: "A", extraContent: ProjectReferenceTargetsWithMultitargeting);
                TransientTestFile entryProject2 = CreateProjectFile(env, 2, defaultTargets: "A", extraContent: OuterBuildSpecificationWithProjectReferenceTargets);
                var graph = new ProjectGraph(new HashSet<string> { entryProject1.Path, entryProject2.Path });
                IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = graph.GetTargetLists(null);
                targetLists[key: GetOuterBuild(graph, 2)].ShouldBe(expected: OuterBuildTargets.Prepend("A"));
            }
        }
 
        public static IEnumerable<object[]> Graphs
        {
            get
            {
                yield return new object[]
                {
                    new Dictionary<int, int[]>()
                };
 
                yield return new object[]
                {
                    new Dictionary<int, int[]>
                    {
                        {1, null}
                    }
                };
 
                yield return new object[]
                {
                    new Dictionary<int, int[]>
                    {
                        {1, null},
                        {2, null}
                    }
                };
 
                yield return new object[]
                {
                    new Dictionary<int, int[]>
                    {
                        {1, new []{2}}
                    }
                };
 
                yield return new object[]
                {
                    new Dictionary<int, int[]>
                    {
                        {1, new []{2}},
                        {2, null}
                    }
                };
 
                yield return new object[]
                {
                    new Dictionary<int, int[]>
                    {
                        {1, new []{2}},
                        {2, new []{3}}
                    }
                };
 
                yield return new object[]
                {
                    new Dictionary<int, int[]>
                    {
                        {1, new []{2, 3}}
                    }
                };
 
                yield return new object[]
                {
                    new Dictionary<int, int[]>
                    {
                        {1, new []{3, 2}},
                        {2, new []{3}}
                    }
                };
 
                yield return new object[]
                {
                    new Dictionary<int, int[]>
                    {
                        {1, new []{2, 3}},
                        {2, new []{4}},
                        {3, new []{4}}
                    }
                };
 
                yield return new object[]
                {
                    new Dictionary<int, int[]>
                    {
                        {1, new []{4, 3, 2}},
                        {2, new []{4}},
                        {3, new []{4}}
                    }
                };
 
                yield return new object[]
                {
                    new Dictionary<int, int[]>
                    {
                        {1, new []{2}},
                        {3, new []{4}}
                    }
                };
 
                yield return new object[]
                {
                    new Dictionary<int, int[]>
                    {
                        {1, new []{2, 4}},
                        {3, new []{4}}
                    }
                };
 
                yield return new object[]
                {
                    new Dictionary<int, int[]>
                    {
                        {1, new []{4, 2}},
                        {2, new []{3}},
                        {3, new []{4}},
                        {4, new []{5}}
                    }
                };
 
                yield return new object[]
                {
                    new Dictionary<int, int[]>
                    {
                        {1, new []{2, 4}},
                        {2, new []{3}},
                        {3, new []{4}},
                    }
                };
 
                yield return new object[]
                {
                    new Dictionary<int, int[]>
                    {
                        {1, new []{3, 2}},
                        {2, new []{3}},
                        {3, new []{5, 4}},
                        {4, new []{5}},
                    }
                };
 
                yield return new object[]
                {
                    new Dictionary<int, int[]>
                    {
                        {1, new []{2, 4, 3, 5} },
                        {2, new []{5} },
                        {3, new []{5} },
                        {4, new []{6} },
                        {5, new []{7} },
                        {6, new []{5} }
                    },
                };
 
                yield return new object[]
                {
                    new Dictionary<int, int[]>
                    {
                        {1, new []{5, 4, 7}},
                        {2, new []{5}},
                        {3, new []{6, 5}},
                        {4, new []{7}},
                        {5, new []{7, 8}},
                        {6, new []{7, 9}}
                    }
                };
            }
        }
 
        [Theory]
        [MemberData(nameof(Graphs))]
        public void TopologicalSortShouldTopologicallySort(Dictionary<int, int[]> edges)
        {
            using (var env = TestEnvironment.Create())
            {
                var projectGraph = Helpers.CreateProjectGraph(env, edges);
 
                var toposort = projectGraph.ProjectNodesTopologicallySorted.ToArray();
 
                toposort.Length.ShouldBe(projectGraph.ProjectNodes.Count);
 
                for (var i = 0; i < toposort.Length; i++)
                {
                    for (var j = 0; j < i; j++)
                    {
                        // toposort is reversed
                        toposort[i].ReferencingProjects.ShouldNotContain(toposort[j], $"Dependency of node at index {j} found at index {i}");
                    }
                }
            }
        }
 
        [Theory]
        [MemberData(nameof(Graphs))]
        public void DotNotationShouldRepresentGraph(Dictionary<int, int[]> edges)
        {
            var graph = Helpers.CreateProjectGraph(
                _env,
                edges,
                globalProperties: new Dictionary<string, string> { { "a", "b" } },
                createProjectFile: (env, projectId, references, _, _, _) => Helpers.CreateProjectFile(
                    env,
                    projectId,
                    references,
                    projectReferenceTargets: new Dictionary<string, string[]>
                    {
                        {"Build", new[] {$"TargetFrom{projectId}", "Build"}}
                    }));
 
            var targetsPerNode = graph.GetTargetLists(new[] { "Build" });
 
            Func<ProjectGraphNode, string> nodeIdProvider = GetProjectFileName;
 
            var dot = graph.ToDot(nodeIdProvider, targetsPerNode);
 
            var edgeCount = 0;
 
            foreach (var node in graph.ProjectNodes)
            {
                var nodeId = nodeIdProvider(node);
 
                var targets = string.Join(".*", targetsPerNode[node]);
                targets.ShouldNotBeNullOrEmpty();
 
                foreach (var globalProperty in node.ProjectInstance.GlobalProperties)
                {
                    dot.ShouldMatch($@"{nodeId}\s*\[.*{targets}.*{globalProperty.Key}.*{globalProperty.Value}.*\]");
                }
 
                foreach (var reference in node.ProjectReferences)
                {
                    edgeCount++;
                    dot.ShouldMatch($@"{nodeId}\s*->\s*{nodeIdProvider(reference)}");
                }
            }
 
            // edge count
            Regex.Matches(dot, "->").Count.ShouldBe(edgeCount);
 
            // node count
            Regex.Matches(dot, "label").Count.ShouldBe(graph.ProjectNodes.Count);
        }
 
        [Fact]
        public void OuterBuildAsRootShouldDirectlyReferenceInnerBuilds()
        {
            var projectFile = _env.CreateTestProjectWithFiles($@"<Project>{MultitargetingSpecificationPropertyGroup}</Project>").ProjectFile;
 
            var graph = new ProjectGraph(projectFile);
 
            var dot = graph.ToDot();
 
            graph.ProjectNodes.Count.ShouldBe(3);
 
            var outerBuild = graph.GraphRoots.First();
 
            AssertOuterBuild(outerBuild, graph);
        }
 
        [Fact]
        public void OuterBuildAsNonRootShouldNotReferenceInnerBuilds()
        {
            var entryProject = CreateProjectFile(
                env: _env,
                projectNumber: 1,
                projectReferences: new[] { 2 }).Path;
            CreateProjectFile(
                env: _env,
                projectNumber: 2,
                projectReferences: null,
                projectReferenceTargets: null,
                defaultTargets: null,
                extraContent: MultitargetingSpecificationPropertyGroup);
 
 
            var graph = new ProjectGraph(entryProject);
 
            var dot = graph.ToDot();
 
            graph.ProjectNodes.Count.ShouldBe(4);
 
            var outerBuild = GetOuterBuild(graph, 2);
 
            AssertOuterBuild(outerBuild, graph);
        }
 
        [Fact]
        public void InnerBuildsFromNonRootOuterBuildInheritEdgesToOuterBuild()
        {
            var entryProject = CreateProjectFile(
                env: _env,
                projectNumber: 1,
                projectReferences: null,
                projectReferenceTargets: null,
                defaultTargets: null,
                extraContent: @"
<ItemGroup>
    <ProjectReference Include='2.proj' Foo='Bar' />
</ItemGroup>")
                .Path;
            CreateProjectFile(
                env: _env,
                projectNumber: 2,
                projectReferences: null,
                projectReferenceTargets: null,
                defaultTargets: null,
                extraContent: MultitargetingSpecificationPropertyGroup);
 
 
            var graph = new ProjectGraph(entryProject);
 
            var dot = graph.ToDot();
 
            graph.ProjectNodes.Count.ShouldBe(4);
 
            var outerBuild = GetOuterBuild(graph, 2);
 
            AssertOuterBuild(outerBuild, graph);
 
            var outerBuildReferencingNode = GetFirstNodeWithProjectNumber(graph, 1);
 
            var edgeToOuterBuild = graph.TestOnly_Edges[(outerBuildReferencingNode, GetOuterBuild(graph, 2))];
 
            foreach (var innerBuild in GetInnerBuilds(graph, 2))
            {
                graph.TestOnly_Edges[(outerBuildReferencingNode, innerBuild)].ShouldBe(edgeToOuterBuild);
                edgeToOuterBuild.GetMetadataValue("Foo").ShouldBe("Bar");
            }
        }
 
        [Fact]
        public void DuplicatedInnerBuildMonikersShouldGetDeduplicated()
        {
            // multitarget to duplicate monikers
            var multitargetingSpecification = MultitargetingSpecificationPropertyGroup +
                                              @"<PropertyGroup>
                                                    <InnerBuildProperties>a;a</InnerBuildProperties>
                                                </PropertyGroup>";
 
            var root = CreateProjectFile(_env, 1, new[] { 2 }, null, null, multitargetingSpecification).Path;
            CreateProjectFile(_env, 2, null, null, null, multitargetingSpecification);
 
            var graph = new ProjectGraph(root);
 
            var dot = graph.ToDot();
 
            graph.ProjectNodes.Count.ShouldBe(4);
 
            var rootOuterBuild = GetOuterBuild(graph, 1);
            var nonRootOuterBuild = GetOuterBuild(graph, 2);
 
            AssertOuterBuild(rootOuterBuild, graph, null, 1);
            AssertOuterBuild(nonRootOuterBuild, graph, null, 1);
        }
 
        [Fact]
        public void ReferenceOfMultitargetingProjectShouldNotInheritInnerBuildSpecificGlobalProperties()
        {
            var root = CreateProjectFile(env: _env, projectNumber: 1, projectReferences: new[] { 2 }, projectReferenceTargets: null, defaultTargets: null, extraContent: MultitargetingSpecificationPropertyGroup).Path;
            CreateProjectFile(env: _env, projectNumber: 2);
 
            var graph = new ProjectGraph(root);
 
            var dot = graph.ToDot();
 
            graph.ProjectNodes.Count.ShouldBe(4);
 
            AssertOuterBuild(graph.GraphRoots.First(), graph);
 
            var nonMultitargetingNode = GetFirstNodeWithProjectNumber(graph, 2);
 
            AssertNonMultitargetingNode(nonMultitargetingNode);
        }
 
        [Fact]
        public void InnerBuildAsRootViaLocalPropertyShouldNotPropagateInnerBuildPropertyToReference()
        {
            var innerBuildViaLocalProperty = MultitargetingSpecificationPropertyGroup + $"<PropertyGroup><{InnerBuildPropertyName}>foo</{InnerBuildPropertyName}></PropertyGroup>";
 
            var root = CreateProjectFile(
                env: _env,
                projectNumber: 1,
                projectReferences: new[] { 2 },
                projectReferenceTargets: null,
                defaultTargets: null,
                extraContent: innerBuildViaLocalProperty).Path;
 
            CreateProjectFile(env: _env, projectNumber: 2);
 
            var graph = new ProjectGraph(root);
 
            var dot = graph.ToDot();
 
            graph.ProjectNodes.Count.ShouldBe(2);
 
            AssertInnerBuildEvaluation(graph.GraphRoots.First(), false, new Dictionary<string, string>());
 
            var nonMultitargetingNode = GetFirstNodeWithProjectNumber(graph, 2);
 
            AssertNonMultitargetingNode(nonMultitargetingNode);
        }
 
        [Fact]
        public void InnerBuildAsRootViaGlobalPropertyShouldNotPropagateInnerBuildPropertyToReference()
        {
            var root = CreateProjectFile(env: _env, projectNumber: 1, projectReferences: new[] { 2 }, projectReferenceTargets: null, defaultTargets: null, extraContent: MultitargetingSpecificationPropertyGroup).Path;
            CreateProjectFile(env: _env, projectNumber: 2);
 
            var graph = new ProjectGraph(root, new Dictionary<string, string> { { InnerBuildPropertyName, "foo" } });
 
            var dot = graph.ToDot();
 
            graph.ProjectNodes.Count.ShouldBe(2);
 
            AssertInnerBuildEvaluation(graph.GraphRoots.First(), true, new Dictionary<string, string>());
 
            var nonMultitargetingNode = GetFirstNodeWithProjectNumber(graph, 2);
 
            AssertNonMultitargetingNode(nonMultitargetingNode);
        }
 
        [Fact]
        public void NonMultitargetingProjectsAreCompatibleWithMultitargetingProjects()
        {
            var root = CreateProjectFile(env: _env, projectNumber: 1, projectReferences: new[] { 2, 3 }, projectReferenceTargets: null, defaultTargets: null, extraContent: MultitargetingSpecificationPropertyGroup).Path;
            CreateProjectFile(env: _env, projectNumber: 2, projectReferences: new[] { 4 });
            CreateProjectFile(env: _env, projectNumber: 3, projectReferences: new[] { 4 });
            CreateProjectFile(env: _env, projectNumber: 4, projectReferences: null, projectReferenceTargets: null, defaultTargets: null, extraContent: MultitargetingSpecificationPropertyGroup);
 
            var graph = new ProjectGraph(root);
 
            var dot = graph.ToDot();
 
            graph.ProjectNodes.Count.ShouldBe(8);
 
            AssertOuterBuild(graph.GraphRoots.First(), graph);
            AssertOuterBuild(GetOuterBuild(graph, 4), graph);
 
            AssertNonMultitargetingNode(GetFirstNodeWithProjectNumber(graph, 2));
            AssertNonMultitargetingNode(GetFirstNodeWithProjectNumber(graph, 3));
        }
 
        [Fact]
        public void InnerBuildsCanHaveSeparateReferences()
        {
            var extraInnerBuildReferenceSpec = MultitargetingSpecificationPropertyGroup +
                                          $@"<ItemGroup>
                                                <ProjectReference Condition=`'$({InnerBuildPropertyName})'=='b'` Include=`4.proj;5.proj`/>
                                            </ItemGroup>".Cleanup();
 
            var root = CreateProjectFile(env: _env, projectNumber: 1, projectReferences: new[] { 2, 3 }, projectReferenceTargets: null, defaultTargets: null, extraContent: extraInnerBuildReferenceSpec).Path;
            CreateProjectFile(env: _env, projectNumber: 2, projectReferences: null, projectReferenceTargets: null, defaultTargets: null, extraContent: MultitargetingSpecificationPropertyGroup);
            CreateProjectFile(env: _env, projectNumber: 3);
            CreateProjectFile(env: _env, projectNumber: 4, projectReferences: null, projectReferenceTargets: null, defaultTargets: null, extraContent: MultitargetingSpecificationPropertyGroup);
            CreateProjectFile(env: _env, projectNumber: 5);
 
            var graph = new ProjectGraph(root);
 
            var dot = graph.ToDot();
 
            graph.ProjectNodes.Count.ShouldBe(11);
 
            AssertOuterBuild(graph.GraphRoots.First(), graph);
            AssertOuterBuild(GetOuterBuild(graph, 2), graph);
            AssertOuterBuild(GetOuterBuild(graph, 2), graph);
 
            AssertNonMultitargetingNode(GetFirstNodeWithProjectNumber(graph, 3));
            AssertNonMultitargetingNode(GetFirstNodeWithProjectNumber(graph, 5));
 
            var innerBuildWithCommonReferences = GetNodesWithProjectNumber(graph, 1).First(n => n.ProjectInstance.GlobalProperties.TryGetValue(InnerBuildPropertyName, out string p) && p == "a");
 
            innerBuildWithCommonReferences.ProjectReferences.Count.ShouldBe(4);
            var referenceNumbersSet = innerBuildWithCommonReferences.ProjectReferences.Select(r => Path.GetFileNameWithoutExtension(r.ProjectInstance.FullPath)).ToHashSet();
            referenceNumbersSet.ShouldBeSameIgnoringOrder(new HashSet<string> { "2", "3" });
 
            var innerBuildWithAdditionalReferences = GetNodesWithProjectNumber(graph, 1).First(n => n.ProjectInstance.GlobalProperties.TryGetValue(InnerBuildPropertyName, out string p) && p == "b");
 
            innerBuildWithAdditionalReferences.ProjectReferences.Count.ShouldBe(8);
            referenceNumbersSet = innerBuildWithAdditionalReferences.ProjectReferences.Select(r => Path.GetFileNameWithoutExtension(r.ProjectInstance.FullPath)).ToHashSet();
            referenceNumbersSet.ShouldBeSameIgnoringOrder(new HashSet<string> { "2", "3", "4", "5" });
        }
 
        [Fact]
        public void InnerBuildProducedByOuterBuildCanBeReferencedByAnotherNode()
        {
            var referenceToInnerBuild = $@"<ItemGroup>
                                               <ProjectReference Include='1.proj' Properties='{InnerBuildPropertyName}=a'/>
                                           </ItemGroup>";
 
            var additionalGlobalProperties = new Dictionary<string, string> { { "x", "y" } };
 
            var graph = new ProjectGraph(new[]
            {
                CreateProjectFile(env: _env, projectNumber: 1, projectReferences: null, projectReferenceTargets: null, defaultTargets: null, extraContent: MultitargetingSpecificationPropertyGroup).Path,
                CreateProjectFile(env: _env, projectNumber: 2, projectReferences: null, projectReferenceTargets: null, defaultTargets: null, extraContent: referenceToInnerBuild).Path
            },
            additionalGlobalProperties);
 
            var dot = graph.ToDot();
 
            graph.ProjectNodes.Count.ShouldBe(4);
 
            var outerBuild = graph.GraphRoots.First(i => i.ProjectType == ProjectInterpretation.ProjectType.OuterBuild);
 
            AssertOuterBuild(outerBuild, graph, additionalGlobalProperties);
            AssertNonMultitargetingNode(GetFirstNodeWithProjectNumber(graph, 2), additionalGlobalProperties);
 
            var referencedInnerBuild = GetNodesWithProjectNumber(graph, 1).First(n => n.ProjectInstance.GetPropertyValue(InnerBuildPropertyName) == "a");
 
            var two = GetFirstNodeWithProjectNumber(graph, 2);
 
            two.ProjectReferences.ShouldHaveSingleItem();
            two.ProjectReferences.First().ShouldBe(referencedInnerBuild);
 
            referencedInnerBuild.ReferencingProjects.ShouldBeSameIgnoringOrder(new[] { two, outerBuild });
        }
 
        [Fact]
        public void StandaloneInnerBuildsCanBeReferencedWithoutOuterBuilds()
        {
            var referenceToInnerBuild = $@"<ItemGroup>
                                               <ProjectReference Include='2.proj' Properties='{InnerBuildPropertyName}=a'/>
                                           </ItemGroup>";
 
            var root = CreateProjectFile(env: _env, projectNumber: 1, projectReferences: null, projectReferenceTargets: null, defaultTargets: null, extraContent: referenceToInnerBuild).Path;
            CreateProjectFile(env: _env, projectNumber: 2, projectReferences: new[] { 3 }, projectReferenceTargets: null, defaultTargets: null, extraContent: MultitargetingSpecificationPropertyGroup + $"<PropertyGroup><{InnerBuildPropertyName}>a</{InnerBuildPropertyName}></PropertyGroup>");
            CreateProjectFile(env: _env, projectNumber: 3);
 
            var additionalGlobalProperties = new Dictionary<string, string> { { "x", "y" } };
 
            var graph = new ProjectGraph(root, additionalGlobalProperties);
 
            var dot = graph.ToDot();
 
            graph.ProjectNodes.Count.ShouldBe(3);
 
            var rootNode = graph.GraphRoots.First();
            AssertNonMultitargetingNode(rootNode, additionalGlobalProperties);
 
            rootNode.ProjectReferences.ShouldHaveSingleItem();
            var innerBuildNode = rootNode.ProjectReferences.First();
 
            AssertInnerBuildEvaluation(innerBuildNode, false, additionalGlobalProperties);
 
            innerBuildNode.ProjectReferences.ShouldHaveSingleItem();
            AssertNonMultitargetingNode(innerBuildNode.ProjectReferences.First(), additionalGlobalProperties);
        }
 
        [Fact(Skip = "https://github.com/dotnet/msbuild/issues/4262")]
        public void InnerBuildsProducedByOuterBuildsCanBeReferencedByOtherInnerBuilds()
        {
            var referenceToInnerBuild = $@"<ItemGroup>
                                               <ProjectReference Include='2.proj' Condition=`'$({InnerBuildPropertyName})' == 'a'` Properties='{InnerBuildPropertyName}=a'/>
                                           </ItemGroup>".Cleanup();
 
            var additionalGlobalProperties = new Dictionary<string, string> { { "x", "y" } };
 
            var root = CreateProjectFile(
                env: _env,
                projectNumber: 1,
                projectReferences: null,
                projectReferenceTargets: null,
                defaultTargets: null,
                extraContent: MultitargetingSpecificationPropertyGroup + referenceToInnerBuild)
                .Path;
 
            CreateProjectFile(
                env: _env,
                projectNumber: 2,
                projectReferences: null,
                projectReferenceTargets: null,
                defaultTargets: null,
                extraContent: MultitargetingSpecificationPropertyGroup);
 
            var graph = new ProjectGraph(new[] { root }, additionalGlobalProperties);
 
            var dot = graph.ToDot();
 
            graph.ProjectNodes.Count.ShouldBe(5);
 
            var outerBuild1 = GetOuterBuild(graph, 1);
 
            AssertOuterBuild(outerBuild1, graph, additionalGlobalProperties);
 
            var innerBuild1WithReferenceToInnerBuild2 = outerBuild1.ProjectReferences.FirstOrDefault(n => n.ProjectType == ProjectInterpretation.ProjectType.InnerBuild && n.ProjectInstance.GlobalProperties[InnerBuildPropertyName] == "a");
            innerBuild1WithReferenceToInnerBuild2.ShouldNotBeNull();
 
            var outerBuild2 = GetOuterBuild(graph, 2);
            outerBuild2.ShouldNotBeNull();
 
            var innerBuild2 = GetInnerBuilds(graph, 2).FirstOrDefault();
            innerBuild2.ShouldNotBeNull();
 
            innerBuild2.ProjectInstance.GlobalProperties[InnerBuildPropertyName].ShouldBe("a");
 
            // project 2 has two nodes: the outer build and the referenced inner build
            // the outer build is necessary as the referencing inner build can still call targets on it
            GetNodesWithProjectNumber(graph, 2).Count().ShouldBe(2);
 
            innerBuild1WithReferenceToInnerBuild2.ProjectReferences.ShouldBeSameIgnoringOrder(new[] { outerBuild2, innerBuild2 });
        }
 
        public static IEnumerable<object[]> AllNodesShouldHaveGraphBuildGlobalPropertyData
        {
            get
            {
                var globalVariablesArray = new[]
                {
                    // todo add null
                    new Dictionary<string, string>(),
                    new Dictionary<string, string>
                    {
                        {"a", "b"},
                        {"c", "d"}
                    }
                };
 
                var graph1 = new Dictionary<int, int[]>
                {
                    {1, new[] {3, 2}},
                    {2, new[] {3}},
                    {3, new[] {5, 4}},
                    {4, new[] {5}}
                };
 
                var graph2 = new Dictionary<int, int[]>
                {
                    {1, new[] {5, 4, 7}},
                    {2, new[] {5}},
                    {3, new[] {6, 5}},
                    {4, new[] {7}},
                    {5, new[] {7, 8}},
                    {6, new[] {7, 9}}
                };
 
                foreach (var globalVariables in globalVariablesArray)
                {
                    yield return new object[]
                    {
                        new Dictionary<int, int[]>(),
                        Array.Empty<int>(),
                        globalVariables
                    };
 
                    yield return new object[]
                    {
                        new Dictionary<int, int[]>
                        {
                            {1, null}
                        },
                        new[] {1},
                        globalVariables
                    };
 
                    yield return new object[]
                    {
                        graph1,
                        new[] {1},
                        globalVariables
                    };
 
                    yield return new object[]
                    {
                        graph1,
                        new[] {1, 4, 3},
                        globalVariables
                    };
 
                    yield return new object[]
                    {
                        graph2,
                        new[] {1, 2, 3},
                        globalVariables
                    };
 
                    yield return new object[]
                    {
                        graph2,
                        new[] {1, 2, 6, 4, 3, 7},
                        globalVariables
                    };
                }
            }
        }
 
        [Theory]
        [MemberData(nameof(AllNodesShouldHaveGraphBuildGlobalPropertyData))]
        public void AllNodesShouldHaveGraphBuildGlobalProperty(Dictionary<int, int[]> edges, int[] entryPoints, Dictionary<string, string> globalProperties)
        {
            using (var env = TestEnvironment.Create())
            {
                var projectGraph = Helpers.CreateProjectGraph(env, edges, globalProperties, entryPoints: entryPoints);
 
                var dot = projectGraph.ToDot();
 
                var expectedGlobalProperties = new Dictionary<string, string>(globalProperties);
                expectedGlobalProperties[PropertyNames.IsGraphBuild] = "true";
 
                foreach (var node in projectGraph.ProjectNodes)
                {
                    node.ProjectInstance.GlobalProperties.ShouldBeSameIgnoringOrder(expectedGlobalProperties);
                }
            }
        }
 
        [Fact]
        public void UserValuesForIsGraphBuildGlobalPropertyShouldBePreserved()
        {
            using (var env = TestEnvironment.Create())
            {
                var projectGraph = Helpers.CreateProjectGraph(
                    env,
                    new Dictionary<int, int[]> { { 1, null } },
                    new Dictionary<string, string> { { PropertyNames.IsGraphBuild, "xyz" } });
 
                projectGraph.ProjectNodes.First().ProjectInstance.GlobalProperties[PropertyNames.IsGraphBuild].ShouldBe("xyz");
            }
        }
 
        [Theory]
        [MemberData(nameof(Graphs))]
        public void GraphShouldSupportTransitiveReferences(Dictionary<int, int[]> edges)
        {
            var graph = Helpers.CreateProjectGraph(
                env: _env,
                dependencyEdges: edges,
                extraContentForAllNodes: EnableTransitiveProjectReferencesPropertyGroup);
 
            foreach (var node in graph.ProjectNodes)
            {
                var expectedClosure = ComputeClosure(node);
 
                node.ProjectReferences.ShouldBeSameIgnoringOrder(expectedClosure);
            }
        }
 
        public static IEnumerable<object[]> TransitiveReferencesAreDefinedPerProjectTestData
        {
            get
            {
                yield return new object[]
                {
                    new Dictionary<int, int[]>
                    {
                        {1, new[] {3}},
                        {2, new[] {3}},
                        {3, new[] {4}}
                    },
                    new Dictionary<int, string>(),
                    new Dictionary<int, int[]>
                    {
                        {1, new[] {3}},
                        {2, new[] {3}}
                    }
                };
 
                yield return new object[]
                {
                    new Dictionary<int, int[]>
                    {
                        {1, new[] {2}},
                        {2, new[] {3}},
                        {3, new[] {4}}
                    },
                    new Dictionary<int, string>
                    {
                        {1, EnableTransitiveProjectReferencesPropertyGroup}
                    },
                    new Dictionary<int, int[]>
                    {
                        {1, new[] {2, 3, 4}},
                        {2, new[] {3}},
                        {3, new[] {4}},
                        {4, Array.Empty<int>() }
                    }
                };
 
                yield return new object[]
                {
                    new Dictionary<int, int[]>
                    {
                        {1, new[] {3}},
                        {2, new[] {3}},
                        {3, new[] {4}}
                    },
                    new Dictionary<int, string>
                    {
                        {1, EnableTransitiveProjectReferencesPropertyGroup}
                    },
                    new Dictionary<int, int[]>
                    {
                        {1, new[] {3, 4}},
                        {2, new[] {3}}
                    }
                };
 
                yield return new object[]
                {
                    new Dictionary<int, int[]>
                    {
                        {1, new[] {3}},
                        {2, new[] {3}},
                        {3, new[] {4}}
                    },
                    new Dictionary<int, string>
                    {
                        {1, EnableTransitiveProjectReferencesPropertyGroup},
                        {2, EnableTransitiveProjectReferencesPropertyGroup}
                    },
                    new Dictionary<int, int[]>
                    {
                        {1, new[] {3, 4}},
                        {2, new[] {3, 4}}
                    }
                };
                yield return new object[]
                {
                    new Dictionary<int, int[]>
                    {
                        {1, new[] {2}},
                        {2, new[] {3}},
                        {3, new[] {4}},
                        {4, new[] {5}},
                        {5, new[] {6}}
                    },
                    new Dictionary<int, string>
                    {
                        {1, EnableTransitiveProjectReferencesPropertyGroup},
                        {3, EnableTransitiveProjectReferencesPropertyGroup},
                        {5, EnableTransitiveProjectReferencesPropertyGroup}
                    },
                    new Dictionary<int, int[]>
                    {
                        {1, new[] {2, 3, 4, 5, 6}},
                        {2, new[] {3}},
                        {3, new[] {4, 5, 6}},
                        {4, new[] {5}},
                        {5, new[] {6}},
                        {6, Array.Empty<int>() },
                    }
                };
            }
        }
 
        [Theory]
        [MemberData(nameof(TransitiveReferencesAreDefinedPerProjectTestData))]
        public void TransitiveReferencesAreDefinedPerProject(
            Dictionary<int, int[]> edges,
            Dictionary<int, string> extraContentPerProjectNumber,
            Dictionary<int, int[]> expectedReferences)
        {
            var graph = Helpers.CreateProjectGraph(
                env: _env,
                dependencyEdges: edges,
                extraContentPerProjectNumber: extraContentPerProjectNumber);
 
            graph.AssertReferencesIgnoringOrder(expectedReferences);
        }
 
        [Fact]
        public void TransitiveReferencesShouldNotBeAddedToOuterBuilds()
        {
            var graph = Helpers.CreateProjectGraph(
                env: _env,
                dependencyEdges: new Dictionary<int, int[]>
                {
                    {1, new []{3, 4} },
                    {2, new []{3, 4} },
                    {3, new []{4} },
                    {4, new []{5} },
                    {5, new []{6} }
                },
                extraContentPerProjectNumber: new Dictionary<int, string>
                {
                    {
                        1,
                        EnableTransitiveProjectReferencesPropertyGroup +
                        MultitargetingSpecificationPropertyGroup
                    },
                    {
                        2,
                        EnableTransitiveProjectReferencesPropertyGroup +
                        HardCodedInnerBuildWithMultitargetingSpecification
                    },
                    {
                        4,
                        EnableTransitiveProjectReferencesPropertyGroup +
                        MultitargetingSpecificationPropertyGroup
                    },
                    {
                        5,
                        HardCodedInnerBuildWithMultitargetingSpecification
                    },
                    {
                        6,
                        MultitargetingSpecificationPropertyGroup
                    }
                });
 
            GetOuterBuild(graph, 1).AssertReferencesIgnoringOrder(new[] { 1, 1 });
 
            var innerBuilds1 = GetInnerBuilds(graph, 1);
            innerBuilds1.Count.ShouldBe(2);
 
            foreach (var innerBuild in innerBuilds1)
            {
                innerBuild.AssertReferencesIgnoringOrder(new[] { 3, 4, 4, 4, 5, 6, 6, 6 });
            }
 
            GetFirstNodeWithProjectNumber(graph, 2).AssertReferencesIgnoringOrder(new[] { 3, 4, 4, 4, 5, 6, 6, 6 });
 
            GetOuterBuild(graph, 4).AssertReferencesIgnoringOrder(new[] { 4, 4 });
 
            var innerBuilds4 = GetInnerBuilds(graph, 4);
            innerBuilds4.Count.ShouldBe(2);
 
            foreach (var innerBuild in innerBuilds4)
            {
                innerBuild.AssertReferencesIgnoringOrder(new[] { 5, 6, 6, 6 });
            }
        }
 
        [Fact]
        public void TransitiveReferencesShouldNotOverwriteMultitargetingEdges()
        {
            var graph = Helpers.CreateProjectGraph(
                env: _env,
                dependencyEdges: new Dictionary<int, int[]>()
                {
                    {1, new[] {2}},
                    {2, new[] {3}}
                },
                extraContentPerProjectNumber: new Dictionary<int, string>()
                {
                    {
                        1,
                        $@"
<PropertyGroup>
    <{InnerBuildPropertiesName}>1A;1B</{InnerBuildPropertiesName}>
</PropertyGroup>
 
<ItemGroup>
    <ProjectReference Update='@(ProjectReference)' Targets='1ATarget' Condition=`'$({InnerBuildPropertyName})' == '1A'` />
    <ProjectReference Update='@(ProjectReference)' Targets='1BTarget' Condition=`'$({InnerBuildPropertyName})' == '1B'` />
</ItemGroup>"
                    },
                    {
                        2,
                        $@"
<PropertyGroup>
    <{InnerBuildPropertiesName}>2A;2B</{InnerBuildPropertiesName}>
</PropertyGroup>
 
<ItemGroup>
    <ProjectReference Update='@(ProjectReference)' Targets='2ATarget' Condition=`'$({InnerBuildPropertyName})' == '2A'` />
    <ProjectReference Update='@(ProjectReference)' Targets='2BTarget' Condition=`'$({InnerBuildPropertyName})' == '2B'` />
</ItemGroup>"
                    },
                    {
                        3,
                        $@"
<PropertyGroup>
    <{InnerBuildPropertiesName}>3A;3B</{InnerBuildPropertiesName}>
</PropertyGroup>"
                    }
                },
                extraContentForAllNodes: @$"
<PropertyGroup>
    <{ProjectInterpretation.AddTransitiveProjectReferencesInStaticGraphPropertyName}>true</{ProjectInterpretation.AddTransitiveProjectReferencesInStaticGraphPropertyName}>
</PropertyGroup>
 
<PropertyGroup>
    <InnerBuildProperty>{InnerBuildPropertyName}</InnerBuildProperty>
    <InnerBuildPropertyValues>{InnerBuildPropertiesName}</InnerBuildPropertyValues>
</PropertyGroup>
 
<ItemGroup>
    <ProjectReferenceTargets Include='Build' Targets='Build;{MSBuildConstants.ProjectReferenceTargetsOrDefaultTargetsMarker}' />
    <ProjectReferenceTargets Include='Build' Targets='BuildForOuterBuild' OuterBuild='true' />
</ItemGroup>");
 
            var targetLists = graph.GetTargetLists(new[] { "Build" });
 
            var outerBuild1 = GetOuterBuild(graph, 1);
            targetLists[outerBuild1].ShouldBe(new[] { "Build" });
 
            AssertOuterBuild(outerBuild1, graph, expectedInnerBuildCount: 2);
 
            var innerBuildsFor1 = GetInnerBuilds(graph, 1);
            innerBuildsFor1.Count.ShouldBe(2);
 
            foreach (var inner1 in innerBuildsFor1)
            {
                // Outer build targets are added to inner builds because
                targetLists[inner1].ShouldBe(new[] { "BuildForOuterBuild", "Build" });
            }
 
            var outerBuild2 = GetOuterBuild(graph, 2);
            targetLists[outerBuild2].ShouldBe(new[] { "BuildForOuterBuild" });
            AssertOuterBuild(outerBuild2, graph, expectedInnerBuildCount: 2);
 
            var innerBuildsFor2 = GetInnerBuilds(graph, 2);
            innerBuildsFor2.Count.ShouldBe(2);
 
            foreach (var inner2 in innerBuildsFor2)
            {
                targetLists[inner2].ShouldBe(new[] { "BuildForOuterBuild", "Build", "1ATarget", "1BTarget" });
            }
 
            var outerBuild3 = GetOuterBuild(graph, 3);
            targetLists[outerBuild3].ShouldBe(new[] { "BuildForOuterBuild" });
 
            outerBuild3.ReferencingProjects.Count.ShouldBe(4);
 
            AssertOuterBuild(outerBuild3, graph, expectedInnerBuildCount: 2);
            var innerBuildsFor3 = GetInnerBuilds(graph, 3);
            innerBuildsFor3.Count.ShouldBe(2);
 
            foreach (var inner3 in innerBuildsFor3)
            {
                inner3.ReferencingProjects.Count.ShouldBe(5);
 
                // 3 does not get called with 1ATarget or 1BTarget because those apply only to direct references
                targetLists[inner3]
                    .ShouldBe(new[] { "BuildForOuterBuild", "Build", "2ATarget", "2BTarget" });
            }
        }
 
        [Fact]
        public void DuplicateProjectReferences()
        {
            var graph = Helpers.CreateProjectGraph(
                env: _env,
                dependencyEdges: new Dictionary<int, int[]>()
                {
                    { 1, new[] { 2 } },
                },
                extraContentPerProjectNumber: new Dictionary<int, string>()
                {
                    {
                        1,
                        $@"
<ItemGroup>
    <ProjectReferenceTmp Include='@(ProjectReference)' />
    <ProjectReference Include='@(ProjectReferenceTmp)' />
</ItemGroup>
 
<ItemGroup>
    <ProjectReferenceTargets Include='SomeDefaultTarget1' Targets='{MSBuildConstants.ProjectReferenceTargetsOrDefaultTargetsMarker}' />
</ItemGroup>
 
<Target Name='SomeDefaultTarget1' />
"
                    },
                    {
                        2,
                        @"<Target Name='SomeDefaultTarget2' />"
                    }
                });
 
            IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = graph.GetTargetLists(Array.Empty<string>());
 
            ProjectGraphNode project1 = GetFirstNodeWithProjectNumber(graph, 1);
            ProjectGraphNode project2 = GetFirstNodeWithProjectNumber(graph, 2);
 
            project1.ProjectReferences.ShouldHaveSingleItem().ShouldBe(project2);
            targetLists[project1].ShouldBe(new[] { "SomeDefaultTarget1" });
            targetLists[project2].ShouldBe(new[] { "SomeDefaultTarget2" });
        }
 
        [Fact]
        public void MultipleProjectReferencesSameFileDifferentTargets()
        {
            var graph = Helpers.CreateProjectGraph(
                env: _env,
                dependencyEdges: new Dictionary<int, int[]>()
                {
                    {1, new[] {2}},
                },
                extraContentPerProjectNumber: new Dictionary<int, string>()
                {
                    {
                        1,
                        $@"
<ItemGroup>
    <ProjectReferenceTmp Include='@(ProjectReference)' />
    <ProjectReference Include='@(ProjectReferenceTmp)' Targets='SomeOtherTarget' />
</ItemGroup>
 
<ItemGroup>
    <ProjectReferenceTargets Include='SomeDefaultTarget1' Targets='{MSBuildConstants.ProjectReferenceTargetsOrDefaultTargetsMarker}' />
</ItemGroup>
 
<Target Name='SomeDefaultTarget1' />
"
                    },
                    {
                        2,
                        @"<Target Name='SomeDefaultTarget2' />"
                    }
                });
 
            IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = graph.GetTargetLists(Array.Empty<string>());
 
            ProjectGraphNode project1 = GetFirstNodeWithProjectNumber(graph, 1);
            ProjectGraphNode project2 = GetFirstNodeWithProjectNumber(graph, 2);
 
            project1.ProjectReferences.ShouldHaveSingleItem().ShouldBe(project2);
            targetLists[project1].ShouldBe(new[] { "SomeDefaultTarget1" });
            targetLists[project2].ShouldBe(new[] { "SomeDefaultTarget2", "SomeOtherTarget" });
        }
 
        [Fact]
        public void MultitargettingTargetsWithBuildProjectReferencesFalse()
        {
            // This test should emulate Microsoft.Managed.After.targets's handling of multitargetting projects.
            ProjectGraph graph = Helpers.CreateProjectGraph(
                env: _env,
                dependencyEdges: new Dictionary<int, int[]>()
                {
                    { 1, new[] { 2 } },
                },
                globalProperties: new Dictionary<string, string> { { "BuildProjectReferences", "false" } },
                extraContentForAllNodes: """
                <PropertyGroup>
                  <TargetFrameworks>netcoreapp3.1;net6.0;net7.0</TargetFrameworks>
                </PropertyGroup>
 
                <PropertyGroup Condition="'$(TargetFrameworks)' != '' and '$(TargetFramework)' == ''">
                  <IsCrossTargetingBuild>true</IsCrossTargetingBuild>
                </PropertyGroup>
 
                <PropertyGroup>
                  <InnerBuildProperty>TargetFramework</InnerBuildProperty>
                  <InnerBuildPropertyValues>TargetFrameworks</InnerBuildPropertyValues>
                </PropertyGroup>
 
                <PropertyGroup Condition="'$(IsGraphBuild)' == 'true' and '$(IsCrossTargetingBuild)' != 'true'">
                  <_MainReferenceTargetForBuild Condition="'$(BuildProjectReferences)' == '' or '$(BuildProjectReferences)' == 'true'">.projectReferenceTargetsOrDefaultTargets</_MainReferenceTargetForBuild>
                  <_MainReferenceTargetForBuild Condition="'$(_MainReferenceTargetForBuild)' == ''">GetTargetPath</_MainReferenceTargetForBuild>
 
                  <ProjectReferenceTargetsForBuild>$(_MainReferenceTargetForBuild);GetNativeManifest;$(_RecursiveTargetForContentCopying);$(ProjectReferenceTargetsForBuild)</ProjectReferenceTargetsForBuild>
                </PropertyGroup>
                <PropertyGroup Condition="'$(IsGraphBuild)' == 'true' and '$(IsCrossTargetingBuild)' == 'true'">
                  <ProjectReferenceTargetsForBuild>.default;$(ProjectReferenceTargetsForBuild)</ProjectReferenceTargetsForBuild>
                </PropertyGroup>
 
                <ItemGroup Condition="'$(IsGraphBuild)' == 'true'">
                  <ProjectReferenceTargets Include="Build" Targets="GetTargetFrameworks" OuterBuild="true" SkipNonexistentTargets="true" Condition="'$(IsCrossTargetingBuild)' != 'true'" />
                  <ProjectReferenceTargets Include="Build" Targets="$(ProjectReferenceTargetsForBuild)" Condition=" '$(ProjectReferenceTargetsForBuild)' != '' " />
 
                  <ProjectReferenceTargets Include="Build" Targets="GetTargetFrameworksWithPlatformForSingleTargetFramework" SkipNonexistentTargets="true" Condition="'$(IsCrossTargetingBuild)' != 'true'" />
                </ItemGroup>
 
                <Target Name="Build" />
                <Target Name="GetTargetPath" />
                <Target Name="GetNativeManifest" />
                <Target Name="GetTargetFrameworks" />
                <Target Name="GetTargetFrameworksWithPlatformForSingleTargetFramework" />
                """);
 
            IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = graph.GetTargetLists(Array.Empty<string>());
 
            targetLists[GetOuterBuild(graph, 1)].ShouldBe(new[] { "Build" });
            foreach (ProjectGraphNode inner in GetInnerBuilds(graph, 1))
            {
                targetLists[inner].ShouldBe(new[] { "Build" });
            }
 
            targetLists[GetOuterBuild(graph, 2)].ShouldBe(new[] { "GetTargetFrameworks" });
            foreach (ProjectGraphNode inner in GetInnerBuilds(graph, 2))
            {
                // GetTargetFrameworks actually shouldn't be here...
                targetLists[inner].ShouldBe(new[] { "GetTargetFrameworks", "GetTargetPath", "GetNativeManifest", "GetTargetFrameworksWithPlatformForSingleTargetFramework" });
            }
        }
 
        [Theory]
        // Built-in targets
        [InlineData(new string[0], new[] { "Project1Default" }, new[] { "Project2Default" })]
        [InlineData(new[] { "Build" }, new[] { "Project1Default" }, new[] { "Project2Default" })]
        [InlineData(new[] { "Rebuild" }, new[] { "Rebuild" }, new[] { "Rebuild" })]
        [InlineData(new[] { "Clean" }, new[] { "Clean" }, new[] { "Clean" })]
        [InlineData(new[] { "Publish" }, new[] { "Publish" }, new[] { "Publish" })]
        // Traversal targets
        [InlineData(new[] { "Project1" }, new[] { "Project1Default" }, new string[0])]
        [InlineData(new[] { "Project2" }, new string[0], new[] { "Project2Default" })]
        [InlineData(new[] { "Project1", "Project2" }, new[] { "Project1Default" }, new[] { "Project2Default" })]
        [InlineData(new[] { "Project1:Rebuild" }, new[] { "Rebuild" }, new string[0])]
        [InlineData(new[] { "Project2:Rebuild" }, new string[0], new[] { "Rebuild" })]
        [InlineData(new[] { "Project1:Rebuild", "Project2:Clean" }, new[] { "Rebuild" }, new[] { "Clean" })]
        [InlineData(new[] { "CustomTarget" }, new[] { "CustomTarget" }, new[] { "CustomTarget" })]
        [InlineData(new[] { "Project1:CustomTarget" }, new[] { "CustomTarget" }, new string[0])]
        [InlineData(new[] { "Project2:CustomTarget" }, new string[0], new[] { "CustomTarget" })]
        [InlineData(new[] { "Project1:CustomTarget", "Project2:CustomTarget" }, new[] { "CustomTarget" }, new[] { "CustomTarget" })]
        public void GetTargetListsWithSolution(string[] entryTargets, string[] expectedProject1Targets, string[] expectedProject2Targets)
        {
            using (var env = TestEnvironment.Create())
            {
                const string ExtraContent = """
                    <Target Name="CustomTarget" />
                    """;
                TransientTestFile project1File = CreateProjectFile(env: env, projectNumber: 1, defaultTargets: "Project1Default", extraContent: ExtraContent);
                TransientTestFile project2File = CreateProjectFile(env: env, projectNumber: 2, defaultTargets: "Project2Default", extraContent: ExtraContent);
 
                string solutionFileContents = $$"""
                    Microsoft Visual Studio Solution File, Format Version 12.00
                    # Visual Studio Version 17
                    VisualStudioVersion = 17.0.31903.59
                    MinimumVisualStudioVersion = 17.0.31903.59
                    Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Project1", "{{project1File.Path}}", "{8761499A-7280-43C4-A32F-7F41C47CA6DF}"
                    EndProject
                    Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Project2", "{{project2File.Path}}", "{2022C11A-1405-4983-BEC2-3A8B0233108F}"
                    EndProject
                    Global
                        GlobalSection(SolutionConfigurationPlatforms) = preSolution
                            Debug|x64 = Debug|x64
                            Release|x64 = Release|x64
                        EndGlobalSection
                        GlobalSection(ProjectConfigurationPlatforms) = postSolution
                            {8761499A-7280-43C4-A32F-7F41C47CA6DF}.Debug|x64.ActiveCfg = Debug|x64
                            {8761499A-7280-43C4-A32F-7F41C47CA6DF}.Debug|x64.Build.0 = Debug|x64
                            {8761499A-7280-43C4-A32F-7F41C47CA6DF}.Release|x64.ActiveCfg = Release|x64
                            {8761499A-7280-43C4-A32F-7F41C47CA6DF}.Release|x64.Build.0 = Release|x64
                            {2022C11A-1405-4983-BEC2-3A8B0233108F}.Debug|x64.ActiveCfg = Debug|x64
                            {2022C11A-1405-4983-BEC2-3A8B0233108F}.Debug|x64.Build.0 = Debug|x64
                            {2022C11A-1405-4983-BEC2-3A8B0233108F}.Release|x64.ActiveCfg = Release|x64
                            {2022C11A-1405-4983-BEC2-3A8B0233108F}.Release|x64.Build.0 = Release|x64
                        EndGlobalSection
                        GlobalSection(SolutionProperties) = preSolution
                            HideSolutionNode = FALSE
                        EndGlobalSection
                    EndGlobal
                    """;
                TransientTestFile slnFile = env.CreateFile(@"Solution.sln", solutionFileContents);
                SolutionFile solutionFile = SolutionFile.Parse(slnFile.Path);
 
                ProjectGraph projectGraph = new(slnFile.Path);
                ProjectGraphNode project1Node = GetFirstNodeWithProjectNumber(projectGraph, 1);
                ProjectGraphNode project2Node = GetFirstNodeWithProjectNumber(projectGraph, 2);
 
                IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = projectGraph.GetTargetLists(entryTargets);
                targetLists.Count.ShouldBe(projectGraph.ProjectNodes.Count);
                targetLists[project1Node].ShouldBe(expectedProject1Targets);
                targetLists[project2Node].ShouldBe(expectedProject2Targets);
            }
        }
 
        [Theory]
        [InlineData("Project1:Build")]
        [InlineData("Project1:")]
        public void GetTargetListsWithSolutionInvalidTargets(string entryTarget)
        {
            using (var env = TestEnvironment.Create())
            {
                TransientTestFile project1File = CreateProjectFile(env: env, projectNumber: 1);
                string solutionFileContents = $$"""
                    Microsoft Visual Studio Solution File, Format Version 12.00
                    # Visual Studio Version 17
                    VisualStudioVersion = 17.0.31903.59
                    MinimumVisualStudioVersion = 17.0.31903.59
                    Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Project1", "{{project1File.Path}}", "{8761499A-7280-43C4-A32F-7F41C47CA6DF}"
                    EndProject
                    Global
                        GlobalSection(SolutionConfigurationPlatforms) = preSolution
                            Debug|x64 = Debug|x64
                            Release|x64 = Release|x64
                        EndGlobalSection
                        GlobalSection(ProjectConfigurationPlatforms) = postSolution
                            {8761499A-7280-43C4-A32F-7F41C47CA6DF}.Debug|x64.ActiveCfg = Debug|x64
                            {8761499A-7280-43C4-A32F-7F41C47CA6DF}.Debug|x64.Build.0 = Debug|x64
                            {8761499A-7280-43C4-A32F-7F41C47CA6DF}.Release|x64.ActiveCfg = Release|x64
                            {8761499A-7280-43C4-A32F-7F41C47CA6DF}.Release|x64.Build.0 = Release|x64
                        EndGlobalSection
                        GlobalSection(SolutionProperties) = preSolution
                            HideSolutionNode = FALSE
                        EndGlobalSection
                    EndGlobal
                    """;
                TransientTestFile slnFile = env.CreateFile(@"Solution.sln", solutionFileContents);
                SolutionFile solutionFile = SolutionFile.Parse(slnFile.Path);
 
                ProjectGraph projectGraph = new(slnFile.Path);
 
                var getTargetListsFunc = (() => projectGraph.GetTargetLists([entryTarget]));
                InvalidProjectFileException exception = getTargetListsFunc.ShouldThrow<InvalidProjectFileException>();
                exception.Message.ShouldContain($"The target \"{entryTarget}\" does not exist in the project.");
            }
        }
 
        public void Dispose()
        {
            _env.Dispose();
        }
    }
}