> Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation. ## The Problem `FileArtifactStore.prune` is above the Kotlin target of 15, measured by the nightly mutation build of 2026-10-05: | Method | Mutation-CRAP | Complexity | Mutation score | |---------------------------|---------------|------------|----------------| | `FileArtifactStore.prune` | 23.50 | 7 | 0.30 | Most of its complexity and most of its 23 mutants were not its own. `lock.write` and `use` are inline functions, so their code is compiled into `prune`: - `lock.write` gives back every read hold before it takes the write lock, and takes them again afterwards, for a caller that upgrades from the read lock; a prune never holds it, so those loops never run and no test can tell their mutants apart; - `use` carries the closing of the stream with its exception handling. The few mutants that were its own — the journal line and its condition — no test looked at. ## Non-Goals - No change in behaviour: the same directories are deleted under the same lock, with the same journal line. - No change to `persist`, which takes the read lock as before. ## The Scenarios ### Feature: pruning removes what no result keeps, and says so #### Scenario#125.01: prune says in the journal what it removed, and is silent when nothing goes So that the journal tells which artifacts went, without a line for every poll that removed nothing. - **Given** an artifact directory with nested reports that no kept result references - **When** the store is pruned - **Then** the directory is deleted with everything in it, and its key is returned - **and** the journal says `pruned 1 artifact dir(s): [<key>]` - **When** the store is pruned again - **Then** nothing is returned and the journal says nothing - **When** the store never persisted anything - **Then** a prune removes nothing and creates no `branches` directory ##### Verified by - [FileArtifactStoreUnitTest](../../src/test/kotlin/de/hoennig/werkator/artifacts/FileArtifactStoreUnitTest.kt) `prune says in the journal what it removed, and is silent when nothing goes (Scenario#125.01)`, `prune of a store that never persisted anything removes nothing and creates nothing (Scenario#125.01)`, and the earlier prune tests, including `concurrent persists and a prune do not interfere` ## The Solution `prune` takes the write lock itself, `lock()` and `unlock()` in `finally`, and hands the work to `pruneUnreferenced`. `pruneUnreferenced` lists the directory completely through `childrenOf`, then deletes what no kept key names, then writes the journal line. Listing first and deleting afterwards also stops deleting entries out of a directory stream that is still being read. A local PIT run of `FileArtifactStore` afterwards: | Method | Mutation-CRAP | Complexity | Mutation score | |---------------------------------------|---------------|------------|----------------| | `FileArtifactStore.prune` | 1.04 | 1 | 0.67 | | `FileArtifactStore.pruneUnreferenced` | 5.05 | 5 | 0.88 | | `FileArtifactStore.childrenOf` | 1.30 | 1 | 0.33 | The highest score left in the class is `copyArtifactDirs` at 5.93. The project maximum is now `UiFormats.repoStatusLine` at 16.03, so the gate goes down from 24 to 17. ## Additional Changes - `maxMutationCrap` in `build.gradle.kts` lowered from 24 to 17. - Version 1.37.6 with its entry in `releases.html`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: mhoennig <michael@hoennig.de> Reviewed-on: #125
73 lines
3.8 KiB
Markdown
73 lines
3.8 KiB
Markdown
# PR#125: Artifact Store Pruning Split into Its Steps
|
|
|
|
> **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
|
|
|
|
`FileArtifactStore.prune` is above the Kotlin target of 15, measured by the nightly mutation build of 2026-10-05:
|
|
|
|
| Method | Mutation-CRAP | Complexity | Mutation score |
|
|
|---------------------------|---------------|------------|----------------|
|
|
| `FileArtifactStore.prune` | 23.50 | 7 | 0.30 |
|
|
|
|
Most of its complexity and most of its 23 mutants were not its own.
|
|
`lock.write` and `use` are inline functions, so their code is compiled into `prune`:
|
|
|
|
- `lock.write` gives back every read hold before it takes the write lock, and takes them again afterwards, for a caller that upgrades from the read lock; a prune never holds it, so those loops never run and no test can tell their mutants apart;
|
|
- `use` carries the closing of the stream with its exception handling.
|
|
|
|
The few mutants that were its own — the journal line and its condition — no test looked at.
|
|
|
|
## Non-Goals
|
|
|
|
- No change in behaviour: the same directories are deleted under the same lock, with the same journal line.
|
|
- No change to `persist`, which takes the read lock as before.
|
|
|
|
## The Scenarios
|
|
|
|
### Feature: pruning removes what no result keeps, and says so
|
|
|
|
#### Scenario#125.01: prune says in the journal what it removed, and is silent when nothing goes
|
|
|
|
So that the journal tells which artifacts went, without a line for every poll that removed nothing.
|
|
|
|
- **Given** an artifact directory with nested reports that no kept result references
|
|
- **When** the store is pruned
|
|
- **Then** the directory is deleted with everything in it, and its key is returned
|
|
- **and** the journal says `pruned 1 artifact dir(s): [<key>]`
|
|
- **When** the store is pruned again
|
|
- **Then** nothing is returned and the journal says nothing
|
|
- **When** the store never persisted anything
|
|
- **Then** a prune removes nothing and creates no `branches` directory
|
|
|
|
##### Verified by
|
|
|
|
- [FileArtifactStoreUnitTest](../../src/test/kotlin/de/hoennig/werkator/artifacts/FileArtifactStoreUnitTest.kt) `prune says in the journal what it removed, and is silent when nothing goes (Scenario#125.01)`, `prune of a store that never persisted anything removes nothing and creates nothing (Scenario#125.01)`, and the earlier prune tests, including `concurrent persists and a prune do not interfere`
|
|
|
|
## The Solution
|
|
|
|
`prune` takes the write lock itself, `lock()` and `unlock()` in `finally`, and hands the work to `pruneUnreferenced`.
|
|
`pruneUnreferenced` lists the directory completely through `childrenOf`, then deletes what no kept key names, then writes the journal line.
|
|
Listing first and deleting afterwards also stops deleting entries out of a directory stream that is still being read.
|
|
|
|
A local PIT run of `FileArtifactStore` afterwards:
|
|
|
|
| Method | Mutation-CRAP | Complexity | Mutation score |
|
|
|---------------------------------------|---------------|------------|----------------|
|
|
| `FileArtifactStore.prune` | 1.04 | 1 | 0.67 |
|
|
| `FileArtifactStore.pruneUnreferenced` | 5.05 | 5 | 0.88 |
|
|
| `FileArtifactStore.childrenOf` | 1.30 | 1 | 0.33 |
|
|
|
|
The highest score left in the class is `copyArtifactDirs` at 5.93.
|
|
The project maximum is now `UiFormats.repoStatusLine` at 16.03, so the gate goes down from 24 to 17.
|
|
|
|
## Additional Changes
|
|
|
|
- `maxMutationCrap` in `build.gradle.kts` lowered from 24 to 17.
|
|
- Version 1.37.6 with its entry in `releases.html`.
|
|
|
|
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|