Files
werkator/docs/prs/2026-10-05-PR#125-artifact-prune-split.md
miandmhoennig 209cd696d4
werkator build succeeded after 03:20
werkator/deploy build succeeded after 00:08
Artifact store pruning split into its steps, takes the write lock directly, gate down to 17 (v1.37.6) (#125)
> 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
2026-10-05 20:04:45 +02:00

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)