Files
werkator/docs/prs/2026-10-04-PR#107-environment-probes.md
miandmhoennig fadbcd305f
werkator build succeeded after 02:19
werkator/deploy build succeeded after 00:12
Unit tests for detecting the Docker socket, the running jar, the hostname and the state directory (v1.34.4) (#107)
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
2026-10-04 12:23:50 +02:00

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.