Show a failed-badge behind report links on the artifact index
Each listed report link now carries the failures counter parsed from the report's Gradle-style index.html (id="failures" info box); reports with failures get a red "N failed" badge, so a red build reveals which report to open without clicking through all of them. Pages without such a counter (Jacoco, profile, documentation) stay unmarked. A full-text FAILED scan was deliberately not used: even green hsadmin-ng builds contain the word in embedded test output. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
4ca077da80
commit
b351ab33c5
@@ -209,8 +209,9 @@ class UiController(
|
|||||||
/**
|
/**
|
||||||
* The browsable `index.html` pages under `reports/`, shallowest first; pages nested
|
* The browsable `index.html` pages under `reports/`, shallowest first; pages nested
|
||||||
* below an already-listed report index are skipped — like the legacy artifact index.
|
* 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<String> {
|
private fun reportIndexes(artifactDir: Path): List<ReportIndexView> {
|
||||||
val reportsDir = artifactDir.resolve("reports")
|
val reportsDir = artifactDir.resolve("reports")
|
||||||
if (!Files.isDirectory(reportsDir)) {
|
if (!Files.isDirectory(reportsDir)) {
|
||||||
return emptyList()
|
return emptyList()
|
||||||
@@ -234,6 +235,26 @@ class UiController(
|
|||||||
knownDirs += dir
|
knownDirs += dir
|
||||||
topmost += relativeIndex
|
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
|
||||||
|
* (`<div class="infoBox" id="failures"><div class="counter">N</div>…`);
|
||||||
|
* 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*<div class="counter">(\d+)""")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`. */
|
/** One card of the current-builds view; the live log is fetched by `gittally.js`. */
|
||||||
data class CurrentBuildView(
|
data class CurrentBuildView(
|
||||||
val branch: String,
|
val branch: String,
|
||||||
|
|||||||
@@ -62,8 +62,10 @@
|
|||||||
<h2>Build Artifacts</h2>
|
<h2>Build Artifacts</h2>
|
||||||
<ul th:if="${!#lists.isEmpty(reportIndexes)}">
|
<ul th:if="${!#lists.isEmpty(reportIndexes)}">
|
||||||
<li th:each="report : ${reportIndexes}">
|
<li th:each="report : ${reportIndexes}">
|
||||||
<a th:href="${filesBase} + '/reports/' + ${report}" target="_blank"
|
<a th:href="${filesBase} + '/reports/' + ${report.path}" target="_blank"
|
||||||
rel="noopener noreferrer" th:text="'reports/' + ${report}">reports/tests/index.html</a>
|
rel="noopener noreferrer" th:text="'reports/' + ${report.path}">reports/tests/index.html</a>
|
||||||
|
<span th:if="${report.failures != null and report.failures > 0}" class="status status-failed"
|
||||||
|
th:text="${report.failures} + ' failed'">2 failed</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p th:if="${#lists.isEmpty(reportIndexes)}" class="muted">
|
<p th:if="${#lists.isEmpty(reportIndexes)}" class="muted">
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import de.hoennig.gittally.metrics.MetricAggregate
|
|||||||
import de.hoennig.gittally.metrics.SystemMetrics
|
import de.hoennig.gittally.metrics.SystemMetrics
|
||||||
import de.hoennig.gittally.metrics.SystemMetricsCollector
|
import de.hoennig.gittally.metrics.SystemMetricsCollector
|
||||||
import io.kotest.core.spec.style.FunSpec
|
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.clearMocks
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import org.hamcrest.Matchers.containsString
|
import org.hamcrest.Matchers.containsString
|
||||||
@@ -233,6 +236,40 @@ class UiControllerTest : FunSpec() {
|
|||||||
).andExpect(content().string(not(containsString("reports/tests/test/packages/index.html"))))
|
).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"),
|
||||||
|
"""<div class="infoBox" id="failures">
|
||||||
|
<div class="counter">2</div>
|
||||||
|
<p>failures</p></div>""",
|
||||||
|
)
|
||||||
|
Files.createDirectories(artifactDir.resolve("reports/tests/unitTest"))
|
||||||
|
Files.writeString(
|
||||||
|
artifactDir.resolve("reports/tests/unitTest/index.html"),
|
||||||
|
"""<div class="infoBox" id="failures">
|
||||||
|
<div class="counter">0</div>
|
||||||
|
<p>failures</p></div>""",
|
||||||
|
)
|
||||||
|
Files.createDirectories(artifactDir.resolve("reports/jacoco"))
|
||||||
|
Files.writeString(artifactDir.resolve("reports/jacoco/index.html"), "<html>no counter</html>")
|
||||||
|
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</span>""").findAll(page).count() shouldBe 1
|
||||||
|
}
|
||||||
|
|
||||||
test("artifact index of a pruned build explains the missing artifacts") {
|
test("artifact index of a pruned build explains the missing artifacts") {
|
||||||
every { repository.history() } returns listOf(successResult)
|
every { repository.history() } returns listOf(successResult)
|
||||||
every { artifactStore.artifactDir("main-abc123-key") } returns null
|
every { artifactStore.artifactDir("main-abc123-key") } returns null
|
||||||
|
|||||||
Reference in New Issue
Block a user