Repository: vanniktech/gradle-dependency-graph-generator-plugin (kotlin)

Issue:
Customize links between nodes
Would you be open to allow customizing links between nodes?
We might want to highlight a specific path, or a specific dependency type, similarly to `dependencyNode` and `projectNode`.

https://github.com/vanniktech/gradle-dependency-graph-generator-plugin/blob/104abbdb1cc0c689688f665d5cc27b5b3649bb4a/src/main/kotlin/com/vanniktech/dependency/graph/generator/DependencyGraphGeneratorExtension.kt#L62-L65

Pull request description:
- [x] Link between two `Project`s in `DependencyGraphGeneratorExtension.ProjectGenerator`
- [x] Link between two `ResolvedDependency` in `DependencyGraphGeneratorExtension.Generator`

Closes #169

Patch:
diff --git a/src/main/kotlin/com/vanniktech/dependency/graph/generator/DependencyContainer.kt b/src/main/kotlin/com/vanniktech/dependency/graph/generator/DependencyContainer.kt
new file mode 100644
index 0000000..747194b
--- /dev/null
+++ b/src/main/kotlin/com/vanniktech/dependency/graph/generator/DependencyContainer.kt
@@ -0,0 +1,20 @@
+package com.vanniktech.dependency.graph.generator
+
+import org.gradle.api.Project as GradleProject
+import org.gradle.api.artifacts.ResolvedDependency as GradleResolvedDependency
+
+sealed interface DependencyContainer {
+
+  companion object {
+    internal operator fun invoke(project: GradleProject) = Project(project)
+    internal operator fun invoke(dependency: GradleResolvedDependency) = ResolvedDependency(dependency)
+  }
+
+  data class Project internal constructor(
+    internal val project: GradleProject,
+  ) : DependencyContainer, GradleProject by project
+
+  data class ResolvedDependency internal constructor(
+    internal val resolvedDependency: GradleResolvedDependency,
+  ) : DependencyContainer, GradleResolvedDependency by resolvedDependency
+}
diff --git a/src/main/kotlin/com/vanniktech/dependency/graph/generator/DependencyGraphGenerator.kt b/src/main/kotlin/com/vanniktech/dependency/graph/generator/DependencyGraphGenerator.kt
index 50d80ca..bbc9850 100644
--- a/src/main/kotlin/com/vanniktech/dependency/graph/generator/DependencyGraphGenerator.kt
+++ b/src/main/kotlin/com/vanniktech/dependency/graph/generator/DependencyGraphGenerator.kt
@@ -21,9 +21,9 @@ internal class DependencyGraphGenerator(
   // We keep a map of an identifier to a parent identifier in order to not add unique dependencies more than once.
   // One scenario is A depending on B and B on C.
   // If a module depends on both A and B the connection from B to C could be wired twice.
-  private val addedConnections = mutableSetOf<Pair<String, String>>()
+  private val addedConnections = mutableSetOf<Pair<DotIdentifier, DotIdentifier>>()
 
-  private val nodes = mutableMapOf<String, MutableNode>()
+  private val nodes = mutableMapOf<DotIdentifier, MutableNode>()
 
   @Suppress("Detekt.SpreadOperator") fun generateGraph(): MutableGraph {
     val graph = mutGraph("G").setDirected(true).graphAttrs().add(GraphAttr.dpi(100))
@@ -34,12 +34,12 @@ internal class DependencyGraphGenerator(
     val projects = (if (project.subprojects.size > 0) project.subprojects else setOf(project))
       .filter { generator.includeProject(it) }
 
-    val rootNodes = mutableSetOf<String>()
+    val rootNodes = mutableSetOf<DotIdentifier>()
 
     // Generate top level projects.
     projects.forEach {
       val projectId = it.dotIdentifier
-      val node = mutNode(projectId)
+      val node = mutNode(projectId.value)
         .add(Label.of(it.name))
         .add(Shape.RECTANGLE)
       val tunedNode = generator.projectNode.invoke(node, it)
@@ -55,46 +55,52 @@ internal class DependencyGraphGenerator(
           .filter { it.isCanBeResolved }
           .filter { generator.includeConfiguration.invoke(it) }
           .flatMap { it.resolvedConfiguration.firstLevelModuleDependencies }
-          .map { project to it }
-      }
-      .forEach { (project, dependency) ->
-        append(dependency, project.dotIdentifier, graph, rootNodes)
+          .map { DependencyContainer(project) to it }
       }
+      .forEach { (project, dependency) -> append(dependency, project, graph, rootNodes) }
 
     if (rootNodes.isNotEmpty()) {
       graph.add(
         graph()
           .graphAttr()
           .with(Rank.inSubgraph(RankType.SAME))
-          .with(*rootNodes.map { mutNode(it) }.toTypedArray())
+          .with(*rootNodes.map { mutNode(it.value) }.toTypedArray())
       )
     }
 
     return generator.graph(graph)
   }
 
-  private fun append(dependency: ResolvedDependency, parentIdentifier: String, graph: MutableGraph, rootNodes: MutableSet<String>) {
-    val identifier = (dependency.moduleGroup + dependency.moduleName).dotIdentifier
-    val pair = parentIdentifier to identifier
+  private fun append(
+    dependency: ResolvedDependency,
+    parent: DependencyContainer,
+    graph: MutableGraph,
+    rootNodes: MutableSet<DotIdentifier>
+  ) {
+    val identifier = dependency.dotIdentifier
+    val pair = parent.dotIdentifier to identifier
 
-    if (generator.include.invoke(dependency) && !addedConnections.contains(pair)) {
-      addedConnections.add(pair)
+    if (pair in addedConnections) return
+    if (!generator.include(dependency)) return
 
-      val node = mutNode(identifier)
-        .add(Label.of(dependency.getDisplayName()))
-        .add(Shape.RECTANGLE)
+    addedConnections.add(pair)
 
-      val mutated = generator.dependencyNode.invoke(node, dependency)
-      nodes[identifier] = mutated
-      graph.add(mutated)
+    val node = mutNode(identifier.value)
+      .add(Label.of(dependency.getDisplayName()))
+      .add(Shape.RECTANGLE)
 
-      rootNodes.remove(identifier)
+    val mutated = generator.dependencyNode.invoke(node, dependency)
+    nodes[identifier] = mutated
+    graph.add(mutated)
 
-      nodes[parentIdentifier]?.addLink(mutated)
+    rootNodes.remove(identifier)
 
-      if (generator.children.invoke(dependency)) {
-        dependency.children.forEach { append(it, identifier, graph, rootNodes) }
-      }
+    nodes[parent.dotIdentifier]?.apply {
+      addLink(generator.link(linkTo(mutated), parent, DependencyContainer(dependency)))
+    }
+
+    if (generator.children.invoke(dependency)) {
+      dependency.children.forEach { append(it, DependencyContainer(dependency), graph, rootNodes) }
     }
   }
 
diff --git a/src/main/kotlin/com/vanniktech/dependency/graph/generator/DependencyGraphGeneratorExtension.kt b/src/main/kotlin/com/vanniktech/dependency/graph/generator/DependencyGraphGeneratorExtension.kt
index 067cb50..65211cd 100644
--- a/src/main/kotlin/com/vanniktech/dependency/graph/generator/DependencyGraphGeneratorExtension.kt
+++ b/src/main/kotlin/com/vanniktech/dependency/graph/generator/DependencyGraphGeneratorExtension.kt
@@ -2,10 +2,12 @@ package com.vanniktech.dependency.graph.generator
 
 import groovy.lang.Closure
 import guru.nidi.graphviz.attribute.Label
+import guru.nidi.graphviz.attribute.Style
 import guru.nidi.graphviz.engine.Format
 import guru.nidi.graphviz.engine.Format.PNG
 import guru.nidi.graphviz.engine.Format.SVG
 import guru.nidi.graphviz.engine.Graphviz
+import guru.nidi.graphviz.model.Link
 import guru.nidi.graphviz.model.MutableGraph
 import guru.nidi.graphviz.model.MutableNode
 import org.gradle.api.NamedDomainObjectContainer
@@ -64,6 +66,8 @@ open class DependencyGraphGeneratorExtension(project: Project) {
     @get:Nested var dependencyNode: (MutableNode, ResolvedDependency) -> MutableNode = { node, _ -> node },
     /** Allows to change the [MutableNode] for the given [Project]. */
     @get:Nested var projectNode: (MutableNode, Project) -> MutableNode = { node, _ -> node },
+    /** Allows to change the [Link] between two [DependencyContainer]. */
+    @get:Nested var link: (link: Link, from: DependencyContainer, to: DependencyContainer) -> Link = { it, _, _ -> it },
     /** Optional label that can be displayed wrapped around the graph. */
     @get:Internal var label: Label? = null, // Not serializable making it unusable as an Input.
     /** Return true when you want to include this [Configuration], false otherwise. */
@@ -114,6 +118,10 @@ open class DependencyGraphGeneratorExtension(project: Project) {
     @get:Input var name: String = "",
     /** Allows to change the [MutableNode] for the given [Project]. */
     @get:Nested var projectNode: (MutableNode, Project) -> MutableNode = { node, _ -> node },
+    /** Allows to change the [Link] between two [Project]s and for the given [Configuration]. */
+    @get:Nested var link: (link: Link, from: Project, to: Project, configuration: Configuration) -> Link = { it, _, _, configuration ->
+      if (configuration.isImplementation()) it.with(Style.DOTTED) else it
+    },
     /** Return true when you want to include this [Project], false otherwise. */
     @get:Nested var includeProject: (Project) -> Boolean = { true },
     /** Return true when you want to include this [Configuration], false otherwise. */
diff --git a/src/main/kotlin/com/vanniktech/dependency/graph/generator/DotIdentifier.kt b/src/main/kotlin/com/vanniktech/dependency/graph/generator/DotIdentifier.kt
new file mode 100644
index 0000000..5b3eeea
--- /dev/null
+++ b/src/main/kotlin/com/vanniktech/dependency/graph/generator/DotIdentifier.kt
@@ -0,0 +1,24 @@
+package com.vanniktech.dependency.graph.generator
+
+import org.gradle.api.Project
+import org.gradle.api.artifacts.ResolvedDependency
+
+@JvmInline
+internal value class DotIdentifier(internal val value: String) {
+  init {
+    require(value.none(illegalChars))
+  }
+}
+
+private val illegalChars: (Char) -> Boolean = { it == '-' || it == '.' || it.isWhitespace() }
+
+internal val String.dotIdentifier get() = DotIdentifier(filterNot(illegalChars))
+
+internal val Project.dotIdentifier get() = "$group$name".dotIdentifier
+
+internal val ResolvedDependency.dotIdentifier get() = (moduleGroup + moduleName).dotIdentifier
+
+internal val DependencyContainer.dotIdentifier get() = when (this) {
+  is DependencyContainer.Project -> project.dotIdentifier
+  is DependencyContainer.ResolvedDependency -> resolvedDependency.dotIdentifier
+}
diff --git a/src/main/kotlin/com/vanniktech/dependency/graph/generator/ProjectDependencyGraphGenerator.kt b/src/main/kotlin/com/vanniktech/dependency/graph/generator/ProjectDependencyGraphGenerator.kt
index d02299a..094dde4 100644
--- a/src/main/kotlin/com/vanniktech/dependency/graph/generator/ProjectDependencyGraphGenerator.kt
+++ b/src/main/kotlin/com/vanniktech/dependency/graph/generator/ProjectDependencyGraphGenerator.kt
@@ -14,6 +14,7 @@ import guru.nidi.graphviz.model.Link
 import guru.nidi.graphviz.model.MutableGraph
 import guru.nidi.graphviz.model.MutableNode
 import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
 import org.gradle.api.artifacts.ProjectDependency
 
 // Based on https://github.com/JakeWharton/SdkSearch/blob/766d612ed52cdf3af9cd0728b6afd87006746ae5/gradle/projectDependencyGraph.gradle
@@ -31,7 +32,7 @@ internal class ProjectDependencyGraphGenerator(
           .flatMap { configuration ->
             configuration.dependencies
               .withType(ProjectDependency::class.java)
-              .map { ProjectDependencyContainer(project, it.dependencyProject, configuration.name.lowercase().endsWith("implementation")) }
+              .map { ProjectDependencyContainer(project, it.dependencyProject, configuration) }
           }
           .forEach {
             dependencies.add(it)
@@ -72,22 +73,21 @@ internal class ProjectDependencyGraphGenerator(
   }
 
   private fun addDependencies(dependencies: MutableList<ProjectDependencyContainer>, graph: MutableGraph) {
-    val rootNodes: List<MutableNode> = graph.rootNodes().filter { it.links().isEmpty() }
+    val rootNodes: List<MutableNode> = graph.rootNodes().filterNotNull().filter { it.links().isEmpty() }
     dependencies
       .filterNot { (from, to, _) -> from == to }
-      .distinctBy { (from, to, _) -> from to to }
-      .forEach { (from, to, isImplementation) ->
+      .forEach { (from, to, configuration) ->
         val fromNode = rootNodes.single { it.name().toString() == from.path }
         val toNode = rootNodes.singleOrNull { it.name().toString() == to.path } ?: return@forEach
-        val link = Link.to(toNode)
-        graph.add(fromNode.addLink(if (isImplementation) link.with(Style.DOTTED) else link))
+        val link = projectGenerator.link(Link.to(toNode), from, to, configuration)
+        graph.add(fromNode.addLink(link))
       }
   }
 
   internal data class ProjectDependencyContainer(
     val from: Project,
     val to: Project,
-    val isImplementation: Boolean
+    val configuration: Configuration,
   )
 
   internal companion object {
diff --git a/src/main/kotlin/com/vanniktech/dependency/graph/generator/extensions.kt b/src/main/kotlin/com/vanniktech/dependency/graph/generator/extensions.kt
index 8163299..4bc9437 100644
--- a/src/main/kotlin/com/vanniktech/dependency/graph/generator/extensions.kt
+++ b/src/main/kotlin/com/vanniktech/dependency/graph/generator/extensions.kt
@@ -2,14 +2,9 @@ package com.vanniktech.dependency.graph.generator
 
 import com.vanniktech.dependency.graph.generator.ProjectTarget.MULTIPLATFORM
 import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
 import org.gradle.api.artifacts.ProjectDependency
 
-private val whitespaceRegex = Regex("\\s")
-
-internal val String.dotIdentifier get() = replace("-", "")
-  .replace(".", "")
-  .replace(whitespaceRegex, "")
-
 internal fun String.nonEmptyPrepend(prepend: String) =
   if (isNotEmpty()) prepend + this else this
 
@@ -22,8 +17,6 @@ internal fun String.toHyphenCase(): String {
     .joinToString(separator = "") { if (it[0].isUpperCase()) "-${it[0].lowercase()}" else it }
 }
 
-internal val Project.dotIdentifier get() = "$group$name".dotIdentifier
-
 fun Project.isDependingOnOtherProject() = configurations.any { configuration -> configuration.dependencies.any { it is ProjectDependency } }
 
 fun Project.isCommonsProject() = plugins.hasPlugin("org.jetbrains.kotlin.platform.common")
@@ -39,3 +32,5 @@ internal fun Project.target(): ProjectTarget {
     else -> withoutMultiplatform.firstOrNull() ?: ProjectTarget.OTHER
   }
 }
+
+internal fun Configuration.isImplementation() = name.lowercase().endsWith("implementation")


Test patch:
diff --git a/src/test/java/com/vanniktech/dependency/graph/generator/DependencyGraphGeneratorLinkTest.kt b/src/test/java/com/vanniktech/dependency/graph/generator/DependencyGraphGeneratorLinkTest.kt
new file mode 100644
index 0000000..5114ef2
--- /dev/null
+++ b/src/test/java/com/vanniktech/dependency/graph/generator/DependencyGraphGeneratorLinkTest.kt
@@ -0,0 +1,59 @@
+package com.vanniktech.dependency.graph.generator
+
+import com.vanniktech.dependency.graph.generator.DependencyContainer.Project
+import com.vanniktech.dependency.graph.generator.DependencyContainer.ResolvedDependency
+import com.vanniktech.dependency.graph.generator.DependencyGraphGeneratorExtension.Generator.Companion.ALL
+import guru.nidi.graphviz.attribute.Color.BLUE
+import guru.nidi.graphviz.attribute.Style.DASHED
+import guru.nidi.graphviz.model.Link
+import org.gradle.api.internal.project.DefaultProject
+import org.gradle.api.plugins.JavaLibraryPlugin
+import org.gradle.testfixtures.ProjectBuilder
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+
+class DependencyGraphGeneratorLinkTest {
+
+  private lateinit var project: DefaultProject
+
+  @Before fun setUp() {
+    project = ProjectBuilder.builder().withName("root").build() as DefaultProject
+    project.plugins.apply(JavaLibraryPlugin::class.java)
+    project.repositories.run { add(mavenCentral()) }
+    project.dependencies.add("api", "org.jetbrains.kotlin:kotlin-stdlib:1.2.30")
+    project.dependencies.add("implementation", "io.reactivex.rxjava2:rxjava:2.1.10")
+  }
+
+  @Test fun customizeLinkBasedOnFromAndTo() {
+    val blueDashedLinkForRootToRxJava = { link: Link, from: DependencyContainer, to: DependencyContainer ->
+      if ((from as? Project)?.project == project && (to as? ResolvedDependency)?.moduleName == "rxjava") link.attrs()
+        .add(BLUE, DASHED)
+      else link
+    }
+    assertEquals(
+      // language=dot
+      """
+      digraph "G" {
+      edge ["dir"="forward"]
+      graph ["dpi"="100"]
+      "root" ["label"="root","shape"="rectangle"]
+      "orgjetbrainskotlinkotlinstdlib" ["label"="kotlin-stdlib","shape"="rectangle"]
+      "orgjetbrainsannotations" ["label"="jetbrains-annotations","shape"="rectangle"]
+      "ioreactivexrxjava2rxjava" ["label"="rxjava","shape"="rectangle"]
+      "orgreactivestreamsreactivestreams" ["label"="reactive-streams","shape"="rectangle"]
+      {
+      edge ["dir"="none"]
+      graph ["rank"="same"]
+      "root"
+      }
+      "root" -> "orgjetbrainskotlinkotlinstdlib"
+      "root" -> "ioreactivexrxjava2rxjava" ["color"="blue","style"="dashed"]
+      "orgjetbrainskotlinkotlinstdlib" -> "orgjetbrainsannotations"
+      "ioreactivexrxjava2rxjava" -> "orgreactivestreamsreactivestreams"
+      }
+      """.trimIndent(),
+      DependencyGraphGenerator(project, ALL.copy(link = blueDashedLinkForRootToRxJava)).generateGraph().toString()
+    )
+  }
+}
diff --git a/src/test/java/com/vanniktech/dependency/graph/generator/DotIdentifierTest.kt b/src/test/java/com/vanniktech/dependency/graph/generator/DotIdentifierTest.kt
new file mode 100644
index 0000000..3d29541
--- /dev/null
+++ b/src/test/java/com/vanniktech/dependency/graph/generator/DotIdentifierTest.kt
@@ -0,0 +1,46 @@
+package com.vanniktech.dependency.graph.generator
+
+import org.gradle.api.plugins.JavaLibraryPlugin
+import org.gradle.testfixtures.ProjectBuilder
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class DotIdentifierTest {
+
+  @Test fun valid() {
+    assertEquals("javainject", "java.inject".dotIdentifier.value)
+    assertEquals("firebasecore", "firebase-core".dotIdentifier.value)
+    assertEquals("firebasecore", "firebase core".dotIdentifier.value)
+  }
+
+  @Test fun validProject() {
+    val project = ProjectBuilder.builder().withName("app").build()
+    assertEquals("app", project.dotIdentifier.value)
+  }
+
+  @Test fun validResolvedDependency() {
+    val project = ProjectBuilder.builder().withName("app").build()
+    project.plugins.apply(JavaLibraryPlugin::class.java)
+    project.repositories.run { add(mavenCentral()) }
+    project.dependencies.add("compileOnly", "org.jetbrains.kotlin:kotlin-stdlib:1.2.30")
+    val dependency = project.configurations
+      .filter { it.isCanBeResolved }
+      .flatMap { it.resolvedConfiguration.firstLevelModuleDependencies }
+      .single()
+    assertEquals("orgjetbrainskotlinkotlinstdlib", dependency.dotIdentifier.value)
+  }
+
+  @Test(expected = IllegalArgumentException::class) fun dash() {
+    DotIdentifier("a-b")
+  }
+
+  @Test(expected = IllegalArgumentException::class)
+  fun dot() {
+    DotIdentifier("a.b")
+  }
+
+  @Test(expected = IllegalArgumentException::class)
+  fun whitespace() {
+    DotIdentifier("a b")
+  }
+}
diff --git a/src/test/java/com/vanniktech/dependency/graph/generator/ExtensionsTest.kt b/src/test/java/com/vanniktech/dependency/graph/generator/ExtensionsTest.kt
index ac75965..38247dd 100644
--- a/src/test/java/com/vanniktech/dependency/graph/generator/ExtensionsTest.kt
+++ b/src/test/java/com/vanniktech/dependency/graph/generator/ExtensionsTest.kt
@@ -4,11 +4,6 @@ import org.junit.Assert.assertEquals
 import org.junit.Test
 
 class ExtensionsTest {
-  @Test fun dotIdentifier() {
-    assertEquals("javainject", "java.inject".dotIdentifier)
-    assertEquals("firebasecore", "firebase-core".dotIdentifier)
-    assertEquals("firebasecore", "firebase core".dotIdentifier)
-  }
 
   @Test fun nonEmptyPrepend() {
     assertEquals("", "".nonEmptyPrepend("-"))
diff --git a/src/test/java/com/vanniktech/dependency/graph/generator/ProjectDependencyGraphGeneratorLinkTest.kt b/src/test/java/com/vanniktech/dependency/graph/generator/ProjectDependencyGraphGeneratorLinkTest.kt
new file mode 100644
index 0000000..75053cb
--- /dev/null
+++ b/src/test/java/com/vanniktech/dependency/graph/generator/ProjectDependencyGraphGeneratorLinkTest.kt
@@ -0,0 +1,66 @@
+package com.vanniktech.dependency.graph.generator
+
+import com.vanniktech.dependency.graph.generator.DependencyGraphGeneratorExtension.ProjectGenerator.Companion.ALL
+import guru.nidi.graphviz.attribute.Color.BLUE
+import guru.nidi.graphviz.attribute.Style.DASHED
+import guru.nidi.graphviz.model.Link
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.plugins.JavaLibraryPlugin
+import org.gradle.testfixtures.ProjectBuilder
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+
+class ProjectDependencyGraphGeneratorLinkTest {
+
+  private lateinit var root: Project
+  private lateinit var app: Project
+  private lateinit var lib1: Project
+  private lateinit var lib2: Project
+
+  @Before fun setUp() {
+    root = ProjectBuilder.builder().withName("root").build()
+    root.plugins.apply(JavaLibraryPlugin::class.java)
+
+    app = ProjectBuilder.builder().withParent(root).withName("app").build()
+    app.plugins.apply(JavaLibraryPlugin::class.java)
+
+    lib1 = ProjectBuilder.builder().withParent(root).withName("lib1").build()
+    lib1.plugins.apply(JavaLibraryPlugin::class.java)
+
+    lib2 = ProjectBuilder.builder().withParent(root).withName("lib2").build()
+    lib2.plugins.apply(JavaLibraryPlugin::class.java)
+
+    app.dependencies.add("implementation", lib1)
+    app.dependencies.add("implementation", lib2)
+  }
+
+  @Test fun customizeLinkBasedOnFromAndTo() {
+    val blueDashedLinkForAppToLib1 = { link: Link, from: Project, to: Project, _: Configuration ->
+      if (from == app && to == lib1) link.attrs().add(BLUE, DASHED)
+      else link
+    }
+    assertEquals(
+      // language=DOT
+      """
+      digraph {
+      edge ["dir"="forward"]
+      graph ["dpi"="100","label"="root","labelloc"="t","fontsize"="35"]
+      node ["style"="filled"]
+      ":app" ["shape"="rectangle","fillcolor"="#FF7043"]
+      ":lib1" ["fillcolor"="#FF7043"]
+      ":lib2" ["fillcolor"="#FF7043"]
+      {
+      edge ["dir"="none"]
+      graph ["rank"="same"]
+      ":app"
+      }
+      ":app" -> ":lib1" ["color"="blue","style"="dashed"]
+      ":app" -> ":lib2"
+      }
+      """.trimIndent(),
+      ProjectDependencyGraphGenerator(root, ALL.copy(link = blueDashedLinkForAppToLib1)).generateGraph().toString()
+    )
+  }
+}