Report error on circular local dependencies (#731)

If a stack overflow is found during project evaluation, present any
circular imports found in the dependency graph.
This commit is contained in:
Islon Scherer
2024-10-25 01:45:18 +02:00
committed by GitHub
parent 1ceb489d78
commit 93cc3253eb
10 changed files with 377 additions and 11 deletions
@@ -21,9 +21,11 @@ import java.util.regex.Pattern
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatCode
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.io.TempDir
import org.pkl.commons.test.FileTestUtils
import org.pkl.commons.test.PackageServer
import org.pkl.commons.toPath
import org.pkl.commons.writeString
import org.pkl.core.*
import org.pkl.core.evaluatorSettings.PklEvaluatorSettings
@@ -188,4 +190,58 @@ class ProjectTest {
)
}
}
@Test
fun `fails if project has cyclical dependencies`() {
val projectPath = javaClass.getResource("projectCycle1/PklProject")!!.toURI().toPath()
val e = assertThrows<PklException> { Project.loadFromPath(projectPath) }
val cleanMsg = e.message!!.replace(Regex("file:///.*/resources/test"), "file://")
assertThat(cleanMsg)
.isEqualTo(
"""
–– Pkl Error ––
Local project dependencies cannot be circular.
Cycle:
┌─>
│ file:///org/pkl/core/project/projectCycle2/PklProject
│ file:///org/pkl/core/project/projectCycle3/PklProject
└─
"""
.trimIndent()
)
}
@Test
fun `fails if a project has cyclical dependencies -- multiple cycles found`() {
val projectPath = javaClass.getResource("projectCycle4/PklProject")!!.toURI().toPath()
val e = assertThrows<PklException> { Project.loadFromPath(projectPath) }
val cleanMsg = e.message!!.replace(Regex("file://.*/resources/test"), "file://")
assertThat(cleanMsg)
.isEqualTo(
"""
–– Pkl Error ––
Local project dependencies cannot be circular.
The following circular imports were found.
Not all of them are necessarily problematic.
The problematic cycles are those declared as local dependencies.
Cycle 1:
┌─>
│ file:///org/pkl/core/project/projectCycle2/PklProject
│ file:///org/pkl/core/project/projectCycle3/PklProject
└─
Cycle 2:
┌─>
│ file:///org/pkl/core/project/projectCycle4/PklProject
└─
"""
.trimIndent()
)
}
}
@@ -0,0 +1,95 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.util
import java.net.URI
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.pkl.core.ImportGraph
class ImportGraphUtilsTest {
@Test
fun basic() {
val barUri = URI("file:///bar.pkl")
val fooUri = URI("file:///foo.pkl")
val graph =
ImportGraph(
mapOf(
fooUri to setOf(ImportGraph.Import(barUri)),
barUri to setOf(ImportGraph.Import(fooUri))
),
// resolved URIs is not important
mapOf()
)
val cycles = ImportGraphUtils.findImportCycles(graph)
assertThat(cycles).isEqualTo(listOf(listOf(fooUri, barUri)))
}
@Test
fun `two cycles`() {
val barUri = URI("file:///bar.pkl")
val fooUri = URI("file:///foo.pkl")
val bizUri = URI("file:///biz.pkl")
val quxUri = URI("file:///qux.pkl")
val graph =
ImportGraph(
mapOf(
fooUri to setOf(ImportGraph.Import(barUri)),
barUri to setOf(ImportGraph.Import(fooUri)),
bizUri to setOf(ImportGraph.Import(quxUri)),
quxUri to setOf(ImportGraph.Import(bizUri))
),
// resolved URIs is not important
mapOf()
)
val cycles = ImportGraphUtils.findImportCycles(graph)
assertThat(cycles).isEqualTo(listOf(listOf(fooUri, barUri), listOf(bizUri, quxUri)))
}
@Test
fun `no cycles`() {
val barUri = URI("file:///bar.pkl")
val fooUri = URI("file:///foo.pkl")
val bizUri = URI("file:///biz.pkl")
val quxUri = URI("file:///qux.pkl")
val graph =
ImportGraph(
mapOf(
barUri to setOf(ImportGraph.Import(fooUri)),
fooUri to setOf(ImportGraph.Import(bizUri)),
bizUri to setOf(ImportGraph.Import(quxUri)),
quxUri to setOf()
),
// resolved URIs is not important
mapOf()
)
val cycles = ImportGraphUtils.findImportCycles(graph)
assertThat(cycles).isEmpty()
}
@Test
fun `self-import`() {
val fooUri = URI("file:///foo.pkl")
val graph =
ImportGraph(
mapOf(fooUri to setOf(ImportGraph.Import(fooUri))),
// resolved URIs is not important
mapOf()
)
val cycles = ImportGraphUtils.findImportCycles(graph)
assertThat(cycles).isEqualTo(listOf(listOf(fooUri)))
}
}
@@ -0,0 +1,12 @@
amends "pkl:Project"
package {
name = "projectCycle1"
version = "1.0.0"
packageZipUrl = "https://bogus.value"
baseUri = "package://localhost:0/projectCycle1"
}
dependencies {
["projectCycle2"] = import("../projectCycle2/PklProject")
}
@@ -0,0 +1,12 @@
amends "pkl:Project"
package {
name = "projectCycle2"
version = "1.0.0"
packageZipUrl = "https://bogus.value"
baseUri = "package://localhost:0/projectCycle2"
}
dependencies {
["projectCycle3"] = import("../projectCycle3/PklProject")
}
@@ -0,0 +1,12 @@
amends "pkl:Project"
package {
name = "projectCycle3"
version = "1.0.0"
packageZipUrl = "https://bogus.value"
baseUri = "package://localhost:0/projectCycle3"
}
dependencies {
["projectCycle2"] = import("../projectCycle2/PklProject")
}
@@ -0,0 +1,7 @@
amends "pkl:Project"
import "PklProject"
dependencies {
["projectCycle1"] = import("../projectCycle1/PklProject")
}