Files
werkator/docs/prs/2026-10-10-PR#138-executor-tests-without-waiting.md
claude-code 20b7a002a0
werkator build succeeded after 02:33
werkator/deploy build succeeded after 00:08
Executor tests without waiting (v1.38.5) (#138)
Co-authored-by: claude-code <claude-code@javagil.de>
2026-10-10 10:14:47 +02:00

3.3 KiB

PR#138: Executor Tests without Waiting

WARNING: This document describes only the change applied in this PR. It may already be outdated once the next PR is merged. Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.

The Problem

BuildExecutor costs more time in the nightly mutation build than any other class. An estimate from the nightly of 2026-10-10 puts about a third of all minion time on it: BuildExecutorUnitTest alone about 14 of 81 minutes, and 35 of the 63 timed-out mutants are in BuildExecutor.

BuildExecutorUnitTest starts no process, but it waits:

  • The timeout test waits out a real build timeout of one second.
  • Two tests sleep 300 ms to see that a build still waits for its slot.
  • awaitStatus polls for up to five seconds until a build has the expected status, also when the build already ended in another one.

A normal run of the spec takes about 3.7 s, more than half of it in these waits. PIT runs the whole spec again for each of the 233 mutants it covers. And a mutant that makes the builds end in the wrong status lets every test poll for five seconds, so the spec runs past PIT's timeout: the mutant ends TIMED_OUT after more than 20 s, and PIT starts a new minion.

Non-Goals

  • No change to how the executor runs builds: threads, slots and the drain stay as they are.
  • The drain test keeps its pause of 500 ms: the drain polls every 200 ms, and only a pause shows that it does not restart the instance early.
  • The tests of BuildSlots, the integration tests and UiControllerRestTest follow in their own PRs.

The Scenarios

The behaviour of the executor does not change, so its scenarios stay as they are. This PR changes only how three of them are verified:

  • Scenario#108.08 (timeout): the fake process answers a wait with a timeout at once that the time is up, which is what a real process does once the timeout has passed.
  • Scenario#119.02 and Scenario#119.04 (waiting reviews and CI builds): the test sees the build wait at its slot, through the number of builds waiting for a slot, instead of sleeping.

The Solution

  • BuildSlots.waiting() counts the builds waiting at a semaphore of any pool, and BuildExecutor.waitingForSlots() passes it on; both are internal.
  • awaitStatus stops waiting as soon as the build has the expected status or any final one, and then compares.
  • awaitWaitingForSlots waits until the given builds wait for a slot, and fails at once when one of them was let in.
  • FakeProcess(outlivesDeadline = true) lets the timeout test run without waiting for its timeout.

A normal run of the spec now takes 2.1 s instead of 3.7 s. A local PIT run of BuildExecutor alone, two minions, without history:

Run Duration Mutants timed out
Before 27.5 min 36
With the PR 23.7 min 35

The timeouts stay because PIT runs the fastest covering spec first. For BuildExecutor these are PullRequestTargetBuildIntegrationTest and BuildExecutorArtifactIntegrationTest, which wait up to 30 s for a build that a mutant never ends.

Follow-up PRs

  • Leave the two integration specs out of the mutation build, once unit tests cover what only they kill.