Third Mutation-CRAP refactoring: the environment probes. - `DockerSocketLocator`: seams for env, path existence, socket group; `candidate` holds the rule order (56 -> 5.1). - `InitCommand.jarPathOf` (runningJarPath 56 -> 6), `HostingEnvironments.hostnameOf` (readHostname 42 -> 2). - `StateHome.of`: the one XDG_STATE_HOME rule, used by metrics, nginx default, artifact store. - Nightly gate `maxMutationCrap` 57 -> 47 (highest left: BuildExecutor.runBuildCommands 46.55). PR-doc: `docs/prs/2026-10-04-PR#107-environment-probes.md` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: mhoennig <michael@hoennig.de> Reviewed-on: #107
107 lines
5.4 KiB
Markdown
107 lines
5.4 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#106 the highest Mutation-CRAP scores of Werkator were methods that ask the host something, and no test reached them:
|
|
|
|
| Method | Mutation-CRAP | Killed / eligible |
|
|
| ----------------------------------- | ------------- | ----------------- |
|
|
| `DockerSocketLocator.locate` | 56 | 0 / 9 |
|
|
| `InitCommand.runningJarPath` | 56 | 0 / 7 |
|
|
| `HostingEnvironments.readHostname` | 42 | 0 / 7 |
|
|
| `MetricsConfiguration.stateHome` | 42 | 0 / 6 |
|
|
| `NginxProxyManager.defaultStateDir` | 26.83 | 1 / 6 |
|
|
|
|
Each read the environment, the file system or the JVM directly, so its rules — which socket wins, where the jar is, which name counts — could only be tested on a host that happens to look a certain way.
|
|
The rule for Werkator's state directory, `XDG_STATE_HOME` or `~/.local/state`, was written out three times: for the metrics, the managed nginx and the artifact store.
|
|
|
|
## Non-Goals
|
|
|
|
- No change in behaviour: every socket, path and name is found as before.
|
|
- `MetricsConfiguration.monitoredDisks` and `NginxProxyManager.cleanupStaleContainers` stay for a later PR.
|
|
|
|
## The Scenarios
|
|
|
|
### Feature: What Werkator asks the host is decided by rules with tests
|
|
|
|
#### Scenario#107.01: A unix DOCKER_HOST wins, then the rootless socket, then the system socket
|
|
|
|
So that a build container gets the socket the legacy script would have given it.
|
|
|
|
- **Given** an environment, a set of existing sockets and their groups
|
|
- **When** the socket for a uid is located
|
|
- **Then** a unix `DOCKER_HOST` wins, then `/run/user/<uid>/docker.sock` (rootless), then `/var/run/docker.sock` with its group
|
|
- **And** a tcp `DOCKER_HOST` or a socket that does not exist gives none
|
|
|
|
##### Verified by
|
|
|
|
- [DockerSocketLocatorUnitTest](../../src/test/kotlin/de/hoennig/werkator/build/DockerSocketLocatorUnitTest.kt) — Scenario#107.01
|
|
|
|
#### Scenario#107.02: The running jar is the one java -jar names, else the one in the loader's URL
|
|
|
|
So that `init --systemd` writes the jar the service really runs.
|
|
|
|
- **Given** a launch command and a code source URL
|
|
- **When** the running jar is found
|
|
- **Then** a `java -jar` command names it, a Spring Boot loader URL holds it, and running from classes gives none
|
|
|
|
##### Verified by
|
|
|
|
- [InitCommandUnitTest](../../src/test/kotlin/de/hoennig/werkator/commands/InitCommandUnitTest.kt) — Scenario#107.02
|
|
|
|
#### Scenario#107.03: The machine's name comes from /etc/hostname, the resolver only when the file says nothing
|
|
|
|
So that a resolver alias never names a Hostsharing machine.
|
|
|
|
- **Given** a hostname file and a resolver, each answering, blank, or failing
|
|
- **When** the machine's name is read
|
|
- **Then** the file's name wins, the resolver answers when the file is blank or missing, and nothing answers when both fail
|
|
|
|
##### Verified by
|
|
|
|
- [HostingEnvironmentUnitTest](../../src/test/kotlin/de/hoennig/werkator/server/HostingEnvironmentUnitTest.kt) — Scenario#107.03
|
|
|
|
#### Scenario#107.04: The state home is XDG_STATE_HOME, else ~/.local/state
|
|
|
|
So that the artifact store, the managed nginx and the metrics agree where Werkator's state lives.
|
|
|
|
- **Given** `XDG_STATE_HOME` set, unset, and blank
|
|
- **When** the state home is asked for
|
|
- **Then** it is that directory, or `~/.local/state`
|
|
|
|
##### Verified by
|
|
|
|
- [StateHomeUnitTest](../../src/test/kotlin/de/hoennig/werkator/config/StateHomeUnitTest.kt) — Scenario#107.04
|
|
|
|
## The Solution
|
|
|
|
- `DockerSocketLocator` asks the host through three seams — the environment, whether a path exists, the group of a socket — given by its Spring constructor; `candidate` holds the order of the rules.
|
|
- `InitCommand.jarPathOf` decides from the launch command and the code source URL; `runningJarPath` only reads the two.
|
|
- `HostingEnvironments.hostnameOf` decides between the hostname file and the resolver; `readHostname` only names the two.
|
|
- `StateHome.of` is the one rule for the state directory; the metrics, the nginx default and the artifact store use it.
|
|
- The gate `maxMutationCrap` falls from 57 to 47: the highest score left is `BuildExecutor.runBuildCommands` at 46.55.
|
|
|
|
Measured with PIT as the nightly build runs it:
|
|
|
|
| Method | Before | After |
|
|
| ----------------------------------- | ------ | ---------------- |
|
|
| `DockerSocketLocator.locate` | 56 | 5.12 |
|
|
| `DockerSocketLocator.candidate` | — | 5 (6 / 6 killed) |
|
|
| `InitCommand.runningJarPath` | 56 | 6 |
|
|
| `InitCommand.jarPathOf` | — | 6 (6 / 6 killed) |
|
|
| `HostingEnvironments.readHostname` | 42 | 2 |
|
|
| `HostingEnvironments.hostnameOf` | — | 8 (8 / 8 killed) |
|
|
| `MetricsConfiguration.stateHome` | 42 | no mutant left |
|
|
| `StateHome.of` | — | 6.17 |
|
|
| `NginxProxyManager.defaultStateDir` | 26.83 | 2 |
|
|
|
|
## Additional Changes
|
|
|
|
- Version 1.34.4 with its release note.
|
|
|
|
## Follow-up PRs
|
|
|
|
- The rest of `BuildExecutor` (`runBuildCommands` 46.55, `shutdown` 42, `cancel` 30, `startBuild` 28.67), with a decision on how the nightly run measures it.
|