step 22b: repo context (#11)
* Step 22 B: RepoContext over the current repository A RepoContext bundles a repository's primary checkout with the state that lives inside or is keyed by it (results, artifact store) and carries its name. Today there is exactly one, opened over the current working directory; the result and artifact-store beans now come from it, so nothing else changes yet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Step 22 B: the watcher polls a RepoContext start/poll/recoverOnStartup take the context instead of a working directory and read results and artifacts from it; the per-repository poll memory (logged fetch error, deprecation warning, cached branch definitions) moves into a RepoWatch keyed by context, so the next session can iterate contexts without one repository's outage silencing another's. The shared WatcherState is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Step 22 B: the executor runs builds of a RepoContext startBuild takes the context first; builds serialize per (context, branch) and share the global maxConcurrent cap across repositories, results and artifacts go to the build's own context. ConsoleBuildRunner, the build/retry commands and the builds API restart pass the current repository's context along. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Step 22 B: the UI and the branch listing read their RepoContext UiController and BuildsApiController take the current repository's context instead of a settable working directory; BranchListing lists the branches of a context and reads the results from it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Step 22 B: document the RepoContext, PR-doc for PR #11 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
713edf77d6
commit
096cce3659
+3
-3
@@ -6,6 +6,7 @@ import de.hoennig.werkator.build.FileBuildResultRepository
|
||||
import de.hoennig.werkator.build.ProcessBuildRunner
|
||||
import de.hoennig.werkator.config.ConfigLoader
|
||||
import de.hoennig.werkator.gitea.GiteaClient
|
||||
import de.hoennig.werkator.repo.RepoContext
|
||||
import io.kotest.assertions.nondeterministic.eventually
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.nulls.shouldNotBeNull
|
||||
@@ -37,18 +38,17 @@ class BuildExecutorArtifactIntegrationTest : FunSpec() {
|
||||
)
|
||||
val workspace = Files.createDirectories(workingDir.resolve("workspace"))
|
||||
val store = FileArtifactStore(ConfigLoader(), workingDir)
|
||||
val repo = RepoContext("test", workingDir, FileBuildResultRepository(workingDir.resolve("build-results.json")), store)
|
||||
val executor =
|
||||
BuildExecutor(
|
||||
repository = FileBuildResultRepository(workingDir.resolve("build-results.json")),
|
||||
configLoader = ConfigLoader(),
|
||||
giteaClient = mockk<GiteaClient>(relaxed = true),
|
||||
buildRunner = ProcessBuildRunner(),
|
||||
workspaces = BranchWorkspaces { _, _, _ -> workspace },
|
||||
artifactStore = store,
|
||||
eventPublisher = ApplicationEventPublisher { },
|
||||
)
|
||||
|
||||
val build = executor.startBuild("main", "abc123", workingDir)
|
||||
val build = executor.startBuild(repo, "main", "abc123")
|
||||
|
||||
lateinit var artifactDir: java.nio.file.Path
|
||||
eventually(30.seconds) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.hoennig.werkator.build
|
||||
|
||||
import de.hoennig.werkator.config.ConfigLoader
|
||||
import de.hoennig.werkator.gitea.GiteaClient
|
||||
import de.hoennig.werkator.repo.RepoContext
|
||||
import io.kotest.assertions.nondeterministic.eventually
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.booleans.shouldBeFalse
|
||||
@@ -48,14 +49,13 @@ class BuildExecutorTest : FunSpec() {
|
||||
Files.createDirectories(workingDir.resolve(workspaceSubdir))
|
||||
}
|
||||
}
|
||||
val repo = RepoContext("test", workingDir, repository, artifactStore)
|
||||
val executor =
|
||||
BuildExecutor(
|
||||
repository = repository,
|
||||
configLoader = ConfigLoader(),
|
||||
giteaClient = giteaClient,
|
||||
buildRunner = buildRunner,
|
||||
workspaces = workspaces,
|
||||
artifactStore = artifactStore,
|
||||
eventPublisher =
|
||||
ApplicationEventPublisher { event ->
|
||||
if (event is BuildStatusChangedEvent) {
|
||||
@@ -112,7 +112,7 @@ class BuildExecutorTest : FunSpec() {
|
||||
cleanCommand = "echo clean-\$branch",
|
||||
)
|
||||
|
||||
val build = h.executor.startBuild("main", "abc123", h.workingDir)
|
||||
val build = h.executor.startBuild(h.repo, "main", "abc123")
|
||||
|
||||
awaitStatus(h, "main", BuildStatus.SUCCESS)
|
||||
awaitIdle(h)
|
||||
@@ -141,7 +141,7 @@ class BuildExecutorTest : FunSpec() {
|
||||
test("build commands run in the workspace prepared for the branch") {
|
||||
val h = harness(buildCommand = "pwd", workspaceSubdir = "branch-workspace")
|
||||
|
||||
val build = h.executor.startBuild("main", "abc123", h.workingDir)
|
||||
val build = h.executor.startBuild(h.repo, "main", "abc123")
|
||||
|
||||
awaitStatus(h, "main", BuildStatus.SUCCESS)
|
||||
awaitIdle(h)
|
||||
@@ -151,7 +151,7 @@ class BuildExecutorTest : FunSpec() {
|
||||
test("the repository reports RUNNING while the build sleeps") {
|
||||
val h = harness("sleep 10")
|
||||
|
||||
val build = h.executor.startBuild("main", "abc123", h.workingDir)
|
||||
val build = h.executor.startBuild(h.repo, "main", "abc123")
|
||||
|
||||
eventually(10.seconds) {
|
||||
h.repository.latestFor("main")?.status shouldBe BuildStatus.RUNNING
|
||||
@@ -166,8 +166,8 @@ class BuildExecutorTest : FunSpec() {
|
||||
// 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)
|
||||
h.executor.startBuild(h.repo, "main", "abc123")
|
||||
val second = h.executor.startBuild(h.repo, "main", "abc124")
|
||||
|
||||
eventually(30.seconds) {
|
||||
h.repository
|
||||
@@ -207,7 +207,7 @@ class BuildExecutorTest : FunSpec() {
|
||||
}
|
||||
val h = harness("unused", buildRunner = auxRunner)
|
||||
|
||||
val build = h.executor.startBuild("main", "abc123", h.workingDir)
|
||||
val build = h.executor.startBuild(h.repo, "main", "abc123")
|
||||
eventually(10.seconds) {
|
||||
h.repository.latestFor("main")?.status shouldBe BuildStatus.RUNNING
|
||||
}
|
||||
@@ -232,7 +232,7 @@ class BuildExecutorTest : FunSpec() {
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
val nightly = h.executor.startBuild("main", "sha-1", h.workingDir, "pitest")
|
||||
val nightly = h.executor.startBuild(h.repo, "main", "sha-1", "pitest")
|
||||
awaitStatus(h, "main@pitest", BuildStatus.SUCCESS)
|
||||
awaitIdle(h)
|
||||
|
||||
@@ -250,7 +250,7 @@ class BuildExecutorTest : FunSpec() {
|
||||
h.repository.latestFor("main") shouldBe null
|
||||
|
||||
// the same branch under the default build runs the regular command
|
||||
val regular = h.executor.startBuild("main", "sha-2", h.workingDir)
|
||||
val regular = h.executor.startBuild(h.repo, "main", "sha-2")
|
||||
awaitStatus(h, "main", BuildStatus.SUCCESS)
|
||||
awaitIdle(h)
|
||||
Files.readString(regular.stagingDir.resolve("build.stdout.log")) shouldContain "regular-main"
|
||||
@@ -263,7 +263,7 @@ class BuildExecutorTest : FunSpec() {
|
||||
test("a build whose definition was removed from the config falls back to the branch's settings") {
|
||||
val h = harness(buildCommand = "echo regular-\$branch")
|
||||
|
||||
val build = h.executor.startBuild("main", "sha-1", h.workingDir, "gone-build")
|
||||
val build = h.executor.startBuild(h.repo, "main", "sha-1", "gone-build")
|
||||
awaitStatus(h, "main@gone-build", BuildStatus.SUCCESS)
|
||||
awaitIdle(h)
|
||||
|
||||
@@ -273,23 +273,23 @@ class BuildExecutorTest : FunSpec() {
|
||||
test("startBuild returns the active build of the same branch and commit instead of stacking a duplicate") {
|
||||
val h = harness("sleep 30")
|
||||
|
||||
val first = h.executor.startBuild("main", "abc123", h.workingDir)
|
||||
val first = h.executor.startBuild(h.repo, "main", "abc123")
|
||||
// a double-triggered UI restart: same branch, same commit, while queued or running
|
||||
val duplicate = h.executor.startBuild("main", "abc123", h.workingDir)
|
||||
val duplicate = h.executor.startBuild(h.repo, "main", "abc123")
|
||||
duplicate.artifactKey shouldBe first.artifactKey
|
||||
h.repository.history().map { it.artifactKey } shouldContainExactly listOf(first.artifactKey)
|
||||
|
||||
// another build definition of the same commit is its own pool — not a duplicate
|
||||
val nightly = h.executor.startBuild("main", "abc123", h.workingDir, "pitest")
|
||||
val nightly = h.executor.startBuild(h.repo, "main", "abc123", "pitest")
|
||||
nightly.artifactKey shouldNotBe first.artifactKey
|
||||
|
||||
// another commit of the branch is a distinct build, queued behind the first
|
||||
val newerCommit = h.executor.startBuild("main", "abc124", h.workingDir)
|
||||
val newerCommit = h.executor.startBuild(h.repo, "main", "abc124")
|
||||
newerCommit.artifactKey shouldNotBe first.artifactKey
|
||||
|
||||
// a cancel-requested build no longer blocks re-queueing its commit
|
||||
h.executor.cancel(first.artifactKey).shouldBeTrue()
|
||||
val again = h.executor.startBuild("main", "abc123", h.workingDir)
|
||||
val again = h.executor.startBuild(h.repo, "main", "abc123")
|
||||
again.artifactKey shouldNotBe first.artifactKey
|
||||
|
||||
h.executor.cancel(nightly.artifactKey).shouldBeTrue()
|
||||
@@ -303,8 +303,8 @@ class BuildExecutorTest : FunSpec() {
|
||||
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)
|
||||
val first = h.executor.startBuild(h.repo, "main", "abc123")
|
||||
val second = h.executor.startBuild(h.repo, "main", "abc124")
|
||||
eventually(30.seconds) {
|
||||
h.executor.currentBuilds().map { it.artifactKey } shouldContain first.artifactKey
|
||||
}
|
||||
@@ -325,7 +325,7 @@ class BuildExecutorTest : FunSpec() {
|
||||
test("a failing build command records FAILED with a duration") {
|
||||
val h = harness("exit 3")
|
||||
|
||||
h.executor.startBuild("main", "abc123", h.workingDir)
|
||||
h.executor.startBuild(h.repo, "main", "abc123")
|
||||
|
||||
awaitStatus(h, "main", BuildStatus.FAILED)
|
||||
awaitIdle(h)
|
||||
@@ -337,7 +337,7 @@ class BuildExecutorTest : FunSpec() {
|
||||
test("a failing clean command fails the build without running the build command") {
|
||||
val h = harness(buildCommand = "echo forbidden-\$branch", cleanCommand = "exit 1")
|
||||
|
||||
val build = h.executor.startBuild("main", "abc123", h.workingDir)
|
||||
val build = h.executor.startBuild(h.repo, "main", "abc123")
|
||||
|
||||
awaitStatus(h, "main", BuildStatus.FAILED)
|
||||
awaitIdle(h)
|
||||
@@ -348,7 +348,7 @@ class BuildExecutorTest : FunSpec() {
|
||||
test("cancel kills a sleeping process tree and records CANCELLED") {
|
||||
val h = harness("echo \$\$ > pid-file; sleep 30 & sleep 30 & wait")
|
||||
|
||||
val build = h.executor.startBuild("main", "abc123", h.workingDir)
|
||||
val build = h.executor.startBuild(h.repo, "main", "abc123")
|
||||
|
||||
lateinit var root: ProcessHandle
|
||||
var children = emptyList<ProcessHandle>()
|
||||
@@ -376,7 +376,7 @@ class BuildExecutorTest : FunSpec() {
|
||||
test("shutdown kills an executing build and records INTERRUPTED, not FAILED") {
|
||||
val h = harness("echo \$\$ > pid-file; sleep 30")
|
||||
|
||||
val build = h.executor.startBuild("main", "abc123", h.workingDir)
|
||||
val build = h.executor.startBuild(h.repo, "main", "abc123")
|
||||
eventually(10.seconds) {
|
||||
Files.exists(h.workingDir.resolve("pid-file")).shouldBeTrue()
|
||||
}
|
||||
@@ -402,8 +402,8 @@ class BuildExecutorTest : FunSpec() {
|
||||
test("a build still queued at shutdown stays PENDING for the startup recovery") {
|
||||
val h = harness("sleep 30")
|
||||
|
||||
val first = h.executor.startBuild("main", "sha-1", h.workingDir)
|
||||
val second = h.executor.startBuild("main", "sha-2", h.workingDir)
|
||||
val first = h.executor.startBuild(h.repo, "main", "sha-1")
|
||||
val second = h.executor.startBuild(h.repo, "main", "sha-2")
|
||||
eventually(10.seconds) {
|
||||
h.repository
|
||||
.history()
|
||||
@@ -432,7 +432,7 @@ class BuildExecutorTest : FunSpec() {
|
||||
test("shutdown without any build in flight is a no-op") {
|
||||
val h = harness("echo ok")
|
||||
|
||||
h.executor.startBuild("main", "abc123", h.workingDir)
|
||||
h.executor.startBuild(h.repo, "main", "abc123")
|
||||
awaitStatus(h, "main", BuildStatus.SUCCESS)
|
||||
awaitIdle(h)
|
||||
|
||||
@@ -450,7 +450,7 @@ class BuildExecutorTest : FunSpec() {
|
||||
test("the live log grows while the build is still running") {
|
||||
val h = harness("echo one-\$branch; sleep 3; echo two-\$branch")
|
||||
|
||||
val build = h.executor.startBuild("main", "abc123", h.workingDir)
|
||||
val build = h.executor.startBuild(h.repo, "main", "abc123")
|
||||
|
||||
eventually(10.seconds) {
|
||||
Files.readString(build.liveLogFile) shouldContain "one-main"
|
||||
@@ -467,7 +467,7 @@ class BuildExecutorTest : FunSpec() {
|
||||
h.giteaClient.publishStatus(any(), any(), any(), any(), any(), any())
|
||||
} throws RuntimeException("gitea down")
|
||||
|
||||
h.executor.startBuild("main", "abc123", h.workingDir)
|
||||
h.executor.startBuild(h.repo, "main", "abc123")
|
||||
|
||||
awaitStatus(h, "main", BuildStatus.SUCCESS)
|
||||
}
|
||||
@@ -488,8 +488,8 @@ class BuildExecutorTest : FunSpec() {
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
h.executor.startBuild("branch-a", "sha-a", h.workingDir)
|
||||
h.executor.startBuild("branch-b", "sha-b", h.workingDir)
|
||||
h.executor.startBuild(h.repo, "branch-a", "sha-a")
|
||||
h.executor.startBuild(h.repo, "branch-b", "sha-b")
|
||||
|
||||
h.repository.latestFor("branch-b")?.status shouldBe BuildStatus.PENDING
|
||||
|
||||
@@ -503,8 +503,8 @@ class BuildExecutorTest : FunSpec() {
|
||||
test("with maxConcurrent 2 two branches build at the same time") {
|
||||
val h = harness("sleep 10", maxConcurrent = 2)
|
||||
|
||||
val buildA = h.executor.startBuild("branch-a", "sha-a", h.workingDir)
|
||||
val buildB = h.executor.startBuild("branch-b", "sha-b", h.workingDir)
|
||||
val buildA = h.executor.startBuild(h.repo, "branch-a", "sha-a")
|
||||
val buildB = h.executor.startBuild(h.repo, "branch-b", "sha-b")
|
||||
|
||||
eventually(10.seconds) {
|
||||
h.repository.latestFor("branch-a")?.status shouldBe BuildStatus.RUNNING
|
||||
@@ -522,8 +522,8 @@ class BuildExecutorTest : FunSpec() {
|
||||
test("a second build of the same branch waits even when a slot is free") {
|
||||
val h = harness("sleep 1", maxConcurrent = 2)
|
||||
|
||||
val first = h.executor.startBuild("main", "sha-1", h.workingDir)
|
||||
val second = h.executor.startBuild("main", "sha-2", h.workingDir)
|
||||
val first = h.executor.startBuild(h.repo, "main", "sha-1")
|
||||
val second = h.executor.startBuild(h.repo, "main", "sha-2")
|
||||
|
||||
eventually(30.seconds) {
|
||||
h.repository
|
||||
@@ -539,8 +539,8 @@ class BuildExecutorTest : FunSpec() {
|
||||
test("cancel only affects the addressed build, other branches keep running") {
|
||||
val h = harness("sleep 10", maxConcurrent = 2)
|
||||
|
||||
val buildA = h.executor.startBuild("branch-a", "sha-a", h.workingDir)
|
||||
val buildB = h.executor.startBuild("branch-b", "sha-b", h.workingDir)
|
||||
val buildA = h.executor.startBuild(h.repo, "branch-a", "sha-a")
|
||||
val buildB = h.executor.startBuild(h.repo, "branch-b", "sha-b")
|
||||
eventually(10.seconds) {
|
||||
h.repository.latestFor("branch-a")?.status shouldBe BuildStatus.RUNNING
|
||||
h.repository.latestFor("branch-b")?.status shouldBe BuildStatus.RUNNING
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.hoennig.werkator.commands
|
||||
|
||||
import de.hoennig.werkator.build.BuildStatus
|
||||
import de.hoennig.werkator.git.GitService
|
||||
import de.hoennig.werkator.repo.RepoContext
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.string.shouldContain
|
||||
@@ -18,11 +19,11 @@ class BuildCommandTest : FunSpec() {
|
||||
private val gitService = mockk<GitService>()
|
||||
private val consoleBuildRunner = mockk<ConsoleBuildRunner>()
|
||||
private val dir: Path = Paths.get(".")
|
||||
private val repo = RepoContext("test", dir, mockk(), mockk())
|
||||
|
||||
private fun command(fragment: String? = null) =
|
||||
BuildCommand(gitService, consoleBuildRunner).apply {
|
||||
BuildCommand(gitService, consoleBuildRunner, repo).apply {
|
||||
branchFragment = fragment
|
||||
workingDir = dir
|
||||
}
|
||||
|
||||
init {
|
||||
@@ -35,13 +36,13 @@ class BuildCommandTest : FunSpec() {
|
||||
every { gitService.currentBranch(dir) } returns "main"
|
||||
every { gitService.localHeadCommit("main", dir) } returns "local-head"
|
||||
every { gitService.hasNewCommits("main", dir) } returns false
|
||||
every { consoleBuildRunner.buildAndStream("main", "local-head", dir) } returns BuildStatus.SUCCESS
|
||||
every { consoleBuildRunner.buildAndStream(repo, "main", "local-head") } returns BuildStatus.SUCCESS
|
||||
|
||||
var exitCode = -1
|
||||
captureConsole { exitCode = command().call() }
|
||||
|
||||
exitCode shouldBe 0
|
||||
verify { consoleBuildRunner.buildAndStream("main", "local-head", dir) }
|
||||
verify { consoleBuildRunner.buildAndStream(repo, "main", "local-head") }
|
||||
}
|
||||
|
||||
test("builds origin's head when the branch has new commits on origin") {
|
||||
@@ -49,20 +50,20 @@ class BuildCommandTest : FunSpec() {
|
||||
every { gitService.localHeadCommit("main", dir) } returns "local-head"
|
||||
every { gitService.hasNewCommits("main", dir) } returns true
|
||||
every { gitService.originHeadCommit("main", dir) } returns "origin-head"
|
||||
every { consoleBuildRunner.buildAndStream("main", "origin-head", dir) } returns BuildStatus.SUCCESS
|
||||
every { consoleBuildRunner.buildAndStream(repo, "main", "origin-head") } returns BuildStatus.SUCCESS
|
||||
|
||||
var exitCode = -1
|
||||
captureConsole { exitCode = command().call() }
|
||||
|
||||
exitCode shouldBe 0
|
||||
verify { consoleBuildRunner.buildAndStream("main", "origin-head", dir) }
|
||||
verify { consoleBuildRunner.buildAndStream(repo, "main", "origin-head") }
|
||||
}
|
||||
|
||||
test("a failing build exits with code 1") {
|
||||
every { gitService.currentBranch(dir) } returns "main"
|
||||
every { gitService.localHeadCommit("main", dir) } returns "local-head"
|
||||
every { gitService.hasNewCommits("main", dir) } returns false
|
||||
every { consoleBuildRunner.buildAndStream("main", "local-head", dir) } returns BuildStatus.FAILED
|
||||
every { consoleBuildRunner.buildAndStream(repo, "main", "local-head") } returns BuildStatus.FAILED
|
||||
|
||||
var exitCode = -1
|
||||
captureConsole { exitCode = command().call() }
|
||||
@@ -75,13 +76,13 @@ class BuildCommandTest : FunSpec() {
|
||||
every { gitService.originBranches(dir) } returns listOf("main", "feature/x")
|
||||
every { gitService.localHeadCommit("feature/x", dir) } returns null
|
||||
every { gitService.originHeadCommit("feature/x", dir) } returns "origin-head"
|
||||
every { consoleBuildRunner.buildAndStream("feature/x", "origin-head", dir) } returns BuildStatus.SUCCESS
|
||||
every { consoleBuildRunner.buildAndStream(repo, "feature/x", "origin-head") } returns BuildStatus.SUCCESS
|
||||
|
||||
var exitCode = -1
|
||||
captureConsole { exitCode = command(fragment = "x").call() }
|
||||
|
||||
exitCode shouldBe 0
|
||||
verify { consoleBuildRunner.buildAndStream("feature/x", "origin-head", dir) }
|
||||
verify { consoleBuildRunner.buildAndStream(repo, "feature/x", "origin-head") }
|
||||
}
|
||||
|
||||
test("an ambiguous fragment lists the candidates and exits with code 2") {
|
||||
@@ -126,7 +127,7 @@ class BuildCommandTest : FunSpec() {
|
||||
every { gitService.currentBranch(dir) } returns "main"
|
||||
every { gitService.localHeadCommit("main", dir) } returns "local-head"
|
||||
every { gitService.hasNewCommits("main", dir) } returns false
|
||||
every { consoleBuildRunner.buildAndStream("main", "local-head", dir) } returns BuildStatus.SUCCESS
|
||||
every { consoleBuildRunner.buildAndStream(repo, "main", "local-head") } returns BuildStatus.SUCCESS
|
||||
|
||||
var exitCode = -1
|
||||
val console = captureConsole { exitCode = command().call() }
|
||||
|
||||
@@ -6,6 +6,7 @@ import de.hoennig.werkator.build.BuildResult
|
||||
import de.hoennig.werkator.build.BuildResultRepository
|
||||
import de.hoennig.werkator.build.BuildStatus
|
||||
import de.hoennig.werkator.build.RunningBuild
|
||||
import de.hoennig.werkator.repo.RepoContext
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.string.shouldContain
|
||||
@@ -23,9 +24,10 @@ class ConsoleBuildRunnerTest : FunSpec() {
|
||||
private val artifactStore = mockk<ArtifactStore>()
|
||||
|
||||
private lateinit var tempDir: Path
|
||||
private lateinit var repo: RepoContext
|
||||
|
||||
private fun runner() =
|
||||
ConsoleBuildRunner(buildExecutor, repository, artifactStore).apply {
|
||||
ConsoleBuildRunner(buildExecutor).apply {
|
||||
pollIntervalMillis = 1
|
||||
persistTimeoutMillis = 100
|
||||
}
|
||||
@@ -56,6 +58,7 @@ class ConsoleBuildRunnerTest : FunSpec() {
|
||||
beforeEach {
|
||||
clearMocks(buildExecutor, repository, artifactStore)
|
||||
tempDir = Files.createTempDirectory("werkator-console-build-test")
|
||||
repo = RepoContext("test", tempDir, repository, artifactStore)
|
||||
}
|
||||
|
||||
afterEach {
|
||||
@@ -66,7 +69,7 @@ class ConsoleBuildRunnerTest : FunSpec() {
|
||||
val stagingDir = Files.createDirectory(tempDir.resolve("staging"))
|
||||
val build = runningBuild(stagingDir)
|
||||
Files.writeString(build.liveLogFile, "compiling ...\ntests green\n")
|
||||
every { buildExecutor.startBuild("main", "0123456789abcdef", tempDir) } returns build
|
||||
every { buildExecutor.startBuild(repo, "main", "0123456789abcdef") } returns build
|
||||
// the terminal status arrives together with the finished persist (staging gone)
|
||||
every { repository.history() } answers {
|
||||
stagingDir.toFile().deleteRecursively()
|
||||
@@ -75,7 +78,7 @@ class ConsoleBuildRunnerTest : FunSpec() {
|
||||
every { artifactStore.artifactDir("main-key") } returns null
|
||||
|
||||
var status: BuildStatus? = null
|
||||
val console = captureConsole { status = runner().buildAndStream("main", "0123456789abcdef", tempDir) }
|
||||
val console = captureConsole { status = runner().buildAndStream(repo, "main", "0123456789abcdef") }
|
||||
|
||||
status shouldBe BuildStatus.SUCCESS
|
||||
console.stdout shouldContain "compiling ...\ntests green\n"
|
||||
@@ -87,12 +90,12 @@ class ConsoleBuildRunnerTest : FunSpec() {
|
||||
val build = runningBuild(stagingDir)
|
||||
val persistedDir = Files.createDirectory(tempDir.resolve("persisted"))
|
||||
Files.writeString(persistedDir.resolve(BuildExecutor.LIVE_LOG_FILE), "full build output\n")
|
||||
every { buildExecutor.startBuild("main", "0123456789abcdef", tempDir) } returns build
|
||||
every { buildExecutor.startBuild(repo, "main", "0123456789abcdef") } returns build
|
||||
every { repository.history() } returns listOf(result(BuildStatus.FAILED))
|
||||
every { artifactStore.artifactDir("main-key") } returns persistedDir
|
||||
|
||||
var status: BuildStatus? = null
|
||||
val console = captureConsole { status = runner().buildAndStream("main", "0123456789abcdef", tempDir) }
|
||||
val console = captureConsole { status = runner().buildAndStream(repo, "main", "0123456789abcdef") }
|
||||
|
||||
status shouldBe BuildStatus.FAILED
|
||||
console.stdout shouldContain "full build output"
|
||||
@@ -103,11 +106,11 @@ class ConsoleBuildRunnerTest : FunSpec() {
|
||||
val stagingDir = Files.createDirectory(tempDir.resolve("staging"))
|
||||
val build = runningBuild(stagingDir)
|
||||
Files.writeString(build.liveLogFile, "some output\n")
|
||||
every { buildExecutor.startBuild("main", "0123456789abcdef", tempDir) } returns build
|
||||
every { buildExecutor.startBuild(repo, "main", "0123456789abcdef") } returns build
|
||||
every { repository.history() } returns listOf(result(BuildStatus.SUCCESS, duration = null))
|
||||
|
||||
var status: BuildStatus? = null
|
||||
val console = captureConsole { status = runner().buildAndStream("main", "0123456789abcdef", tempDir) }
|
||||
val console = captureConsole { status = runner().buildAndStream(repo, "main", "0123456789abcdef") }
|
||||
|
||||
status shouldBe BuildStatus.SUCCESS
|
||||
console.stdout shouldContain "some output"
|
||||
|
||||
@@ -4,6 +4,7 @@ import de.hoennig.werkator.build.BuildResult
|
||||
import de.hoennig.werkator.build.BuildResultRepository
|
||||
import de.hoennig.werkator.build.BuildStatus
|
||||
import de.hoennig.werkator.git.GitService
|
||||
import de.hoennig.werkator.repo.RepoContext
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.string.shouldContain
|
||||
@@ -22,8 +23,9 @@ class RetryCommandTest : FunSpec() {
|
||||
private val repository = mockk<BuildResultRepository>()
|
||||
private val consoleBuildRunner = mockk<ConsoleBuildRunner>()
|
||||
private val dir: Path = Paths.get(".")
|
||||
private val repo = RepoContext("test", dir, repository, mockk())
|
||||
|
||||
private fun command() = RetryCommand(gitService, repository, consoleBuildRunner).apply { workingDir = dir }
|
||||
private fun command() = RetryCommand(gitService, consoleBuildRunner, repo)
|
||||
|
||||
private fun result(
|
||||
branch: String,
|
||||
@@ -52,21 +54,21 @@ class RetryCommandTest : FunSpec() {
|
||||
)
|
||||
every { gitService.originHeadCommit("main", dir) } returns "head-main"
|
||||
every { gitService.originHeadCommit("feature/y", dir) } returns "head-y"
|
||||
every { consoleBuildRunner.buildAndStream(any(), any(), dir, any()) } returns BuildStatus.SUCCESS
|
||||
every { consoleBuildRunner.buildAndStream(repo, any(), any(), any()) } returns BuildStatus.SUCCESS
|
||||
|
||||
var exitCode = -1
|
||||
captureConsole { exitCode = command().call() }
|
||||
|
||||
exitCode shouldBe 0
|
||||
verify { consoleBuildRunner.buildAndStream("main", "head-main", dir, "default") }
|
||||
verify { consoleBuildRunner.buildAndStream("feature/y", "head-y", dir, "default") }
|
||||
verify(exactly = 0) { consoleBuildRunner.buildAndStream("feature/ok", any(), dir, any()) }
|
||||
verify { consoleBuildRunner.buildAndStream(repo, "main", "head-main", "default") }
|
||||
verify { consoleBuildRunner.buildAndStream(repo, "feature/y", "head-y", "default") }
|
||||
verify(exactly = 0) { consoleBuildRunner.buildAndStream(repo, "feature/ok", any(), any()) }
|
||||
}
|
||||
|
||||
test("exits with code 1 when a retried build fails again") {
|
||||
every { repository.latestPerName() } returns listOf(result("main", BuildStatus.FAILED))
|
||||
every { gitService.originHeadCommit("main", dir) } returns "head-main"
|
||||
every { consoleBuildRunner.buildAndStream("main", "head-main", dir) } returns BuildStatus.FAILED
|
||||
every { consoleBuildRunner.buildAndStream(repo, "main", "head-main") } returns BuildStatus.FAILED
|
||||
|
||||
var exitCode = -1
|
||||
captureConsole { exitCode = command().call() }
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package de.hoennig.werkator.repo
|
||||
|
||||
import de.hoennig.werkator.config.ConfigLoader
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
|
||||
class RepoContextsTest : FunSpec() {
|
||||
private val contexts = RepoContexts(ConfigLoader())
|
||||
|
||||
init {
|
||||
test("a context is named after its directory and keeps its state inside the repository") {
|
||||
val dir = Files.createTempDirectory("werkator-repo-context-test").resolve("werkbaum")
|
||||
Files.createDirectories(dir)
|
||||
|
||||
val repo = contexts.open(dir)
|
||||
|
||||
repo.name shouldBe "werkbaum"
|
||||
repo.workingDir shouldBe dir
|
||||
repo.artifactStore
|
||||
.rootDir()
|
||||
.fileName
|
||||
.toString() shouldBe
|
||||
de.hoennig.werkator.build.ArtifactKeys
|
||||
.repoKey(dir)
|
||||
}
|
||||
|
||||
test("the current directory resolves to the same name as its absolute path") {
|
||||
contexts.open(Paths.get(".")).name shouldBe RepoContexts.defaultName(Paths.get(".").toAbsolutePath())
|
||||
}
|
||||
|
||||
test("a filesystem root has no basename and gets the fallback name") {
|
||||
RepoContexts.defaultName(Paths.get("/")) shouldBe "repository"
|
||||
}
|
||||
|
||||
test("the name can be overridden per entry, as the registry will do") {
|
||||
contexts.open(Paths.get("."), name = "custom").name shouldBe "custom"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import de.hoennig.werkator.build.BuildResult
|
||||
import de.hoennig.werkator.build.BuildResultRepository
|
||||
import de.hoennig.werkator.build.BuildStatus
|
||||
import de.hoennig.werkator.git.GitService
|
||||
import de.hoennig.werkator.repo.RepoContext
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.every
|
||||
@@ -14,7 +15,15 @@ import java.time.Instant
|
||||
class BranchListingTest : FunSpec() {
|
||||
private val gitService = mockk<GitService>()
|
||||
private val repository = mockk<BuildResultRepository>()
|
||||
private val listing = BranchListing(gitService, repository)
|
||||
private val repo =
|
||||
RepoContext(
|
||||
"test",
|
||||
java.nio.file.Paths
|
||||
.get("."),
|
||||
repository,
|
||||
mockk(),
|
||||
)
|
||||
private val listing = BranchListing(gitService)
|
||||
|
||||
private val mainResult =
|
||||
BuildResult(
|
||||
@@ -38,7 +47,7 @@ class BranchListingTest : FunSpec() {
|
||||
every { repository.latestFor(any()) } returns null
|
||||
every { repository.latestGreenFor(any()) } returns null
|
||||
|
||||
val rows = listing.branches()
|
||||
val rows = listing.branches(repo)
|
||||
|
||||
// no bare "master" row next to master@release — it would read as "never built"
|
||||
rows.map { it.name } shouldBe listOf("master@release", "idle")
|
||||
@@ -56,7 +65,7 @@ class BranchListingTest : FunSpec() {
|
||||
every { repository.latestFor(any()) } returns null
|
||||
every { repository.latestGreenFor(any()) } returns null
|
||||
|
||||
listing.branches().map { it.branch } shouldBe
|
||||
listing.branches(repo).map { it.branch } shouldBe
|
||||
listOf("main", "develop", "zz-flat", "aa/nested", "feature/x")
|
||||
}
|
||||
|
||||
@@ -68,7 +77,7 @@ class BranchListingTest : FunSpec() {
|
||||
every { repository.latestFor("feature/x") } returns null
|
||||
every { repository.latestGreenFor("feature/x") } returns null
|
||||
|
||||
val branches = listing.branches()
|
||||
val branches = listing.branches(repo)
|
||||
|
||||
branches[0].branch shouldBe "main"
|
||||
branches[0].status shouldBe "success"
|
||||
@@ -90,7 +99,7 @@ class BranchListingTest : FunSpec() {
|
||||
every { repository.latestGreenFor("feature/x") } returns
|
||||
mainResult.copy(branch = "feature/x", name = "feature/x", artifactKey = "green-key")
|
||||
|
||||
val branches = listing.branches()
|
||||
val branches = listing.branches(repo)
|
||||
|
||||
branches[0].status shouldBe "failed"
|
||||
branches[0].latestGreenUrl shouldBe null
|
||||
@@ -107,7 +116,7 @@ class BranchListingTest : FunSpec() {
|
||||
every { repository.latestGreenFor("develop") } returns null
|
||||
every { repository.latestPerName() } returns listOf(mainResult, nightly)
|
||||
|
||||
val rows = listing.branches()
|
||||
val rows = listing.branches(repo)
|
||||
|
||||
rows.map { it.name } shouldBe listOf("main", "main@nightly", "develop")
|
||||
rows[1].branch shouldBe "main"
|
||||
|
||||
@@ -8,6 +8,7 @@ import de.hoennig.werkator.build.BuildResultRepository
|
||||
import de.hoennig.werkator.build.BuildStatus
|
||||
import de.hoennig.werkator.build.RunningBuild
|
||||
import de.hoennig.werkator.git.GitService
|
||||
import de.hoennig.werkator.repo.RepoContext
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.every
|
||||
@@ -50,6 +51,9 @@ class BuildsApiControllerTest : FunSpec() {
|
||||
@MockkBean
|
||||
lateinit var branchListing: BranchListing
|
||||
|
||||
@MockkBean
|
||||
lateinit var repo: RepoContext
|
||||
|
||||
private val startedAt = Instant.parse("2026-07-07T10:00:00Z")
|
||||
|
||||
private val successResult =
|
||||
@@ -74,7 +78,8 @@ class BuildsApiControllerTest : FunSpec() {
|
||||
|
||||
init {
|
||||
beforeEach {
|
||||
clearMocks(repository, buildExecutor, artifactStore, controlTokens, gitService, branchListing)
|
||||
clearMocks(repository, buildExecutor, artifactStore, controlTokens, gitService, branchListing, repo)
|
||||
every { repo.workingDir } returns tempDir
|
||||
every { controlTokens.matches(any()) } answers { firstArg<String?>() == "secret" }
|
||||
every { repository.latestGreenFor(any()) } returns null
|
||||
}
|
||||
@@ -152,7 +157,7 @@ class BuildsApiControllerTest : FunSpec() {
|
||||
test("restart enqueues the branch's last recorded commit, also for branch names with slashes") {
|
||||
val liveLogFile = tempDir.resolve("restart.log")
|
||||
every { repository.latestFor("feature/topic") } returns successResult.copy(branch = "feature/topic", name = "feature/topic")
|
||||
every { buildExecutor.startBuild("feature/topic", successResult.commit) } returns
|
||||
every { buildExecutor.startBuild(repo, "feature/topic", successResult.commit) } returns
|
||||
runningBuild(liveLogFile).copy(branch = "feature/topic", name = "feature/topic")
|
||||
|
||||
mockMvc
|
||||
@@ -164,14 +169,14 @@ class BuildsApiControllerTest : FunSpec() {
|
||||
.andExpect(jsonPath("$.status").value("pending"))
|
||||
.andExpect(jsonPath("$.artifactKey").value("main-abc123-running"))
|
||||
|
||||
verify { buildExecutor.startBuild("feature/topic", successResult.commit) }
|
||||
verify { buildExecutor.startBuild(repo, "feature/topic", successResult.commit) }
|
||||
}
|
||||
|
||||
test("restart of a named build re-runs its build definition on its real branch") {
|
||||
val liveLogFile = tempDir.resolve("named-restart.log")
|
||||
every { repository.latestFor("main@pitest") } returns
|
||||
successResult.copy(build = "pitest", name = "main@pitest")
|
||||
every { buildExecutor.startBuild("main", successResult.commit, build = "pitest") } returns
|
||||
every { buildExecutor.startBuild(repo, "main", successResult.commit, build = "pitest") } returns
|
||||
runningBuild(liveLogFile).copy(build = "pitest", name = "main@pitest")
|
||||
|
||||
mockMvc
|
||||
@@ -183,14 +188,14 @@ class BuildsApiControllerTest : FunSpec() {
|
||||
.andExpect(jsonPath("$.name").value("main@pitest"))
|
||||
|
||||
// the re-run resolves its settings from the current config by the build name
|
||||
verify { buildExecutor.startBuild("main", successResult.commit, build = "pitest") }
|
||||
verify { buildExecutor.startBuild(repo, "main", successResult.commit, build = "pitest") }
|
||||
}
|
||||
|
||||
test("restart with atOriginHead builds the branch as it is now, not the recorded commit") {
|
||||
val liveLogFile = tempDir.resolve("head-restart.log")
|
||||
every { repository.latestFor("main") } returns successResult
|
||||
every { gitService.originHeadCommit("main", any()) } returns "newhead1"
|
||||
every { buildExecutor.startBuild("main", "newhead1") } returns runningBuild(liveLogFile)
|
||||
every { buildExecutor.startBuild(repo, "main", "newhead1") } returns runningBuild(liveLogFile)
|
||||
|
||||
mockMvc
|
||||
.perform(
|
||||
@@ -201,15 +206,15 @@ class BuildsApiControllerTest : FunSpec() {
|
||||
).andExpect(status().isAccepted)
|
||||
|
||||
// the recorded commit is deliberately not used: a branches row stands for a branch
|
||||
verify { buildExecutor.startBuild("main", "newhead1") }
|
||||
verify(exactly = 0) { buildExecutor.startBuild("main", successResult.commit) }
|
||||
verify { buildExecutor.startBuild(repo, "main", "newhead1") }
|
||||
verify(exactly = 0) { buildExecutor.startBuild(repo, "main", successResult.commit) }
|
||||
}
|
||||
|
||||
test("restart with atOriginHead keeps the recorded build definition and its real branch") {
|
||||
val liveLogFile = tempDir.resolve("head-named.log")
|
||||
every { repository.latestFor("main@pitest") } returns successResult.copy(build = "pitest", name = "main@pitest")
|
||||
every { gitService.originHeadCommit("main", any()) } returns "newhead2"
|
||||
every { buildExecutor.startBuild("main", "newhead2", build = "pitest") } returns
|
||||
every { buildExecutor.startBuild(repo, "main", "newhead2", build = "pitest") } returns
|
||||
runningBuild(liveLogFile).copy(build = "pitest", name = "main@pitest")
|
||||
|
||||
mockMvc
|
||||
@@ -220,7 +225,7 @@ class BuildsApiControllerTest : FunSpec() {
|
||||
.header(BuildsApiController.TOKEN_HEADER, "secret"),
|
||||
).andExpect(status().isAccepted)
|
||||
|
||||
verify { buildExecutor.startBuild("main", "newhead2", build = "pitest") }
|
||||
verify { buildExecutor.startBuild(repo, "main", "newhead2", build = "pitest") }
|
||||
}
|
||||
|
||||
test("restart with atOriginHead of a branch gone from origin is refused by name") {
|
||||
@@ -242,7 +247,7 @@ class BuildsApiControllerTest : FunSpec() {
|
||||
val liveLogFile = tempDir.resolve("first-build.log")
|
||||
every { repository.latestFor("fresh") } returns null
|
||||
every { gitService.originHeadCommit("fresh", any()) } returns successResult.commit
|
||||
every { buildExecutor.startBuild("fresh", successResult.commit) } returns
|
||||
every { buildExecutor.startBuild(repo, "fresh", successResult.commit) } returns
|
||||
runningBuild(liveLogFile).copy(branch = "fresh", name = "fresh")
|
||||
|
||||
mockMvc
|
||||
@@ -250,7 +255,7 @@ class BuildsApiControllerTest : FunSpec() {
|
||||
.andExpect(status().isAccepted)
|
||||
.andExpect(jsonPath("$.status").value("pending"))
|
||||
|
||||
verify { buildExecutor.startBuild("fresh", successResult.commit) }
|
||||
verify { buildExecutor.startBuild(repo, "fresh", successResult.commit) }
|
||||
}
|
||||
|
||||
test("restart of a branch without recorded builds and without origin counterpart answers 404") {
|
||||
@@ -290,7 +295,7 @@ class BuildsApiControllerTest : FunSpec() {
|
||||
.header(BuildsApiController.TOKEN_HEADER, "wrong"),
|
||||
).andExpect(status().isForbidden)
|
||||
|
||||
verify(exactly = 0) { buildExecutor.startBuild(any(), any(), any()) }
|
||||
verify(exactly = 0) { buildExecutor.startBuild(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
test("cancel answers 202 for a cancellable build and 404 otherwise") {
|
||||
@@ -317,7 +322,7 @@ class BuildsApiControllerTest : FunSpec() {
|
||||
.perform(delete("/api/builds/some-key").param("token", "secret"))
|
||||
.andExpect(status().isForbidden)
|
||||
|
||||
verify(exactly = 0) { buildExecutor.startBuild(any(), any(), any()) }
|
||||
verify(exactly = 0) { buildExecutor.startBuild(any(), any(), any(), any()) }
|
||||
verify(exactly = 0) { buildExecutor.cancel(any()) }
|
||||
verify(exactly = 0) { repository.delete(any()) }
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import de.hoennig.werkator.config.ConfigLoader
|
||||
import de.hoennig.werkator.config.WerkatorConfig
|
||||
import de.hoennig.werkator.git.GitService
|
||||
import de.hoennig.werkator.metrics.SystemMetricsCollector
|
||||
import de.hoennig.werkator.repo.RepoContext
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.every
|
||||
@@ -22,6 +23,7 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@@ -65,6 +67,9 @@ class PermanentBranchRoutesTest : FunSpec() {
|
||||
@MockkBean
|
||||
lateinit var branchPermalinks: BranchPermalinks
|
||||
|
||||
@MockkBean
|
||||
lateinit var repo: RepoContext
|
||||
|
||||
private val artifactDir: Path = Files.createTempDirectory("werkator-permanent-routes-test")
|
||||
|
||||
private val greenBuild =
|
||||
@@ -89,7 +94,9 @@ class PermanentBranchRoutesTest : FunSpec() {
|
||||
metricsCollector,
|
||||
branchListing,
|
||||
branchPermalinks,
|
||||
repo,
|
||||
)
|
||||
every { repo.workingDir } returns Paths.get(".")
|
||||
every { configLoader.load(any()) } returns WerkatorConfig()
|
||||
every { configLoader.loadWithBranchLayer(any(), anyNullable()) } returns WerkatorConfig()
|
||||
every { gitService.showFileAtCommit(any(), any(), any()) } returns null
|
||||
|
||||
@@ -17,6 +17,7 @@ import de.hoennig.werkator.git.GitService
|
||||
import de.hoennig.werkator.metrics.MetricAggregate
|
||||
import de.hoennig.werkator.metrics.SystemMetrics
|
||||
import de.hoennig.werkator.metrics.SystemMetricsCollector
|
||||
import de.hoennig.werkator.repo.RepoContext
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.string.shouldContain
|
||||
@@ -36,6 +37,7 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@@ -73,6 +75,9 @@ class UiControllerTest : FunSpec() {
|
||||
@MockkBean
|
||||
lateinit var branchPermalinks: BranchPermalinks
|
||||
|
||||
@MockkBean
|
||||
lateinit var repo: RepoContext
|
||||
|
||||
private val startedAt = Instant.parse("2026-07-07T10:00:00Z")
|
||||
|
||||
private val emptySystemMetrics =
|
||||
@@ -113,7 +118,9 @@ class UiControllerTest : FunSpec() {
|
||||
metricsCollector,
|
||||
branchListing,
|
||||
branchPermalinks,
|
||||
repo,
|
||||
)
|
||||
every { repo.workingDir } returns Paths.get(".")
|
||||
every { configLoader.load(any()) } returns
|
||||
WerkatorConfig(
|
||||
server = ServerConfig(impressumUrl = "https://example.org/imprint"),
|
||||
|
||||
@@ -21,6 +21,7 @@ import de.hoennig.werkator.config.TriggerConfig
|
||||
import de.hoennig.werkator.config.WatcherConfig
|
||||
import de.hoennig.werkator.config.WerkatorConfig
|
||||
import de.hoennig.werkator.git.GitService
|
||||
import de.hoennig.werkator.repo.RepoContext
|
||||
import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.booleans.shouldBeFalse
|
||||
@@ -62,12 +63,11 @@ class WatcherTest : FunSpec() {
|
||||
val artifactStore = mockk<ArtifactStore>()
|
||||
val startedBuilds = CopyOnWriteArrayList<Pair<String, String>>()
|
||||
val configLoader = mockk<ConfigLoader>()
|
||||
val repo = RepoContext("test", workingDir, repository, artifactStore)
|
||||
val watcher =
|
||||
Watcher(
|
||||
gitService = gitService,
|
||||
buildExecutor = buildExecutor,
|
||||
repository = repository,
|
||||
artifactStore = artifactStore,
|
||||
configLoader = configLoader,
|
||||
clock = Clock.fixed(noon, ZoneOffset.UTC),
|
||||
)
|
||||
@@ -91,8 +91,8 @@ class WatcherTest : FunSpec() {
|
||||
every { gitService.fastForwardLocalBranches(any()) } returns emptyList()
|
||||
every { buildExecutor.currentBuilds() } returns emptyList()
|
||||
every { buildExecutor.startBuild(any(), any(), any(), any()) } answers {
|
||||
val branch = firstArg<String>()
|
||||
val commit = secondArg<String>()
|
||||
val branch = secondArg<String>()
|
||||
val commit = thirdArg<String>()
|
||||
startedBuilds += branch to commit
|
||||
runningBuild(branch, commit)
|
||||
}
|
||||
@@ -159,7 +159,7 @@ class WatcherTest : FunSpec() {
|
||||
val harness = Harness()
|
||||
every { harness.gitService.fetchOrigin(any()) } throws RuntimeException("origin unreachable")
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
harness.watcher
|
||||
.state()
|
||||
@@ -170,7 +170,7 @@ class WatcherTest : FunSpec() {
|
||||
verify(exactly = 0) { harness.artifactStore.prune(any()) }
|
||||
|
||||
every { harness.gitService.fetchOrigin(any()) } returns Unit
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
harness.watcher
|
||||
.state()
|
||||
@@ -183,19 +183,19 @@ class WatcherTest : FunSpec() {
|
||||
val logged = captureWatcherLog()
|
||||
every { harness.gitService.fetchOrigin(any()) } throws RuntimeException("origin unreachable")
|
||||
|
||||
repeat(5) { harness.watcher.poll(harness.workingDir) }
|
||||
repeat(5) { harness.watcher.poll(harness.repo) }
|
||||
|
||||
// one wrong token used to write a warning every ten seconds, 297 of them in an hour
|
||||
logged().filter { it.contains("fetching origin failed") } shouldHaveSize 1
|
||||
|
||||
every { harness.gitService.fetchOrigin(any()) } returns Unit
|
||||
repeat(3) { harness.watcher.poll(harness.workingDir) }
|
||||
repeat(3) { harness.watcher.poll(harness.repo) }
|
||||
|
||||
logged().filter { it.contains("fetching origin succeeded again") } shouldHaveSize 1
|
||||
|
||||
// a different failure is a different message and is worth saying again
|
||||
every { harness.gitService.fetchOrigin(any()) } throws RuntimeException("host is down")
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
logged().filter { it.contains("fetching origin failed") } shouldHaveSize 2
|
||||
}
|
||||
@@ -209,7 +209,7 @@ class WatcherTest : FunSpec() {
|
||||
every { harness.gitService.originHeadCommit("main", any()) } returns "commit-main"
|
||||
every { harness.gitService.originHeadCommit("feature/new", any()) } returns "commit-feature"
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
harness.startedBuilds shouldContainExactly
|
||||
listOf("main" to "commit-main", "feature/new" to "commit-feature")
|
||||
@@ -223,7 +223,7 @@ class WatcherTest : FunSpec() {
|
||||
every { harness.gitService.originHeadCommit("main", any()) } returns "commit-main"
|
||||
every { harness.gitService.fastForwardLocalBranches(any()) } returns listOf("main")
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
// syncing the ref before the decision would hide the very commit being enqueued here
|
||||
harness.startedBuilds shouldContainExactly listOf("main" to "commit-main")
|
||||
@@ -238,7 +238,7 @@ class WatcherTest : FunSpec() {
|
||||
every { harness.gitService.originBranches(any()) } returns listOf("main")
|
||||
every { harness.gitService.fastForwardLocalBranches(any()) } throws RuntimeException("ref locked")
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
harness.watcher
|
||||
.state()
|
||||
@@ -251,7 +251,7 @@ class WatcherTest : FunSpec() {
|
||||
val harness = Harness(WerkatorConfig(watcher = WatcherConfig(fastForwardLocalRefs = false)))
|
||||
every { harness.gitService.originBranches(any()) } returns listOf("main")
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
verify(exactly = 0) { harness.gitService.fastForwardLocalBranches(any()) }
|
||||
}
|
||||
@@ -264,7 +264,7 @@ class WatcherTest : FunSpec() {
|
||||
every { harness.gitService.hasNewCommits("main", any()) } returns true
|
||||
every { harness.gitService.originHeadCommit("main", any()) } returns "commit-new"
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
harness.startedBuilds.shouldBeEmpty()
|
||||
}
|
||||
@@ -280,7 +280,7 @@ class WatcherTest : FunSpec() {
|
||||
every { harness.gitService.originHeadCommit("main", any()) } returns "commit-2"
|
||||
every { harness.gitService.originHeadCommit("feature/other", any()) } returns "commit-3"
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
harness.startedBuilds shouldContainExactly listOf("feature/other" to "commit-3")
|
||||
harness.watcher.state().queuedBranches shouldContainExactly listOf("main")
|
||||
@@ -294,11 +294,11 @@ class WatcherTest : FunSpec() {
|
||||
every { harness.gitService.hasNewCommits("main", any()) } returns true
|
||||
every { harness.gitService.originHeadCommit("main", any()) } returns "commit-abc"
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
harness.startedBuilds.shouldBeEmpty()
|
||||
|
||||
every { harness.gitService.originHeadCommit("main", any()) } returns "commit-def"
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
harness.startedBuilds shouldContainExactly listOf("main" to "commit-def")
|
||||
}
|
||||
@@ -306,7 +306,7 @@ class WatcherTest : FunSpec() {
|
||||
test("poll filters new origin branches by the configured newBranchMaxAge") {
|
||||
val harness = Harness(WerkatorConfig(watcher = WatcherConfig(newBranchMaxAge = "12h")))
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
verify { harness.gitService.newOriginBranches(Duration.ofHours(12), any()) }
|
||||
}
|
||||
@@ -319,7 +319,7 @@ class WatcherTest : FunSpec() {
|
||||
every { harness.gitService.originHeadCommit("feature/no-pr", any()) } returns "commit-solo"
|
||||
every { harness.gitService.pullRequestHeads(any()) } returns setOf("commit-pr")
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
harness.startedBuilds shouldContainExactly listOf("feature/pr" to "commit-pr")
|
||||
}
|
||||
@@ -330,7 +330,7 @@ class WatcherTest : FunSpec() {
|
||||
every { harness.gitService.newOriginBranches(any(), any()) } returns listOf("feature/x")
|
||||
every { harness.gitService.originHeadCommit("feature/x", any()) } returns "commit-x"
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
harness.startedBuilds shouldContainExactly listOf("feature/x" to "commit-x")
|
||||
verify(exactly = 0) { harness.gitService.pullRequestHeads(any()) }
|
||||
@@ -348,7 +348,7 @@ class WatcherTest : FunSpec() {
|
||||
every { harness.gitService.newOriginBranches(any(), any()) } returns listOf("feature/no-pr")
|
||||
every { harness.gitService.originHeadCommit("feature/no-pr", any()) } returns "commit-solo"
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
harness.startedBuilds shouldContainExactly listOf("feature/no-pr" to "commit-solo")
|
||||
verify(exactly = 0) { harness.gitService.pullRequestHeads(any()) }
|
||||
@@ -370,7 +370,7 @@ class WatcherTest : FunSpec() {
|
||||
every { harness.gitService.hasNewCommits("main", any()) } returns true
|
||||
every { harness.gitService.originHeadCommit("main", any()) } returns "commit-main"
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
harness.startedBuilds shouldContainExactly listOf("main" to "commit-main")
|
||||
}
|
||||
@@ -394,7 +394,7 @@ class WatcherTest : FunSpec() {
|
||||
every { harness.gitService.originBranches(any()) } returns listOf("main")
|
||||
every { harness.gitService.originHeadCommit("main", any()) } returns "commit-abc"
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
harness.startedBuilds.shouldBeEmpty()
|
||||
harness.autoBuildState().isTriggered("main", LocalDate.parse("2026-07-07"), "11:00").shouldBeFalse()
|
||||
@@ -406,12 +406,12 @@ class WatcherTest : FunSpec() {
|
||||
every { harness.gitService.originBranches(any()) } returns listOf("main")
|
||||
every { harness.gitService.originHeadCommit("main", any()) } returns "commit-abc"
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
harness.startedBuilds shouldContainExactly listOf("main" to "commit-abc")
|
||||
// the deprecated branch schedule rebuilds the branch's own pool with the default build
|
||||
verify { harness.buildExecutor.startBuild("main", "commit-abc", any(), BuildDefinition.DEFAULT) }
|
||||
verify { harness.buildExecutor.startBuild(harness.repo, "main", "commit-abc", BuildDefinition.DEFAULT) }
|
||||
harness.autoBuildState().isTriggered("main", LocalDate.parse("2026-07-07"), "11:00").shouldBeTrue()
|
||||
}
|
||||
|
||||
@@ -434,13 +434,13 @@ class WatcherTest : FunSpec() {
|
||||
every { harness.gitService.originHeadCommit("main", any()) } returns "commit-abc"
|
||||
every { harness.gitService.originHeadCommit("release/1.x", any()) } returns "commit-rel"
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
// glob selector: main and release/1.x fire once, feature/x is not selected
|
||||
harness.startedBuilds shouldContainExactlyInAnyOrder
|
||||
listOf("main" to "commit-abc", "release/1.x" to "commit-rel")
|
||||
verify { harness.buildExecutor.startBuild("main", "commit-abc", any(), "pitest") }
|
||||
verify { harness.buildExecutor.startBuild(harness.repo, "main", "commit-abc", "pitest") }
|
||||
harness.autoBuildState().isTriggered("main@pitest", LocalDate.parse("2026-07-07"), "11:00").shouldBeTrue()
|
||||
}
|
||||
|
||||
@@ -460,10 +460,10 @@ class WatcherTest : FunSpec() {
|
||||
"dormant" to noon.minus(Duration.ofDays(10)),
|
||||
)
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
harness.startedBuilds shouldContainExactly listOf("active" to "commit-act")
|
||||
verify { harness.buildExecutor.startBuild("active", "commit-act", any(), "pitest") }
|
||||
verify { harness.buildExecutor.startBuild(harness.repo, "active", "commit-act", "pitest") }
|
||||
}
|
||||
|
||||
test("an onPush build definition builds the changed branches it selects") {
|
||||
@@ -480,13 +480,13 @@ class WatcherTest : FunSpec() {
|
||||
every { harness.gitService.originHeadCommit("main", any()) } returns "commit-main"
|
||||
every { harness.gitService.originHeadCommit("feature/x", any()) } returns "commit-feat"
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
// the implicit default build covers both branches; lint only selects main
|
||||
verify { harness.buildExecutor.startBuild("main", "commit-main", any(), BuildDefinition.DEFAULT) }
|
||||
verify { harness.buildExecutor.startBuild("feature/x", "commit-feat", any(), BuildDefinition.DEFAULT) }
|
||||
verify { harness.buildExecutor.startBuild("main", "commit-main", any(), "lint") }
|
||||
verify(exactly = 0) { harness.buildExecutor.startBuild("feature/x", "commit-feat", any(), "lint") }
|
||||
verify { harness.buildExecutor.startBuild(harness.repo, "main", "commit-main", BuildDefinition.DEFAULT) }
|
||||
verify { harness.buildExecutor.startBuild(harness.repo, "feature/x", "commit-feat", BuildDefinition.DEFAULT) }
|
||||
verify { harness.buildExecutor.startBuild(harness.repo, "main", "commit-main", "lint") }
|
||||
verify(exactly = 0) { harness.buildExecutor.startBuild(harness.repo, "feature/x", "commit-feat", "lint") }
|
||||
}
|
||||
|
||||
test("builds.default with onPush false disables the implicit on-push build") {
|
||||
@@ -497,7 +497,7 @@ class WatcherTest : FunSpec() {
|
||||
every { harness.gitService.hasNewCommits("main", any()) } returns true
|
||||
every { harness.gitService.originHeadCommit("main", any()) } returns "commit-main"
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
harness.startedBuilds.shouldBeEmpty()
|
||||
}
|
||||
@@ -509,9 +509,9 @@ class WatcherTest : FunSpec() {
|
||||
every { harness.gitService.hasNewCommits("main", any()) } returns true
|
||||
every { harness.gitService.originHeadCommit("main", any()) } returns "commit-main"
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
verify { harness.buildExecutor.startBuild("main", "commit-main", any(), BuildDefinition.DEFAULT) }
|
||||
verify { harness.buildExecutor.startBuild(harness.repo, "main", "commit-main", BuildDefinition.DEFAULT) }
|
||||
}
|
||||
|
||||
test("a build definition committed on a branch fires for that branch, without any entry in the primary config") {
|
||||
@@ -528,10 +528,10 @@ class WatcherTest : FunSpec() {
|
||||
every { harness.gitService.originHeadCommit("experiment", any()) } returns "commit-exp"
|
||||
every { harness.gitService.originHeadCommit("main", any()) } returns "commit-main"
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
harness.startedBuilds shouldContainExactly listOf("experiment" to "commit-exp")
|
||||
verify { harness.buildExecutor.startBuild("experiment", "commit-exp", any(), "pitest") }
|
||||
verify { harness.buildExecutor.startBuild(harness.repo, "experiment", "commit-exp", "pitest") }
|
||||
harness
|
||||
.autoBuildState()
|
||||
.isTriggered("experiment@pitest", LocalDate.parse("2026-07-07"), "11:00")
|
||||
@@ -550,7 +550,7 @@ class WatcherTest : FunSpec() {
|
||||
every { harness.configLoader.loadWithBranchLayer(any(), "branch-yaml") } returns branchLayer
|
||||
every { harness.gitService.originHeadCommit("experiment", any()) } returns "commit-exp"
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
harness.startedBuilds shouldContainExactly listOf("experiment" to "commit-exp")
|
||||
}
|
||||
@@ -569,7 +569,7 @@ class WatcherTest : FunSpec() {
|
||||
every { harness.configLoader.loadWithBranchLayer(any(), "branch-yaml") } returns branchLayer
|
||||
every { harness.gitService.originHeadCommit(any(), any()) } returns "commit-any"
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
// the definition selects main, but it is only known on experiment — so nothing is built
|
||||
harness.startedBuilds.shouldBeEmpty()
|
||||
@@ -580,12 +580,12 @@ class WatcherTest : FunSpec() {
|
||||
every { harness.gitService.originBranches(any()) } returns listOf("main")
|
||||
every { harness.gitService.originBranchHeads(any()) } returns mapOf("main" to "commit-1")
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
harness.watcher.poll(harness.repo)
|
||||
verify(exactly = 1) { harness.gitService.showFileAtCommit("commit-1", Watcher.CONFIG_FILE, any()) }
|
||||
|
||||
every { harness.gitService.originBranchHeads(any()) } returns mapOf("main" to "commit-2")
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
verify(exactly = 1) { harness.gitService.showFileAtCommit("commit-2", Watcher.CONFIG_FILE, any()) }
|
||||
}
|
||||
@@ -596,7 +596,7 @@ class WatcherTest : FunSpec() {
|
||||
every { harness.gitService.originBranchHeads(any()) } returns mapOf("main" to "commit-1")
|
||||
every { harness.gitService.originHeadCommit("main", any()) } returns "commit-1"
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
harness.startedBuilds.shouldBeEmpty()
|
||||
|
||||
// the machine config gains a scheduled build while the branch stays where it is:
|
||||
@@ -608,9 +608,9 @@ class WatcherTest : FunSpec() {
|
||||
every { harness.configLoader.load(any()) } returns edited
|
||||
every { harness.configLoader.loadWithBranchLayer(any(), anyNullable()) } returns edited
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
verify { harness.buildExecutor.startBuild("main", "commit-1", any(), "nightly") }
|
||||
verify { harness.buildExecutor.startBuild(harness.repo, "main", "commit-1", "nightly") }
|
||||
}
|
||||
|
||||
test("an unreadable branch config falls back to the primary definitions instead of failing the poll") {
|
||||
@@ -624,13 +624,13 @@ class WatcherTest : FunSpec() {
|
||||
RuntimeException("mapping problem")
|
||||
every { harness.gitService.originHeadCommit("main", any()) } returns "commit-main"
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
harness.watcher
|
||||
.state()
|
||||
.lastPollError
|
||||
.shouldBeNull()
|
||||
verify { harness.buildExecutor.startBuild("main", "commit-main", any(), BuildDefinition.DEFAULT) }
|
||||
verify { harness.buildExecutor.startBuild(harness.repo, "main", "commit-main", BuildDefinition.DEFAULT) }
|
||||
}
|
||||
|
||||
test("an auto-build slot stays untriggered while the branch is still building") {
|
||||
@@ -639,7 +639,7 @@ class WatcherTest : FunSpec() {
|
||||
every { harness.gitService.originBranches(any()) } returns listOf("main")
|
||||
every { harness.gitService.originHeadCommit("main", any()) } returns "commit-abc"
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
harness.startedBuilds.shouldBeEmpty()
|
||||
harness.autoBuildState().isTriggered("main", LocalDate.parse("2026-07-07"), "11:00").shouldBeFalse()
|
||||
@@ -655,7 +655,7 @@ class WatcherTest : FunSpec() {
|
||||
every { harness.gitService.originHeadCommit("feature/a", any()) } returns "commit-2"
|
||||
every { harness.gitService.originHeadCommit("queued", any()) } returns "commit-3"
|
||||
|
||||
harness.watcher.recoverOnStartup(harness.workingDir)
|
||||
harness.watcher.recoverOnStartup(harness.repo)
|
||||
|
||||
harness.startedBuilds shouldContainExactlyInAnyOrder
|
||||
listOf("main" to "commit-1", "feature/a" to "commit-2", "queued" to "commit-3")
|
||||
@@ -671,7 +671,7 @@ class WatcherTest : FunSpec() {
|
||||
harness.seed("main", BuildStatus.INTERRUPTED, commit = "commit-2")
|
||||
every { harness.gitService.originHeadCommit("main", any()) } returns "commit-2"
|
||||
|
||||
harness.watcher.recoverOnStartup(harness.workingDir)
|
||||
harness.watcher.recoverOnStartup(harness.repo)
|
||||
|
||||
harness.startedBuilds shouldContainExactly listOf("main" to "commit-2")
|
||||
}
|
||||
@@ -681,17 +681,17 @@ class WatcherTest : FunSpec() {
|
||||
harness.seed("main", BuildStatus.INTERRUPTED, commit = "commit-1", build = "pitest")
|
||||
every { harness.gitService.originHeadCommit("main", any()) } returns "commit-1"
|
||||
|
||||
harness.watcher.recoverOnStartup(harness.workingDir)
|
||||
harness.watcher.recoverOnStartup(harness.repo)
|
||||
|
||||
// otherwise a restart mid-nightly-build would repeat it as a regular build in the wrong pool
|
||||
verify { harness.buildExecutor.startBuild("main", "commit-1", any(), "pitest") }
|
||||
verify { harness.buildExecutor.startBuild(harness.repo, "main", "commit-1", "pitest") }
|
||||
}
|
||||
|
||||
test("startup recovery closes out an orphaned PENDING build of a branch gone from origin") {
|
||||
val harness = Harness()
|
||||
val orphan = harness.seed("gone", BuildStatus.PENDING, commit = "commit-1")
|
||||
|
||||
harness.watcher.recoverOnStartup(harness.workingDir)
|
||||
harness.watcher.recoverOnStartup(harness.repo)
|
||||
|
||||
// PENDING is prune-immune; left as-is, the gone branch could never be pruned
|
||||
harness.startedBuilds.shouldBeEmpty()
|
||||
@@ -709,7 +709,7 @@ class WatcherTest : FunSpec() {
|
||||
val removedWorktree = harness.worktreeDir("gone")
|
||||
every { harness.gitService.originBranches(any()) } returns listOf("main")
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
harness.repository.history().map { it.branch } shouldContainExactly listOf("main")
|
||||
verify {
|
||||
@@ -728,7 +728,7 @@ class WatcherTest : FunSpec() {
|
||||
keeping.seed("main", BuildStatus.FAILED, commit = "commit-2")
|
||||
every { keeping.gitService.originBranches(any()) } returns listOf("main")
|
||||
|
||||
keeping.watcher.poll(keeping.workingDir)
|
||||
keeping.watcher.poll(keeping.repo)
|
||||
|
||||
keeping.repository.history().map { it.status } shouldContainExactly
|
||||
listOf(BuildStatus.FAILED, BuildStatus.SUCCESS)
|
||||
@@ -739,7 +739,7 @@ class WatcherTest : FunSpec() {
|
||||
dropping.seed("main", BuildStatus.FAILED, commit = "commit-2")
|
||||
every { dropping.gitService.originBranches(any()) } returns listOf("main")
|
||||
|
||||
dropping.watcher.poll(dropping.workingDir)
|
||||
dropping.watcher.poll(dropping.repo)
|
||||
|
||||
dropping.repository.history().map { it.status } shouldContainExactly listOf(BuildStatus.FAILED)
|
||||
}
|
||||
@@ -751,7 +751,7 @@ class WatcherTest : FunSpec() {
|
||||
harness.seed("main", BuildStatus.FAILED, commit = "commit-2")
|
||||
every { harness.gitService.originBranches(any()) } returns listOf("main")
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
harness.repository.history().map { it.commit } shouldContainExactly listOf("commit-2")
|
||||
}
|
||||
@@ -762,7 +762,7 @@ class WatcherTest : FunSpec() {
|
||||
val busyWorktree = harness.worktreeDir("busy")
|
||||
every { harness.gitService.originBranches(any()) } returns listOf("busy")
|
||||
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
harness.watcher.poll(harness.repo)
|
||||
|
||||
Files.exists(busyWorktree).shouldBeTrue()
|
||||
}
|
||||
@@ -772,14 +772,14 @@ class WatcherTest : FunSpec() {
|
||||
val fetches = CountDownLatch(2)
|
||||
every { harness.gitService.fetchOrigin(any()) } answers { fetches.countDown() }
|
||||
|
||||
harness.watcher.start(harness.workingDir)
|
||||
harness.watcher.start(harness.repo)
|
||||
|
||||
fetches.await(5, TimeUnit.SECONDS).shouldBeTrue()
|
||||
harness.watcher
|
||||
.state()
|
||||
.running
|
||||
.shouldBeTrue()
|
||||
shouldThrow<IllegalStateException> { harness.watcher.start(harness.workingDir) }
|
||||
shouldThrow<IllegalStateException> { harness.watcher.start(harness.repo) }
|
||||
|
||||
harness.watcher.stop()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user