File: SolutionTests\ProjectDependencyGraphTests.cs
Web Access
Project: src\src\Workspaces\CoreTest\Microsoft.CodeAnalysis.Workspaces.UnitTests.csproj (Microsoft.CodeAnalysis.Workspaces.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.UnitTests;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.Host.UnitTests;
 
[UseExportProvider]
[Trait(Traits.Feature, Traits.Features.Workspace)]
public sealed class ProjectDependencyGraphTests : TestBase
{
    #region GetTopologicallySortedProjects
 
    [Fact]
    public void TestGetTopologicallySortedProjects()
    {
        VerifyTopologicalSort(CreateSolutionFromReferenceMap("A"), "A");
        VerifyTopologicalSort(CreateSolutionFromReferenceMap("A B"), "AB", "BA");
        VerifyTopologicalSort(CreateSolutionFromReferenceMap("C:A,B B:A A"), "ABC");
        VerifyTopologicalSort(CreateSolutionFromReferenceMap("B:A A C:A D:C,B"), "ABCD", "ACBD");
    }
 
    [Fact]
    public void TestTopologicallySortedProjectsIncrementalUpdate()
    {
        var solution = CreateSolutionFromReferenceMap("A");
 
        VerifyTopologicalSort(solution, "A");
 
        solution = AddProject(solution, "B");
 
        VerifyTopologicalSort(solution, "AB", "BA");
    }
 
    /// <summary>
    /// Verifies that <see cref="ProjectDependencyGraph.GetTopologicallySortedProjects(CancellationToken)"/> 
    /// returns one of the correct results.
    /// </summary>
    /// <param name="solution"></param>
    /// <param name="expectedResults">A list of possible results. Because topological sorting is ambiguous
    /// in that a graph could have multiple topological sorts, this helper lets you give all the possible
    /// results and it asserts that one of them does match.</param>
    private static void VerifyTopologicalSort(Solution solution, params string[] expectedResults)
    {
        var projectDependencyGraph = solution.GetProjectDependencyGraph();
        var projectIds = projectDependencyGraph.GetTopologicallySortedProjects(CancellationToken.None);
 
        var actualResult = string.Concat(projectIds.Select(id => solution.GetRequiredProject(id).AssemblyName));
        Assert.Contains<string>(actualResult, expectedResults);
    }
 
    #endregion
 
    #region Dependency Sets
 
    [Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/542438")]
    public void TestGetDependencySets()
    {
        VerifyDependencySets(CreateSolutionFromReferenceMap("A B:A C:A D E:D F:D"), "ABC DEF");
        VerifyDependencySets(CreateSolutionFromReferenceMap("A B:A,C C"), "ABC");
        VerifyDependencySets(CreateSolutionFromReferenceMap("A B"), "A B");
        VerifyDependencySets(CreateSolutionFromReferenceMap("A B C:B"), "A BC");
        VerifyDependencySets(CreateSolutionFromReferenceMap("A B:A C:A D:B,C"), "ABCD");
    }
 
    [Fact]
    public void TestDependencySetsIncrementalUpdate()
    {
        var solution = CreateSolutionFromReferenceMap("A");
 
        VerifyDependencySets(solution, "A");
 
        solution = AddProject(solution, "B");
 
        VerifyDependencySets(solution, "A B");
    }
 
    private static void VerifyDependencySets(Solution solution, string expectedResult)
    {
        var projectDependencyGraph = solution.GetProjectDependencyGraph();
        var projectIds = projectDependencyGraph.GetDependencySets(CancellationToken.None);
        var actualResult = string.Join(" ",
            projectIds.Select(
                group => string.Concat(
                    group.Select(p => solution.GetRequiredProject(p).AssemblyName).OrderBy(n => n))).OrderBy(n => n));
        Assert.Equal(expectedResult, actualResult);
    }
 
    #endregion
 
    #region GetProjectsThatThisProjectTransitivelyDependsOn
 
    [Fact]
    public void TestGetProjectsThatThisProjectTransitivelyDependsOn()
    {
        VerifyTransitiveReferences(CreateSolutionFromReferenceMap("A"), "A", []);
        VerifyTransitiveReferences(CreateSolutionFromReferenceMap("B:A A"), "B", ["A"]);
        VerifyTransitiveReferences(CreateSolutionFromReferenceMap("C:B B:A A"), "C", ["B", "A"]);
        VerifyTransitiveReferences(CreateSolutionFromReferenceMap("C:B B:A A"), "A", []);
    }
 
    [Fact]
    public void TestGetProjectsThatThisProjectTransitivelyDependsOnThrowsArgumentNull()
    {
        var solution = CreateSolutionFromReferenceMap("");
 
        Assert.Throws<ArgumentNullException>("projectId",
            () => solution.GetProjectDependencyGraph().GetProjectsThatThisProjectDirectlyDependsOn(null!));
    }
 
    [Fact]
    public void TestTransitiveReferencesIncrementalUpdateInMiddle()
    {
        // We are going to create a solution with the references:
        //
        // A -> B -> C -> D
        //
        // but we will add the B -> C link last, to verify that when we add the B to C link we update the references of A.
 
        var solution = CreateSolutionFromReferenceMap("A B C D");
        VerifyTransitiveReferences(solution, "A", []);
        VerifyTransitiveReferences(solution, "B", []);
        VerifyTransitiveReferences(solution, "C", []);
        VerifyTransitiveReferences(solution, "D", []);
 
        solution = AddProjectReferences(solution, "A", new string[] { "B" });
        solution = AddProjectReferences(solution, "C", new string[] { "D" });
 
        VerifyDirectReferences(solution, "A", ["B"]);
        VerifyDirectReferences(solution, "C", ["D"]);
 
        VerifyTransitiveReferences(solution, "A", ["B"]);
        VerifyTransitiveReferences(solution, "B", []);
        VerifyTransitiveReferences(solution, "C", ["D"]);
        VerifyTransitiveReferences(solution, "D", []);
 
        solution = AddProjectReferences(solution, "B", new string[] { "C" });
 
        VerifyDirectReferences(solution, "B", ["C"]);
 
        VerifyTransitiveReferences(solution, "A", ["B", "C", "D"]);
        VerifyTransitiveReferences(solution, "B", ["C", "D"]);
        VerifyTransitiveReferences(solution, "C", ["D"]);
        VerifyTransitiveReferences(solution, "D", []);
    }
 
    [Fact]
    public void TestTransitiveReferencesIncrementalUpdateInMiddleLongerChain()
    {
        // We are going to create a solution with the references:
        //
        // A -> B -> C   D -> E -> F
        //
        // but we will add the C-> D link last, to verify that when we add the C to D link we update the references of A. This is similar
        // to the previous test but with a longer chain.
 
        var solution = CreateSolutionFromReferenceMap("A:B B:C C D:E E:F F");
        VerifyTransitiveReferences(solution, "A", ["B", "C"]);
        VerifyTransitiveReferences(solution, "B", ["C"]);
        VerifyTransitiveReferences(solution, "D", ["E", "F"]);
        VerifyTransitiveReferences(solution, "E", ["F"]);
 
        solution = AddProjectReferences(solution, "C", new string[] { "D" });
 
        VerifyTransitiveReferences(solution, "A", ["B", "C", "D", "E", "F"]);
    }
 
    [Fact]
    public void TestTransitiveReferencesIncrementalUpdateWithReferencesAlreadyTransitivelyIncluded()
    {
        // We are going to create a solution with the references:
        //
        // A -> B -> C
        //
        // and then we'll add a reference from A -> C, and transitive references should be different
 
        var solution = CreateSolutionFromReferenceMap("A:B B:C C");
 
        void VerifyAllTransitiveReferences()
        {
            VerifyTransitiveReferences(solution, "A", ["B", "C"]);
            VerifyTransitiveReferences(solution, "B", ["C"]);
            VerifyTransitiveReferences(solution, "C", []);
        }
 
        VerifyAllTransitiveReferences();
        VerifyDirectReferences(solution, "A", ["B"]);
 
        solution = AddProjectReferences(solution, "A", new string[] { "C" });
 
        VerifyAllTransitiveReferences();
        VerifyDirectReferences(solution, "A", ["B", "C"]);
    }
 
    [Fact]
    public void TestTransitiveReferencesIncrementalUpdateWithProjectThatHasUnknownReferences()
    {
        // We are going to create a solution with the references:
        //
        // A  B  C -> D
        //
        // and then we will add a link from A to B. We won't ask for transitive references first,
        // so we shouldn't have any information for A, B, or C and have to deal with that.
 
        var solution = CreateSolutionFromReferenceMap("A B C:D D");
        solution = solution.WithProjectReferences(solution.GetProjectsByName("C").Single().Id, []);
 
        VerifyTransitiveReferences(solution, "A", []);
 
        // At this point, we know the references for "A" (it's empty), but B and C's are still unknown.
        // At this point, we're also going to directly use the underlying project graph APIs;
        // the higher level solution APIs often call and ask for transitive information as well, which makes
        // this particularly hard to test -- it turns out the data we think is uncomputed might be computed prior
        // to adding the reference.
        var dependencyGraph = solution.GetProjectDependencyGraph();
        var projectAId = solution.GetProjectsByName("A").Single().Id;
        var projectBId = solution.GetProjectsByName("B").Single().Id;
        dependencyGraph = dependencyGraph.WithAdditionalProjectReferences(projectAId, [new ProjectReference(projectBId)]);
 
        VerifyTransitiveReferences(solution, dependencyGraph, project: "A", expectedResults: ["B"]);
    }
 
    [Fact]
    public void TestTransitiveReferencesWithDanglingProjectReference()
    {
        // We are going to create a solution with the references:
        //
        // A -> B
        //
        // but we're going to add A as a reference with B not existing yet. Then we'll add in B and ask.
 
        var solution = CreateSolution();
        var projectAId = ProjectId.CreateNewId("A");
        var projectBId = ProjectId.CreateNewId("B");
 
        var projectAInfo = ProjectInfo.Create(projectAId, VersionStamp.Create(), "A", "A", LanguageNames.CSharp,
                                projectReferences: [new ProjectReference(projectBId)]);
 
        solution = solution.AddProject(projectAInfo);
 
        VerifyDirectReferences(solution, "A", []);
        VerifyTransitiveReferences(solution, "A", []);
 
        solution = solution.AddProject(projectBId, "B", "B", LanguageNames.CSharp);
 
        VerifyTransitiveReferences(solution, "A", ["B"]);
    }
 
    [Fact]
    public void TestTransitiveReferencesWithMultipleReferences()
    {
        // We are going to create a solution with the references:
        //
        // A    B -> C    D -> E
        //
        // and then add A referencing B and D in one call, to make sure that works.
 
        var solution = CreateSolutionFromReferenceMap("A B:C C D:E E");
        VerifyTransitiveReferences(solution, "A", []);
 
        solution = AddProjectReferences(solution, "A", new string[] { "B", "D" });
 
        VerifyDirectReferences(solution, "A", ["B", "D"]);
        VerifyTransitiveReferences(solution, "A", ["B", "C", "D", "E"]);
    }
 
    private static void VerifyDirectReferences(Solution solution, string project, string[] expectedResults)
    {
        var projectDependencyGraph = solution.GetProjectDependencyGraph();
        var projectId = solution.GetProjectsByName(project).Single().Id;
        var projectIds = projectDependencyGraph.GetProjectsThatThisProjectDirectlyDependsOn(projectId);
 
        var actualResults = projectIds.Select(id => solution.GetRequiredProject(id).Name);
        Assert.Equal<string>(
            expectedResults.OrderBy(n => n),
            actualResults.OrderBy(n => n));
    }
 
    private static void VerifyTransitiveReferences(Solution solution, string project, string[] expectedResults)
    {
        var projectDependencyGraph = solution.GetProjectDependencyGraph();
        VerifyTransitiveReferences(solution, projectDependencyGraph, project, expectedResults);
    }
 
    private static void VerifyTransitiveReferences(Solution solution, ProjectDependencyGraph projectDependencyGraph, string project, string[] expectedResults)
    {
        var projectId = solution.GetProjectsByName(project).Single().Id;
        var projectIds = projectDependencyGraph.GetProjectsThatThisProjectTransitivelyDependsOn(projectId);
 
        var actualResults = projectIds.Select(id => solution.GetRequiredProject(id).Name);
        Assert.Equal<string>(
            expectedResults.OrderBy(n => n),
            actualResults.OrderBy(n => n));
    }
 
    #endregion
 
    [Fact]
    public void TestDirectAndReverseDirectReferencesAfterWithProjectReferences()
    {
        var solution = CreateSolutionFromReferenceMap("A:B B");
 
        VerifyDirectReverseReferences(solution, "B", ["A"]);
 
        solution = solution.WithProjectReferences(solution.GetProjectsByName("A").Single().Id,
            []);
 
        VerifyDirectReferences(solution, "A", []);
        VerifyDirectReverseReferences(solution, "B", []);
    }
 
    #region GetProjectsThatTransitivelyDependOnThisProject
 
    [Fact]
    public void TestGetProjectsThatTransitivelyDependOnThisProject()
    {
        VerifyReverseTransitiveReferences(CreateSolutionFromReferenceMap("A"), "A", []);
        VerifyReverseTransitiveReferences(CreateSolutionFromReferenceMap("B:A A"), "A", ["B"]);
        VerifyReverseTransitiveReferences(CreateSolutionFromReferenceMap("C:B B:A A"), "A", ["B", "C"]);
        VerifyReverseTransitiveReferences(CreateSolutionFromReferenceMap("C:B B:A A"), "C", []);
        VerifyReverseTransitiveReferences(CreateSolutionFromReferenceMap("D:C,B B:A C A"), "A", ["D", "B"]);
    }
 
    [Fact]
    public void TestGetProjectsThatTransitivelyDependOnThisProjectThrowsArgumentNull()
    {
        var solution = CreateSolutionFromReferenceMap("");
 
        Assert.Throws<ArgumentNullException>("projectId",
            () => solution.GetProjectDependencyGraph().GetProjectsThatTransitivelyDependOnThisProject(null!));
    }
 
    [Fact]
    public void TestReverseTransitiveReferencesIncrementalUpdateInMiddle()
    {
        // We are going to create a solution with the references:
        //
        // A -> B -> C -> D
        //
        // but we will add the B -> C link last, to verify that when we add the B to C link we update the reverse references of D.
 
        var solution = CreateSolutionFromReferenceMap("A B C D");
        VerifyReverseTransitiveReferences(solution, "A", []);
        VerifyReverseTransitiveReferences(solution, "B", []);
        VerifyReverseTransitiveReferences(solution, "C", []);
        VerifyReverseTransitiveReferences(solution, "D", []);
 
        solution = AddProjectReferences(solution, "A", new string[] { "B" });
        solution = AddProjectReferences(solution, "C", new string[] { "D" });
 
        VerifyDirectReverseReferences(solution, "B", ["A"]);
        VerifyDirectReverseReferences(solution, "D", ["C"]);
 
        VerifyReverseTransitiveReferences(solution, "A", []);
        VerifyReverseTransitiveReferences(solution, "B", ["A"]);
        VerifyReverseTransitiveReferences(solution, "C", []);
        VerifyReverseTransitiveReferences(solution, "D", ["C"]);
 
        solution = AddProjectReferences(solution, "B", new string[] { "C" });
 
        VerifyDirectReverseReferences(solution, "C", ["B"]);
 
        VerifyReverseTransitiveReferences(solution, "A", []);
        VerifyReverseTransitiveReferences(solution, "B", ["A"]);
        VerifyReverseTransitiveReferences(solution, "C", ["A", "B"]);
        VerifyReverseTransitiveReferences(solution, "D", ["A", "B", "C"]);
    }
 
    [Fact]
    public void TestReverseTransitiveReferencesForUnrelatedProjectAfterWithProjectReferences()
    {
        // We are going to create a solution with the references:
        //
        // A -> B       C -> D
        //
        // and will then remove the reference from C to D. This process will cause us to throw out
        // all our caches, and asking for the reverse references of A will compute it again.
 
        var solution = CreateSolutionFromReferenceMap("A:B B C:D D");
        VerifyReverseTransitiveReferences(solution, "A", []);
        VerifyReverseTransitiveReferences(solution, "B", ["A"]);
        VerifyReverseTransitiveReferences(solution, "C", []);
        VerifyReverseTransitiveReferences(solution, "D", ["C"]);
 
        solution = solution.WithProjectReferences(solution.GetProjectsByName("C").Single().Id, []);
 
        VerifyReverseTransitiveReferences(solution, "B", ["A"]);
    }
 
    [Fact]
    public void TestForwardReferencesAfterProjectRemoval()
    {
        // We are going to create a solution with the references:
        //
        // A -> B -> C -> D
        //
        // and will then remove project B.
 
        var solution = CreateSolutionFromReferenceMap("A:B B:C C:D D");
        VerifyDirectReferences(solution, "A", ["B"]);
        VerifyDirectReferences(solution, "B", ["C"]);
        VerifyDirectReferences(solution, "C", ["D"]);
        VerifyDirectReferences(solution, "D", []);
 
        solution = solution.RemoveProject(solution.GetProjectsByName("B").Single().Id);
 
        VerifyDirectReferences(solution, "A", []);
        VerifyDirectReferences(solution, "C", ["D"]);
        VerifyDirectReferences(solution, "D", []);
    }
 
    [Fact]
    public void TestForwardTransitiveReferencesAfterProjectRemoval()
    {
        // We are going to create a solution with the references:
        //
        // A -> B -> C -> D
        //
        // and will then remove project B.
 
        var solution = CreateSolutionFromReferenceMap("A:B B:C C:D D");
        VerifyTransitiveReferences(solution, "A", ["B", "C", "D"]);
        VerifyTransitiveReferences(solution, "B", ["C", "D"]);
        VerifyTransitiveReferences(solution, "C", ["D"]);
        VerifyTransitiveReferences(solution, "D", []);
 
        solution = solution.RemoveProject(solution.GetProjectsByName("B").Single().Id);
 
        VerifyTransitiveReferences(solution, "A", []);
        VerifyTransitiveReferences(solution, "C", ["D"]);
        VerifyTransitiveReferences(solution, "D", []);
    }
 
    [Fact]
    public void TestReverseReferencesAfterProjectRemoval()
    {
        // We are going to create a solution with the references:
        //
        // A -> B -> C -> D
        //
        // and will then remove project B.
 
        var solution = CreateSolutionFromReferenceMap("A:B B:C C:D D");
        VerifyDirectReverseReferences(solution, "A", []);
        VerifyDirectReverseReferences(solution, "B", ["A"]);
        VerifyDirectReverseReferences(solution, "C", ["B"]);
        VerifyDirectReverseReferences(solution, "D", ["C"]);
 
        solution = solution.RemoveProject(solution.GetProjectsByName("B").Single().Id);
 
        VerifyDirectReverseReferences(solution, "A", []);
        VerifyDirectReverseReferences(solution, "C", []);
        VerifyDirectReverseReferences(solution, "D", ["C"]);
    }
 
    [Fact]
    public void TestReverseTransitiveReferencesAfterProjectRemoval()
    {
        // We are going to create a solution with the references:
        //
        // A -> B -> C -> D
        //
        // and will then remove project B.
 
        var solution = CreateSolutionFromReferenceMap("A:B B:C C:D D");
        VerifyReverseTransitiveReferences(solution, "A", []);
        VerifyReverseTransitiveReferences(solution, "B", ["A"]);
        VerifyReverseTransitiveReferences(solution, "C", ["A", "B"]);
        VerifyReverseTransitiveReferences(solution, "D", ["A", "B", "C"]);
 
        solution = solution.RemoveProject(solution.GetProjectsByName("B").Single().Id);
 
        VerifyReverseTransitiveReferences(solution, "A", []);
        VerifyReverseTransitiveReferences(solution, "C", []);
        VerifyReverseTransitiveReferences(solution, "D", ["C"]);
    }
 
    [Fact]
    public void TestReverseTransitiveReferencesAfterProjectReferenceRemoval_PreserveThroughUnrelatedSequence()
    {
        // We are going to create a solution with the references:
        //
        // A -> B -> C
        //   \
        //    > D
        //
        // and will then remove the project reference A->B. This test verifies that the new project dependency graph
        // did not lose previously-computed information about the transitive reverse references for D.
 
        var solution = CreateSolutionFromReferenceMap("A:B,D B:C C D");
        VerifyReverseTransitiveReferences(solution, "D", ["A"]);
 
        var a = solution.GetProjectsByName("A").Single();
        var b = solution.GetProjectsByName("B").Single();
        var d = solution.GetProjectsByName("D").Single();
        var expected = solution.SolutionState.GetProjectDependencyGraph().GetProjectsThatTransitivelyDependOnThisProject(d.Id);
 
        var aToB = a.ProjectReferences.Single(reference => reference.ProjectId == b.Id);
        solution = solution.RemoveProjectReference(a.Id, aToB);
 
        // Before any other operations, verify that TryGetProjectsThatTransitivelyDependOnThisProject returns a
        // non-null set. Specifically, it returns the _same_ set that was computed prior to the project reference
        // removal.
        Assert.Same(expected, solution.SolutionState.GetProjectDependencyGraph().GetTestAccessor().TryGetProjectsThatTransitivelyDependOnThisProject(d.Id));
    }
 
    [Fact]
    public void TestReverseTransitiveReferencesAfterProjectReferenceRemoval_PreserveUnrelated()
    {
        // We are going to create a solution with the references:
        //
        // A -> B -> C
        // D -> E
        //
        // and will then remove the project reference A->B. This test verifies that the new project dependency graph
        // did not lose previously-computed information about the transitive reverse references for E.
 
        var solution = CreateSolutionFromReferenceMap("A:B B:C C D:E E");
        VerifyReverseTransitiveReferences(solution, "E", ["D"]);
 
        var a = solution.GetProjectsByName("A").Single();
        var b = solution.GetProjectsByName("B").Single();
        var e = solution.GetProjectsByName("E").Single();
        var expected = solution.SolutionState.GetProjectDependencyGraph().GetProjectsThatTransitivelyDependOnThisProject(e.Id);
 
        var aToB = a.ProjectReferences.Single(reference => reference.ProjectId == b.Id);
        solution = solution.RemoveProjectReference(a.Id, aToB);
 
        // Before any other operations, verify that TryGetProjectsThatTransitivelyDependOnThisProject returns a
        // non-null set. Specifically, it returns the _same_ set that was computed prior to the project reference
        // removal.
        Assert.Same(expected, solution.SolutionState.GetProjectDependencyGraph().GetTestAccessor().TryGetProjectsThatTransitivelyDependOnThisProject(e.Id));
    }
 
    [Fact]
    public void TestReverseTransitiveReferencesAfterProjectReferenceRemoval_DiscardImpacted()
    {
        // We are going to create a solution with the references:
        //
        // A -> B -> C
        //   \
        //    > D
        //
        // and will then remove the project reference A->B. This test verifies that the new project dependency graph
        // discards previously-computed information about the transitive reverse references for C.
 
        var solution = CreateSolutionFromReferenceMap("A:B,D B:C C D");
        VerifyReverseTransitiveReferences(solution, "C", ["A", "B"]);
 
        var a = solution.GetProjectsByName("A").Single();
        var b = solution.GetProjectsByName("B").Single();
        var c = solution.GetProjectsByName("C").Single();
        var notExpected = solution.SolutionState.GetProjectDependencyGraph().GetProjectsThatTransitivelyDependOnThisProject(c.Id);
        Assert.NotNull(notExpected);
 
        var aToB = a.ProjectReferences.Single(reference => reference.ProjectId == b.Id);
        solution = solution.RemoveProjectReference(a.Id, aToB);
 
        // Before any other operations, verify that TryGetProjectsThatTransitivelyDependOnThisProject returns a
        // null set.
        Assert.Null(solution.SolutionState.GetProjectDependencyGraph().GetTestAccessor().TryGetProjectsThatTransitivelyDependOnThisProject(c.Id));
        VerifyReverseTransitiveReferences(solution, "C", ["B"]);
    }
 
    [Fact]
    public void TestSameDependencyGraphAfterOneOfMultipleReferencesRemoved()
    {
        // We are going to create a solution with the references:
        //
        // A -> B -> C -> D
        //        \__^
        //
        // This solution has multiple references from B->C. We will remove one reference from B->C and verify that
        // the project dependency graph in the solution state did not change (reference equality).
        //
        // We then remove the second reference, and verify that the dependency graph does change.
 
        var solution = CreateSolutionFromReferenceMap("A:B B:C,C C:D D");
 
        VerifyDirectReferences(solution, "A", ["B"]);
        VerifyDirectReferences(solution, "B", ["C"]);
        VerifyDirectReferences(solution, "C", ["D"]);
        VerifyDirectReferences(solution, "D", []);
 
        VerifyTransitiveReferences(solution, "A", ["B", "C", "D"]);
        VerifyTransitiveReferences(solution, "B", ["C", "D"]);
        VerifyTransitiveReferences(solution, "C", ["D"]);
        VerifyTransitiveReferences(solution, "D", []);
 
        VerifyDirectReverseReferences(solution, "A", []);
        VerifyDirectReverseReferences(solution, "B", ["A"]);
        VerifyDirectReverseReferences(solution, "C", ["B"]);
        VerifyDirectReverseReferences(solution, "D", ["C"]);
 
        VerifyReverseTransitiveReferences(solution, "A", []);
        VerifyReverseTransitiveReferences(solution, "B", ["A"]);
        VerifyReverseTransitiveReferences(solution, "C", ["A", "B"]);
        VerifyReverseTransitiveReferences(solution, "D", ["A", "B", "C"]);
 
        var dependencyGraph = solution.SolutionState.GetProjectDependencyGraph();
        Assert.NotNull(dependencyGraph);
 
        var b = solution.GetProjectsByName("B").Single();
        var c = solution.GetProjectsByName("C").Single();
        var firstBToC = b.ProjectReferences.First(reference => reference.ProjectId == c.Id);
        solution = solution.RemoveProjectReference(b.Id, firstBToC);
        Assert.Same(dependencyGraph, solution.SolutionState.GetProjectDependencyGraph());
 
        b = solution.GetProjectsByName("B").Single();
        var remainingBToC = b.ProjectReferences.Single(reference => reference.ProjectId == c.Id);
        solution = solution.RemoveProjectReference(b.Id, remainingBToC);
        Assert.NotSame(dependencyGraph, solution.SolutionState.GetProjectDependencyGraph());
 
        VerifyDirectReferences(solution, "A", ["B"]);
        VerifyDirectReferences(solution, "B", []);
        VerifyDirectReferences(solution, "C", ["D"]);
        VerifyDirectReferences(solution, "D", []);
 
        VerifyTransitiveReferences(solution, "A", ["B"]);
        VerifyTransitiveReferences(solution, "B", []);
        VerifyTransitiveReferences(solution, "C", ["D"]);
        VerifyTransitiveReferences(solution, "D", []);
 
        VerifyDirectReverseReferences(solution, "A", []);
        VerifyDirectReverseReferences(solution, "B", ["A"]);
        VerifyDirectReverseReferences(solution, "C", []);
        VerifyDirectReverseReferences(solution, "D", ["C"]);
 
        VerifyReverseTransitiveReferences(solution, "A", []);
        VerifyReverseTransitiveReferences(solution, "B", ["A"]);
        VerifyReverseTransitiveReferences(solution, "C", []);
        VerifyReverseTransitiveReferences(solution, "D", ["C"]);
    }
 
    private static void VerifyDirectReverseReferences(Solution solution, string project, string[] expectedResults)
    {
        var projectDependencyGraph = solution.GetProjectDependencyGraph();
        var projectId = solution.GetProjectsByName(project).Single().Id;
        var projectIds = projectDependencyGraph.GetProjectsThatDirectlyDependOnThisProject(projectId);
 
        var actualResults = projectIds.Select(id => solution.GetRequiredProject(id).Name);
        Assert.Equal<string>(
            expectedResults.OrderBy(n => n),
            actualResults.OrderBy(n => n));
    }
 
    private static void VerifyReverseTransitiveReferences(Solution solution, string project, string[] expectedResults)
    {
        var projectDependencyGraph = solution.GetProjectDependencyGraph();
        var projectId = solution.GetProjectsByName(project).Single().Id;
        var projectIds = projectDependencyGraph.GetProjectsThatTransitivelyDependOnThisProject(projectId);
 
        var actualResults = projectIds.Select(id => solution.GetRequiredProject(id).Name);
 
        Assert.Equal<string>(
            expectedResults.OrderBy(n => n),
            actualResults.OrderBy(n => n));
    }
 
    #endregion
 
    #region Helpers
 
    private static Solution CreateSolutionFromReferenceMap(string projectReferences)
    {
        var solution = CreateSolution();
 
        var references = new Dictionary<string, IEnumerable<string>>();
 
        var projectDefinitions = projectReferences.Split(' ');
        foreach (var projectDefinition in projectDefinitions)
        {
            var projectDefinitionParts = projectDefinition.Split(':');
            string[]? referencedProjectNames = null;
 
            if (projectDefinitionParts.Length == 2)
            {
                referencedProjectNames = projectDefinitionParts[1].Split(',');
            }
            else if (projectDefinitionParts.Length != 1)
            {
                throw new ArgumentException("Invalid project definition: " + projectDefinition);
            }
 
            var projectName = projectDefinitionParts[0];
            if (referencedProjectNames != null)
            {
                references.Add(projectName, referencedProjectNames);
            }
 
            solution = AddProject(solution, projectName);
        }
 
        foreach (var (name, refs) in references)
            solution = AddProjectReferences(solution, name, refs);
 
        return solution;
    }
 
    private static Solution AddProject(Solution solution, string projectName)
    {
        var projectId = ProjectId.CreateNewId(debugName: projectName);
        return solution.AddProject(ProjectInfo.Create(projectId, VersionStamp.Create(), projectName, projectName, LanguageNames.CSharp, projectName));
    }
 
    private static Solution AddProjectReferences(Solution solution, string projectName, IEnumerable<string> projectReferences)
    {
        var referencesByTargetProject = new Dictionary<string, List<ProjectReference>>();
        foreach (var targetProject in projectReferences)
        {
            var references = referencesByTargetProject.GetOrAdd(targetProject, _ => []);
            if (references.Count == 0)
            {
                references.Add(new ProjectReference(solution.GetProjectsByName(targetProject).Single().Id));
            }
            else
            {
                references.Add(new ProjectReference(solution.GetProjectsByName(targetProject).Single().Id, [$"alias{references.Count}"]));
            }
        }
 
        return solution.AddProjectReferences(
            solution.GetProjectsByName(projectName).Single().Id,
            referencesByTargetProject.SelectMany(pair => pair.Value));
    }
 
    private static Solution CreateSolution()
        => new AdhocWorkspace().CurrentSolution;
 
    #endregion
}