// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. #nullable enable using System; using System.Collections.Generic; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.UnitTests; using Roslyn.Test.Utilities; using Roslyn.Utilities; using Xunit; namespace Microsoft.CodeAnalysis.Host.UnitTests { [UseExportProvider] public class ProjectDependencyGraphTests : TestBase { #region GetTopologicallySortedProjects [Fact, Trait(Traits.Feature, Traits.Features.Workspace)] 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, Trait(Traits.Feature, Traits.Features.Workspace)] public void TestTopologicallySortedProjectsIncrementalUpdate() { var solution = CreateSolutionFromReferenceMap("A"); VerifyTopologicalSort(solution, "A"); solution = AddProject(solution, "B"); VerifyTopologicalSort(solution, "AB", "BA"); } /// /// Verifies that /// returns one of the correct results. /// /// /// 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. private 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.GetProject(id)!.AssemblyName)); Assert.Contains(actualResult, expectedResults); } #endregion #region Dependency Sets [Fact, Trait(Traits.Feature, Traits.Features.Workspace)] [WorkItem(542438, "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, Trait(Traits.Feature, Traits.Features.Workspace)] public void TestDependencySetsIncrementalUpdate() { var solution = CreateSolutionFromReferenceMap("A"); VerifyDependencySets(solution, "A"); solution = AddProject(solution, "B"); VerifyDependencySets(solution, "A B"); } private 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.GetProject(p)!.AssemblyName).OrderBy(n => n))).OrderBy(n => n)); Assert.Equal(expectedResult, actualResult); } #endregion #region GetProjectsThatThisProjectTransitivelyDependsOn [Fact, Trait(Traits.Feature, Traits.Features.Workspace)] public void TestGetProjectsThatThisProjectTransitivelyDependsOn() { VerifyTransitiveReferences(CreateSolutionFromReferenceMap("A"), "A", new string[] { }); VerifyTransitiveReferences(CreateSolutionFromReferenceMap("B:A A"), "B", new string[] { "A" }); VerifyTransitiveReferences(CreateSolutionFromReferenceMap("C:B B:A A"), "C", new string[] { "B", "A" }); VerifyTransitiveReferences(CreateSolutionFromReferenceMap("C:B B:A A"), "A", new string[] { }); } [Fact, Trait(Traits.Feature, Traits.Features.Workspace)] public void TestGetProjectsThatThisProjectTransitivelyDependsOnThrowsArgumentNull() { var solution = CreateSolutionFromReferenceMap(""); Assert.Throws("projectId", () => solution.GetProjectDependencyGraph().GetProjectsThatThisProjectDirectlyDependsOn(null!)); } [Fact, Trait(Traits.Feature, Traits.Features.Workspace)] 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", new string[] { }); VerifyTransitiveReferences(solution, "B", new string[] { }); VerifyTransitiveReferences(solution, "C", new string[] { }); VerifyTransitiveReferences(solution, "D", new string[] { }); solution = AddProjectReferences(solution, "A", new string[] { "B" }); solution = AddProjectReferences(solution, "C", new string[] { "D" }); VerifyDirectReferences(solution, "A", new string[] { "B" }); VerifyDirectReferences(solution, "C", new string[] { "D" }); VerifyTransitiveReferences(solution, "A", new string[] { "B" }); VerifyTransitiveReferences(solution, "B", new string[] { }); VerifyTransitiveReferences(solution, "C", new string[] { "D" }); VerifyTransitiveReferences(solution, "D", new string[] { }); solution = AddProjectReferences(solution, "B", new string[] { "C" }); VerifyDirectReferences(solution, "B", new string[] { "C" }); VerifyTransitiveReferences(solution, "A", new string[] { "B", "C", "D" }); VerifyTransitiveReferences(solution, "B", new string[] { "C", "D" }); VerifyTransitiveReferences(solution, "C", new string[] { "D" }); VerifyTransitiveReferences(solution, "D", new string[] { }); } [Fact, Trait(Traits.Feature, Traits.Features.Workspace)] 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", new string[] { "B", "C" }); VerifyTransitiveReferences(solution, "B", new string[] { "C" }); VerifyTransitiveReferences(solution, "D", new string[] { "E", "F" }); VerifyTransitiveReferences(solution, "E", new string[] { "F" }); solution = AddProjectReferences(solution, "C", new string[] { "D" }); VerifyTransitiveReferences(solution, "A", new string[] { "B", "C", "D", "E", "F" }); } [Fact, Trait(Traits.Feature, Traits.Features.Workspace)] 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", new string[] { "B", "C" }); VerifyTransitiveReferences(solution, "B", new string[] { "C" }); VerifyTransitiveReferences(solution, "C", new string[] { }); } VerifyAllTransitiveReferences(); VerifyDirectReferences(solution, "A", new string[] { "B" }); solution = AddProjectReferences(solution, "A", new string[] { "C" }); VerifyAllTransitiveReferences(); VerifyDirectReferences(solution, "A", new string[] { "B", "C" }); } [Fact, Trait(Traits.Feature, Traits.Features.Workspace)] 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, SpecializedCollections.EmptyEnumerable()); VerifyTransitiveReferences(solution, "A", new string[] { }); // 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[] { projectBId }); VerifyTransitiveReferences(solution, dependencyGraph, project: "A", expectedResults: new string[] { "B" }); } [Fact, Trait(Traits.Feature, Traits.Features.Workspace)] 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[] { new ProjectReference(projectBId) }); solution = solution.AddProject(projectAInfo); VerifyDirectReferences(solution, "A", new string[] { }); VerifyTransitiveReferences(solution, "A", new string[] { }); solution = solution.AddProject(projectBId, "B", "B", LanguageNames.CSharp); VerifyTransitiveReferences(solution, "A", new string[] { "B" }); } [Fact, Trait(Traits.Feature, Traits.Features.Workspace)] 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", new string[] { }); solution = AddProjectReferences(solution, "A", new string[] { "B", "D" }); VerifyDirectReferences(solution, "A", new string[] { "B", "D" }); VerifyTransitiveReferences(solution, "A", new string[] { "B", "C", "D", "E" }); } private 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.GetProject(id)!.Name); Assert.Equal( expectedResults.OrderBy(n => n), actualResults.OrderBy(n => n)); } private void VerifyTransitiveReferences(Solution solution, string project, string[] expectedResults) { var projectDependencyGraph = solution.GetProjectDependencyGraph(); VerifyTransitiveReferences(solution, projectDependencyGraph, project, expectedResults); } private 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.GetProject(id)!.Name); Assert.Equal( expectedResults.OrderBy(n => n), actualResults.OrderBy(n => n)); } #endregion [Fact, Trait(Traits.Feature, Traits.Features.Workspace)] public void TestDirectAndReverseDirectReferencesAfterWithProjectReferences() { var solution = CreateSolutionFromReferenceMap("A:B B"); VerifyDirectReverseReferences(solution, "B", new string[] { "A" }); solution = solution.WithProjectReferences(solution.GetProjectsByName("A").Single().Id, Enumerable.Empty()); VerifyDirectReferences(solution, "A", new string[] { }); VerifyDirectReverseReferences(solution, "B", new string[] { }); } #region GetProjectsThatTransitivelyDependOnThisProject [Fact, Trait(Traits.Feature, Traits.Features.Workspace)] public void TestGetProjectsThatTransitivelyDependOnThisProject() { VerifyReverseTransitiveReferences(CreateSolutionFromReferenceMap("A"), "A", new string[] { }); VerifyReverseTransitiveReferences(CreateSolutionFromReferenceMap("B:A A"), "A", new string[] { "B" }); VerifyReverseTransitiveReferences(CreateSolutionFromReferenceMap("C:B B:A A"), "A", new string[] { "B", "C" }); VerifyReverseTransitiveReferences(CreateSolutionFromReferenceMap("C:B B:A A"), "C", new string[] { }); VerifyReverseTransitiveReferences(CreateSolutionFromReferenceMap("D:C,B B:A C A"), "A", new string[] { "D", "B" }); } [Fact, Trait(Traits.Feature, Traits.Features.Workspace)] public void TestGetProjectsThatTransitivelyDependOnThisProjectThrowsArgumentNull() { var solution = CreateSolutionFromReferenceMap(""); Assert.Throws("projectId", () => solution.GetProjectDependencyGraph().GetProjectsThatTransitivelyDependOnThisProject(null!)); } [Fact, Trait(Traits.Feature, Traits.Features.Workspace)] 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", new string[] { }); VerifyReverseTransitiveReferences(solution, "B", new string[] { }); VerifyReverseTransitiveReferences(solution, "C", new string[] { }); VerifyReverseTransitiveReferences(solution, "D", new string[] { }); solution = AddProjectReferences(solution, "A", new string[] { "B" }); solution = AddProjectReferences(solution, "C", new string[] { "D" }); VerifyDirectReverseReferences(solution, "B", new string[] { "A" }); VerifyDirectReverseReferences(solution, "D", new string[] { "C" }); VerifyReverseTransitiveReferences(solution, "A", new string[] { }); VerifyReverseTransitiveReferences(solution, "B", new string[] { "A" }); VerifyReverseTransitiveReferences(solution, "C", new string[] { }); VerifyReverseTransitiveReferences(solution, "D", new string[] { "C" }); solution = AddProjectReferences(solution, "B", new string[] { "C" }); VerifyDirectReverseReferences(solution, "C", new string[] { "B" }); VerifyReverseTransitiveReferences(solution, "A", new string[] { }); VerifyReverseTransitiveReferences(solution, "B", new string[] { "A" }); VerifyReverseTransitiveReferences(solution, "C", new string[] { "A", "B" }); VerifyReverseTransitiveReferences(solution, "D", new string[] { "A", "B", "C" }); } [Fact, Trait(Traits.Feature, Traits.Features.Workspace)] 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", new string[] { }); VerifyReverseTransitiveReferences(solution, "B", new string[] { "A" }); VerifyReverseTransitiveReferences(solution, "C", new string[] { }); VerifyReverseTransitiveReferences(solution, "D", new string[] { "C" }); solution = solution.WithProjectReferences(solution.GetProjectsByName("C").Single().Id, SpecializedCollections.EmptyEnumerable()); VerifyReverseTransitiveReferences(solution, "B", new string[] { "A" }); } [Fact, Trait(Traits.Feature, Traits.Features.Workspace)] 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", new string[] { "B" }); VerifyDirectReferences(solution, "B", new string[] { "C" }); VerifyDirectReferences(solution, "C", new string[] { "D" }); VerifyDirectReferences(solution, "D", new string[] { }); solution = solution.RemoveProject(solution.GetProjectsByName("B").Single().Id); VerifyDirectReferences(solution, "A", new string[] { }); VerifyDirectReferences(solution, "C", new string[] { "D" }); VerifyDirectReferences(solution, "D", new string[] { }); } [Fact, Trait(Traits.Feature, Traits.Features.Workspace)] 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", new string[] { "B", "C", "D" }); VerifyTransitiveReferences(solution, "B", new string[] { "C", "D" }); VerifyTransitiveReferences(solution, "C", new string[] { "D" }); VerifyTransitiveReferences(solution, "D", new string[] { }); solution = solution.RemoveProject(solution.GetProjectsByName("B").Single().Id); VerifyTransitiveReferences(solution, "A", new string[] { }); VerifyTransitiveReferences(solution, "C", new string[] { "D" }); VerifyTransitiveReferences(solution, "D", new string[] { }); } [Fact, Trait(Traits.Feature, Traits.Features.Workspace)] 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", new string[] { }); VerifyDirectReverseReferences(solution, "B", new string[] { "A" }); VerifyDirectReverseReferences(solution, "C", new string[] { "B" }); VerifyDirectReverseReferences(solution, "D", new string[] { "C" }); solution = solution.RemoveProject(solution.GetProjectsByName("B").Single().Id); VerifyDirectReverseReferences(solution, "A", new string[] { }); VerifyDirectReverseReferences(solution, "C", new string[] { }); VerifyDirectReverseReferences(solution, "D", new string[] { "C" }); } [Fact, Trait(Traits.Feature, Traits.Features.Workspace)] 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", new string[] { }); VerifyReverseTransitiveReferences(solution, "B", new string[] { "A" }); VerifyReverseTransitiveReferences(solution, "C", new string[] { "A", "B" }); VerifyReverseTransitiveReferences(solution, "D", new string[] { "A", "B", "C" }); solution = solution.RemoveProject(solution.GetProjectsByName("B").Single().Id); VerifyReverseTransitiveReferences(solution, "A", new string[] { }); VerifyReverseTransitiveReferences(solution, "C", new string[] { }); VerifyReverseTransitiveReferences(solution, "D", new string[] { "C" }); } private 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.GetProject(id)!.Name); Assert.Equal( expectedResults.OrderBy(n => n), actualResults.OrderBy(n => n)); } private 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.GetProject(id)!.Name); Assert.Equal( expectedResults.OrderBy(n => n), actualResults.OrderBy(n => n)); } #endregion #region Helpers private Solution CreateSolutionFromReferenceMap(string projectReferences) { var solution = CreateSolution(); var references = new Dictionary>(); 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 kvp in references) { solution = AddProjectReferences(solution, kvp.Key, kvp.Value); } 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 projectReferences) { return solution.AddProjectReferences( solution.GetProjectsByName(projectName).Single().Id, projectReferences.Select(name => new ProjectReference(solution.GetProjectsByName(name).Single().Id))); } private Solution CreateSolution() { return new AdhocWorkspace().CurrentSolution; } #endregion } }