Separate queue wait from build duration

BuildResult gains runningSince, set when a build leaves the queue; the
recorded duration now measures pure build time from that point, so build
runtimes can be tracked without queue wait. A build cancelled while still
queued records neither. The UI shows the live wait time in italics while
pending and switches to the real build time once the build runs; the Gitea
"after mm:ss" descriptions now also report pure build time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-10 15:55:36 +02:00
co-authored by Claude Fable 5
parent a03f85bc17
commit 56aa306481
12 changed files with 112 additions and 17 deletions
@@ -11,6 +11,8 @@ import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
import io.kotest.matchers.ints.shouldBeGreaterThan
import io.kotest.matchers.longs.shouldBeGreaterThan
import io.kotest.matchers.longs.shouldBeLessThan
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
@@ -157,6 +159,51 @@ class BuildExecutorTest : FunSpec() {
awaitStatus(h, "main", BuildStatus.CANCELLED)
}
test("the duration measures build time only, not the queue wait") {
// the first build sleeps, the second (queued behind it) finishes instantly
val h = harness("test -f slow-done || { touch slow-done; sleep 2; }")
h.executor.startBuild("main", "abc123", h.workingDir)
val second = h.executor.startBuild("main", "abc124", h.workingDir)
eventually(30.seconds) {
h.repository
.history()
.first { it.artifactKey == second.artifactKey }
.status shouldBe BuildStatus.SUCCESS
}
val result = h.repository.history().first { it.artifactKey == second.artifactKey }
val runningSince = result.runningSince.shouldNotBeNull()
val waitMillis =
java.time.Duration
.between(result.startedAt, runningSince)
.toMillis()
waitMillis shouldBeGreaterThan 1000L
result.duration.shouldNotBeNull().toMillis() shouldBeLessThan waitMillis
}
test("a build cancelled while still queued records neither runningSince nor a duration") {
val h = harness("sleep 30")
val first = h.executor.startBuild("main", "abc123", h.workingDir)
val second = h.executor.startBuild("main", "abc124", h.workingDir)
eventually(30.seconds) {
h.executor.currentBuilds().map { it.artifactKey } shouldContain first.artifactKey
}
h.executor.cancel(second.artifactKey).shouldBeTrue()
h.executor.cancel(first.artifactKey).shouldBeTrue()
eventually(30.seconds) {
h.repository
.history()
.first { it.artifactKey == second.artifactKey }
.status shouldBe BuildStatus.CANCELLED
}
val cancelled = h.repository.history().first { it.artifactKey == second.artifactKey }
cancelled.runningSince shouldBe null
cancelled.duration shouldBe null
}
test("a failing build command records FAILED with a duration") {
val h = harness("exit 3")