diff --git a/src/main/kotlin/de/hoennig/gittally/server/UiController.kt b/src/main/kotlin/de/hoennig/gittally/server/UiController.kt index fb67a54..934f2d3 100644 --- a/src/main/kotlin/de/hoennig/gittally/server/UiController.kt +++ b/src/main/kotlin/de/hoennig/gittally/server/UiController.kt @@ -209,8 +209,9 @@ class UiController( /** * The browsable `index.html` pages under `reports/`, shallowest first; pages nested * below an already-listed report index are skipped — like the legacy artifact index. + * Each entry carries the report's failures counter for the failed-badge. */ - private fun reportIndexes(artifactDir: Path): List { + private fun reportIndexes(artifactDir: Path): List { val reportsDir = artifactDir.resolve("reports") if (!Files.isDirectory(reportsDir)) { return emptyList() @@ -234,6 +235,26 @@ class UiController( knownDirs += dir topmost += relativeIndex } - return topmost + return topmost.map { ReportIndexView(path = it, failures = reportFailures(reportsDir.resolve(it))) } + } + + /** + * The failures counter of a Gradle-style HTML test report index + * (`
N
…`); + * null for report pages without one, e.g. Jacoco or profile reports. + */ + private fun reportFailures(indexFile: Path): Int? = + try { + FAILURES_COUNTER + .find(Files.readString(indexFile)) + ?.groupValues + ?.get(1) + ?.toIntOrNull() + } catch (_: Exception) { + null + } + + companion object { + private val FAILURES_COUNTER = Regex("""id="failures">\s*
(\d+)""") } } diff --git a/src/main/kotlin/de/hoennig/gittally/server/UiViews.kt b/src/main/kotlin/de/hoennig/gittally/server/UiViews.kt index d879937..12161f8 100644 --- a/src/main/kotlin/de/hoennig/gittally/server/UiViews.kt +++ b/src/main/kotlin/de/hoennig/gittally/server/UiViews.kt @@ -126,6 +126,16 @@ data class BuildRowView( } } +/** + * One report link of the artifact index. [failures] is the failures counter of a + * Gradle-style HTML report index page; null when the page declares none (e.g. Jacoco), + * so only real test reports get a failed-badge. + */ +data class ReportIndexView( + val path: String, + val failures: Int?, +) + /** One card of the current-builds view; the live log is fetched by `gittally.js`. */ data class CurrentBuildView( val branch: String, diff --git a/src/main/resources/templates/artifact.html b/src/main/resources/templates/artifact.html index fc3226a..b282dd9 100644 --- a/src/main/resources/templates/artifact.html +++ b/src/main/resources/templates/artifact.html @@ -62,8 +62,10 @@

Build Artifacts

diff --git a/src/test/kotlin/de/hoennig/gittally/server/UiControllerTest.kt b/src/test/kotlin/de/hoennig/gittally/server/UiControllerTest.kt index 6c4fac4..6c68404 100644 --- a/src/test/kotlin/de/hoennig/gittally/server/UiControllerTest.kt +++ b/src/test/kotlin/de/hoennig/gittally/server/UiControllerTest.kt @@ -15,6 +15,9 @@ import de.hoennig.gittally.metrics.MetricAggregate import de.hoennig.gittally.metrics.SystemMetrics import de.hoennig.gittally.metrics.SystemMetricsCollector import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotContain import io.mockk.clearMocks import io.mockk.every import org.hamcrest.Matchers.containsString @@ -233,6 +236,40 @@ class UiControllerTest : FunSpec() { ).andExpect(content().string(not(containsString("reports/tests/test/packages/index.html")))) } + test("report links carry a failed-badge from the report's failures counter") { + val artifactDir = Files.createDirectories(tempDir.resolve("main-abc123-key")) + Files.createDirectories(artifactDir.resolve("reports/tests/test")) + Files.writeString( + artifactDir.resolve("reports/tests/test/index.html"), + """

+
2
+

failures

""", + ) + Files.createDirectories(artifactDir.resolve("reports/tests/unitTest")) + Files.writeString( + artifactDir.resolve("reports/tests/unitTest/index.html"), + """
+
0
+

failures

""", + ) + Files.createDirectories(artifactDir.resolve("reports/jacoco")) + Files.writeString(artifactDir.resolve("reports/jacoco/index.html"), "no counter") + every { repository.history() } returns listOf(successResult) + every { artifactStore.artifactDir("main-abc123-key") } returns artifactDir + + val page = + mockMvc + .perform(get("/builds/main-abc123-key")) + .andExpect(status().isOk) + .andReturn() + .response.contentAsString + + page shouldContain "2 failed" + page shouldNotContain "0 failed" + // exactly one badge: the report with failures, none for the clean or counter-less reports + Regex(""" failed""").findAll(page).count() shouldBe 1 + } + test("artifact index of a pruned build explains the missing artifacts") { every { repository.history() } returns listOf(successResult) every { artifactStore.artifactDir("main-abc123-key") } returns null