> 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
3.8 KiB
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.writegives 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;usecarries 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>]
- and the journal says
- 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
branchesdirectory
Verified by
- FileArtifactStoreUnitTest
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, includingconcurrent 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
maxMutationCrapinbuild.gradle.ktslowered from 24 to 17.- Version 1.37.6 with its entry in
releases.html.
🤖 Generated with Claude Code