Skip to content

Commit 2a91005

Browse files
committedFeb 27, 2025
feat(Maven): Add more error handling for Tycho
Create issues for build failures and for projects for which no dependency information could be retrieved. Signed-off-by: Oliver Heger <oliver.heger@bosch.io>
1 parent 17f979e commit 2a91005

File tree

2 files changed

+117
-5
lines changed

2 files changed

+117
-5
lines changed
 

‎plugins/package-managers/maven/src/main/kotlin/Tycho.kt

+47-5
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import org.ossreviewtoolkit.model.Issue
4545
import org.ossreviewtoolkit.model.ProjectAnalyzerResult
4646
import org.ossreviewtoolkit.model.config.AnalyzerConfiguration
4747
import org.ossreviewtoolkit.model.config.RepositoryConfiguration
48+
import org.ossreviewtoolkit.model.createAndLogIssue
4849
import org.ossreviewtoolkit.model.utils.DependencyGraphBuilder
4950
import org.ossreviewtoolkit.plugins.packagemanagers.maven.utils.DependencyTreeLogger
5051
import org.ossreviewtoolkit.plugins.packagemanagers.maven.utils.LocalProjectWorkspaceReader
@@ -90,8 +91,7 @@ class Tycho(
9091
logger.info { "Resolving Tycho dependencies for $definitionFile." }
9192

9293
val collector = TychoProjectsCollector()
93-
val (_, buildLog) = runBuild(collector, definitionFile.parentFile)
94-
// TODO: Create issues for a failed build and projects for which dependencies could not be resolved.
94+
val (exitCode, buildLog) = runBuild(collector, definitionFile.parentFile)
9595

9696
val resolvedProjects = createMavenSupport(collector).use { mavenSupport ->
9797
graphBuilder = createGraphBuilder(mavenSupport, collector.mavenProjects)
@@ -107,8 +107,11 @@ class Tycho(
107107

108108
buildLog.delete()
109109

110-
// Assign issues that are global to the build to the root project.
111-
val rootIssues = mutableListOf<Issue>()
110+
val rootProject = collector.mavenProjects.values.find { it.file == definitionFile }
111+
?: throw TychoBuildException("Tycho root project could not be built.")
112+
113+
val rootIssues = createRootIssues(exitCode, collector, resolvedProjects)
114+
112115
graphBuilder.packages().createIssuesForAutoGeneratedPoms(managerName, rootIssues)
113116

114117
return resolvedProjects.map { mavenProject ->
@@ -119,7 +122,7 @@ class Tycho(
119122
mavenProject.file.parentFile,
120123
graphBuilder.scopesFor(projectId)
121124
)
122-
val issues = rootIssues.takeIf { mavenProject.file == definitionFile } ?: emptyList()
125+
val issues = rootIssues.takeIf { mavenProject == rootProject }.orEmpty()
123126
ProjectAnalyzerResult(project, emptySet(), issues)
124127
}
125128
}
@@ -223,11 +226,50 @@ class Tycho(
223226
graphBuilder.addDependency(DependencyGraph.qualifyScope(projectId, node.dependency.scope), node)
224227
}
225228
}
229+
230+
/**
231+
* Create a list with [Issue]s for global build problems. Since this implementation executes a single
232+
* multi-module build, it is typically not possible to assign single issues to specific projects. Therefore, all
233+
* issues are assigned to the root project. In order to generate the issues, evaluate the [exitCode] of the Maven
234+
* build, the projects found by the [collector], and the [resolvedProjects] for which dependency information was
235+
* found.
236+
*/
237+
private fun createRootIssues(
238+
exitCode: Int,
239+
collector: TychoProjectsCollector,
240+
resolvedProjects: List<MavenProject>
241+
): MutableList<Issue> {
242+
val rootIssues = mutableListOf<Issue>()
243+
if (exitCode != 0) {
244+
rootIssues += createAndLogIssue(
245+
managerName,
246+
"Maven build failed with non-zero exit code $exitCode."
247+
)
248+
}
249+
250+
val missingProjects = collector.mavenProjects.keys - resolvedProjects.mapTo(mutableSetOf()) { it.internalId }
251+
252+
missingProjects.forEach { projectId ->
253+
val coordinates = collector.mavenProjects.getValue(projectId).identifier(projectType).toCoordinates()
254+
rootIssues += createAndLogIssue(
255+
managerName,
256+
"No dependency information found for project '$coordinates'. " +
257+
"This may be caused by a build failure."
258+
)
259+
}
260+
261+
return rootIssues
262+
}
226263
}
227264

228265
/** The name of the logger used by the Maven dependency tree plugin. */
229266
private const val DEPENDENCY_TREE_LOGGER = "org.apache.maven.plugins.dependency.tree.TreeMojo"
230267

268+
/**
269+
* A special exception class to indicate that a Tycho build failed completely.
270+
*/
271+
class TychoBuildException(message: String, cause: Throwable? = null) : Exception(message, cause)
272+
231273
/**
232274
* An internal helper class that gets registered as a Maven lifecycle participant to obtain all [MavenProject]s
233275
* encountered during the build.

‎plugins/package-managers/maven/src/test/kotlin/TychoTest.kt

+70
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919

2020
package org.ossreviewtoolkit.plugins.packagemanagers.maven
2121

22+
import io.kotest.assertions.throwables.shouldThrow
2223
import io.kotest.core.TestConfiguration
2324
import io.kotest.core.spec.style.WordSpec
2425
import io.kotest.engine.spec.tempdir
2526
import io.kotest.engine.spec.tempfile
27+
import io.kotest.inspectors.forAll
2628
import io.kotest.matchers.collections.beEmpty
2729
import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
2830
import io.kotest.matchers.should
@@ -48,6 +50,7 @@ import org.ossreviewtoolkit.model.Severity
4850
import org.ossreviewtoolkit.model.utils.DependencyGraphBuilder
4951
import org.ossreviewtoolkit.plugins.packagemanagers.maven.utils.DependencyTreeMojoNode
5052
import org.ossreviewtoolkit.plugins.packagemanagers.maven.utils.JSON
53+
import org.ossreviewtoolkit.plugins.packagemanagers.maven.utils.identifier
5154

5255
class TychoTest : WordSpec({
5356
"mapDefinitionFiles()" should {
@@ -116,6 +119,73 @@ class TychoTest : WordSpec({
116119

117120
subResults.single().issues should beEmpty()
118121
}
122+
123+
"throw an exception if the root project could not be built" {
124+
val definitionFile = tempfile()
125+
val rootProject = createMavenProject("root", definitionFile)
126+
val subProject1 = createMavenProject("sub1")
127+
val subProject2 = createMavenProject("sub2")
128+
129+
val buildOutput = listOf(subProject2, rootProject, subProject1).toJson()
130+
131+
val tycho = spyk(Tycho("Tycho", tempdir(), mockk(relaxed = true), mockk(relaxed = true)))
132+
injectCliMock(tycho, buildOutput, listOf(subProject1, subProject2))
133+
134+
val exception = shouldThrow<TychoBuildException> {
135+
tycho.resolveDependencies(definitionFile, emptyMap())
136+
}
137+
138+
exception.message shouldContain "Tycho root project could not be built."
139+
}
140+
141+
"generate an issue if the Maven build had a non-zero exit code" {
142+
val definitionFile = tempfile()
143+
val rootProject = createMavenProject("root", definitionFile)
144+
val subProject = createMavenProject("sub")
145+
val projectsList = listOf(rootProject, subProject)
146+
147+
val tycho = spyk(Tycho("Tycho", tempdir(), mockk(relaxed = true), mockk(relaxed = true)))
148+
injectCliMock(tycho, projectsList.toJson(), projectsList, exitCode = 1)
149+
150+
val results = tycho.resolveDependencies(definitionFile, emptyMap())
151+
152+
val rootResults = results.single { it.project.id.name == "root" }
153+
with(rootResults.issues.single()) {
154+
severity shouldBe Severity.ERROR
155+
message shouldContain "Maven build failed"
156+
source shouldBe "Tycho"
157+
}
158+
}
159+
160+
"generate issues for sub projects that do not occur in the build output" {
161+
val definitionFile = tempfile()
162+
val rootProject = createMavenProject("root", definitionFile)
163+
val subProject1 = createMavenProject("sub1")
164+
val subProject2 = createMavenProject("sub2")
165+
val subProject3 = createMavenProject("sub3")
166+
val projectsList = listOf(rootProject, subProject1, subProject2, subProject3)
167+
168+
val tycho = spyk(Tycho("Tycho", tempdir(), mockk(relaxed = true), mockk(relaxed = true)))
169+
injectCliMock(tycho, projectsList.take(2).toJson(), projectsList, exitCode = 1)
170+
171+
val results = tycho.resolveDependencies(definitionFile, emptyMap())
172+
173+
val rootResults = results.single { it.project.id.name == "root" }
174+
rootResults.issues.forAll { issue ->
175+
issue.source shouldBe "Tycho"
176+
issue.severity shouldBe Severity.ERROR
177+
}
178+
179+
val regExBuildProjectError = Regex(".*for project '(.*)'.*")
180+
val projectIssues = rootResults.issues.mapNotNull { issue ->
181+
regExBuildProjectError.matchEntire(issue.message)?.groupValues?.get(1)
182+
}
183+
184+
projectIssues shouldContainExactlyInAnyOrder listOf(
185+
subProject2.identifier("Tycho").toCoordinates(),
186+
subProject3.identifier("Tycho").toCoordinates()
187+
)
188+
}
119189
}
120190
})
121191

0 commit comments

Comments
 (0)
Please sign in to comment.