Files
werkator/docs/prs/2026-10-04-PR#112-build-definition-applied-in-blocks.md
miandmhoennig 52f42f850e
werkator build succeeded after 01:52
werkator/deploy build succeeded after 00:07
Build definition laid over its branch in three blocks: commands and logs, rules, sandbox (v1.34.9) (#112)
## The Problem

After PR#111 `BuildDefinition.applyTo` had the highest Mutation-CRAP score of Werkator: 34 at a complexity of 34.
A score is never below the complexity, so no test could bring it under the limit of 15.

The method laid a build definition over its branch's settings in one `copy` of 21 fields, each with its own `?:` fallback, the docker and werkdock blocks nested inside.
What the definition says falls into three groups, but the method did not show them:
what the build runs, the rules the instance runs it under, and the sandbox it runs in.
Only one test checked a single field of it (Scenario#72.01); every other field was held only by tests of the config loader.

## Non-Goals

- No change in behaviour: which settings a definition can say, how they inherit, and which are pinned stay as they are.

## The Scenarios

### Feature: A build definition laid over its branch's settings

#### Scenario#112.01: A definition that says nothing runs the branch's settings unchanged

So that `builds.<name>` with only a trigger builds exactly what the branch describes.

- **Given** a branch with a non-default value for every setting
- **When** an empty definition, or one with empty docker and werkdock blocks, is applied
- **Then** the result equals the branch's settings

##### Verified by

- [BuildDefinitionUnitTest](../../src/test/kotlin/de/hoennig/werkator/config/BuildDefinitionUnitTest.kt) — Scenario#112.01

#### Scenario#112.02: Every setting a definition says replaces the branch's own

So that no key of `builds.<name>` is read and then silently dropped.

- **Given** the same branch, and a definition saying every setting, docker and werkdock included, with other values
- **When** the definition is applied
- **Then** every setting is the definition's

##### Verified by

- [BuildDefinitionUnitTest](../../src/test/kotlin/de/hoennig/werkator/config/BuildDefinitionUnitTest.kt) — Scenario#112.02

#### Scenario#112.03: A definition saying some settings of a block keeps the rest of that block

So that `docker.image` alone does not reset the branch's network, socket or environment.

- **Given** the same branch, and a definition saying only the timeout, the docker image and the werkdock rootfs
- **When** the definition is applied
- **Then** those three are the definition's, everything else is the branch's

##### Verified by

- [BuildDefinitionUnitTest](../../src/test/kotlin/de/hoennig/werkator/config/BuildDefinitionUnitTest.kt) — Scenario#112.03

## The Solution

`applyTo` lays the definition over the branch in three blocks:

| Block         | Settings                                                                 |
| ------------- | ------------------------------------------------------------------------ |
| `withWork`    | `buildCommand`, `cleanCommand`, `artifactDirs`, `stdoutLog`, `stderrLog` |
| `withRules`   | `requirePullRequest`, `statusContext`, `timeout`, `restartsInstance`     |
| `withSandbox` | `docker`, `werkdock`                                                     |

`DockerOverrides.applyTo` and `WerkdockOverrides.applyTo` lay each sandbox's overrides over the branch's sandbox of that kind.
The overrides and their config class now stand side by side, where Scenario#72.03 already holds that every setting of the one exists in the other.

Measured with PIT as the nightly build runs it:

| Method                        | Before | After      |
| ----------------------------- | ------ | ---------- |
| `BuildDefinition.applyTo`     | 34     | no mutants |
| `BuildDefinition.withWork`    | —      | 6          |
| `BuildDefinition.withRules`   | —      | 5          |
| `BuildDefinition.withSandbox` | —      | 5          |
| `DockerOverrides.applyTo`     | —      | 8          |
| `WerkdockOverrides.applyTo`   | —      | 6          |

Every mutant of the new methods is killed, none is `INCONCLUSIVE`; each score equals its complexity.

The gate `maxMutationCrap` falls from 35 to 34: the highest score left is `CapacitySizing.cpu` at 33.17.

## Additional Changes

- Version 1.34.9 with its release note.
- `BuildExecutorUnitTest`, Scenario#108.05, waits for the executing build's process before it cancels the build: a build counts as running before its process is started, and a cancel in between left no process to see terminated (`List is empty`, seen once in this PR's local build).

## Follow-up PRs

- `CapacitySizing.cpu` (33.17) in its own PR.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: mhoennig <michael@hoennig.de>
Reviewed-on: #112
2026-10-04 18:51:22 +02:00

95 lines
4.6 KiB
Markdown

> **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
After PR#111 `BuildDefinition.applyTo` had the highest Mutation-CRAP score of Werkator: 34 at a complexity of 34.
A score is never below the complexity, so no test could bring it under the limit of 15.
The method laid a build definition over its branch's settings in one `copy` of 21 fields, each with its own `?:` fallback, the docker and werkdock blocks nested inside.
What the definition says falls into three groups, but the method did not show them:
what the build runs, the rules the instance runs it under, and the sandbox it runs in.
Only one test checked a single field of it (Scenario#72.01); every other field was held only by tests of the config loader.
## Non-Goals
- No change in behaviour: which settings a definition can say, how they inherit, and which are pinned stay as they are.
## The Scenarios
### Feature: A build definition laid over its branch's settings
#### Scenario#112.01: A definition that says nothing runs the branch's settings unchanged
So that `builds.<name>` with only a trigger builds exactly what the branch describes.
- **Given** a branch with a non-default value for every setting
- **When** an empty definition, or one with empty docker and werkdock blocks, is applied
- **Then** the result equals the branch's settings
##### Verified by
- [BuildDefinitionUnitTest](../../src/test/kotlin/de/hoennig/werkator/config/BuildDefinitionUnitTest.kt) — Scenario#112.01
#### Scenario#112.02: Every setting a definition says replaces the branch's own
So that no key of `builds.<name>` is read and then silently dropped.
- **Given** the same branch, and a definition saying every setting, docker and werkdock included, with other values
- **When** the definition is applied
- **Then** every setting is the definition's
##### Verified by
- [BuildDefinitionUnitTest](../../src/test/kotlin/de/hoennig/werkator/config/BuildDefinitionUnitTest.kt) — Scenario#112.02
#### Scenario#112.03: A definition saying some settings of a block keeps the rest of that block
So that `docker.image` alone does not reset the branch's network, socket or environment.
- **Given** the same branch, and a definition saying only the timeout, the docker image and the werkdock rootfs
- **When** the definition is applied
- **Then** those three are the definition's, everything else is the branch's
##### Verified by
- [BuildDefinitionUnitTest](../../src/test/kotlin/de/hoennig/werkator/config/BuildDefinitionUnitTest.kt) — Scenario#112.03
## The Solution
`applyTo` lays the definition over the branch in three blocks:
| Block | Settings |
| ------------- | ------------------------------------------------------------------------ |
| `withWork` | `buildCommand`, `cleanCommand`, `artifactDirs`, `stdoutLog`, `stderrLog` |
| `withRules` | `requirePullRequest`, `statusContext`, `timeout`, `restartsInstance` |
| `withSandbox` | `docker`, `werkdock` |
`DockerOverrides.applyTo` and `WerkdockOverrides.applyTo` lay each sandbox's overrides over the branch's sandbox of that kind.
The overrides and their config class now stand side by side, where Scenario#72.03 already holds that every setting of the one exists in the other.
Measured with PIT as the nightly build runs it:
| Method | Before | After |
| ----------------------------- | ------ | ---------- |
| `BuildDefinition.applyTo` | 34 | no mutants |
| `BuildDefinition.withWork` | — | 6 |
| `BuildDefinition.withRules` | — | 5 |
| `BuildDefinition.withSandbox` | — | 5 |
| `DockerOverrides.applyTo` | — | 8 |
| `WerkdockOverrides.applyTo` | — | 6 |
Every mutant of the new methods is killed, none is `INCONCLUSIVE`; each score equals its complexity.
The gate `maxMutationCrap` falls from 35 to 34: the highest score left is `CapacitySizing.cpu` at 33.17.
## Additional Changes
- Version 1.34.9 with its release note.
- `BuildExecutorUnitTest`, Scenario#108.05, waits for the executing build's process before it cancels the build: a build counts as running before its process is started, and a cancel in between left no process to see terminated (`List is empty`, seen once in this PR's local build).
## Follow-up PRs
- `CapacitySizing.cpu` (33.17) in its own PR.