mutineer 0.11.4 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +91 -0
- data/README.md +20 -4
- data/lib/mutineer/baseline.rb +48 -10
- data/lib/mutineer/changed_lines.rb +11 -3
- data/lib/mutineer/cli.rb +18 -3
- data/lib/mutineer/config.rb +5 -0
- data/lib/mutineer/coverage_map.rb +404 -30
- data/lib/mutineer/daemon_backend.rb +18 -2
- data/lib/mutineer/daemon_client.rb +7 -4
- data/lib/mutineer/daemon_server.rb +2 -1
- data/lib/mutineer/file_swap.rb +136 -40
- data/lib/mutineer/progress.rb +46 -0
- data/lib/mutineer/reporter.rb +22 -6
- data/lib/mutineer/runner.rb +59 -32
- data/lib/mutineer/version.rb +1 -1
- data/lib/mutineer/worker_pool.rb +7 -1
- data/lib/mutineer.rb +1 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a96e7bad1e1cf8d106957e770418e6a1854c677943114b9bc45421c4e06da7f2
|
|
4
|
+
data.tar.gz: c404d87071f3d270566e368ac70153f1e87c940eb170b220b972fb1b28b29889
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 15417ab5322c43c537953eaa438c18a9a89794f199a9c4666f031daf60f55f6c3ffabcb2e69e15b7ffd030e48b6f2d5d3ab41ee01ccfe9d52dd6c9ee22b4971c
|
|
7
|
+
data.tar.gz: 1e68e920b3aa610ea1715c16e48e89060e8eba2b2511aafb3c7db0c44e21f8fac077529173befd9acf3afbe2962829cf4534dce371a2f6262628dd507f37469b
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,95 @@ All notable changes to this project are documented here. The format is based on
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.0.1] - 2026-09-18
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **RubyGems `documentation_uri`**: the published gem now points at the docs
|
|
13
|
+
site (`https://davidteren.github.io/mutineer/`) so gem-page discovery
|
|
14
|
+
reaches the Pages docs (#90).
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **A red unmutated suite can no longer pass a mutation gate** — coverage
|
|
18
|
+
capture now keeps the original Minitest/RSpec result, and a failing clean run
|
|
19
|
+
aborts with the existing smoke-check error (exit 1) instead of scoring those
|
|
20
|
+
assertion failures as killed mutants. A warm coverage cache re-checks the
|
|
21
|
+
current suite and cannot bypass this (#96).
|
|
22
|
+
- **Concurrent external runs no longer restore each other's source files** —
|
|
23
|
+
swap and orphan recovery share one exclusive OS lock per source, acquired
|
|
24
|
+
before reading or healing. A live owner's mutant and backup stay intact; a
|
|
25
|
+
dead owner's backup still restores the original bytes (#99).
|
|
26
|
+
- **Coverage cache now invalidates when a required test helper changes** —
|
|
27
|
+
a successful map records fingerprints of project-local loaded Ruby files,
|
|
28
|
+
old cache entries without that data rebuild, and a helper-only edit no
|
|
29
|
+
longer hides a new survivor behind a stale 100% score (#97).
|
|
30
|
+
- **Release version calculation ignores floating major tags** — `release-pr.yml`
|
|
31
|
+
selects the newest complete `vMAJOR.MINOR.PATCH` ancestor and validates the
|
|
32
|
+
next version before writing files, so a later `v1` tag can no longer produce
|
|
33
|
+
`v1..1` (#95).
|
|
34
|
+
|
|
35
|
+
## [1.0.0] - 2026-09-08
|
|
36
|
+
|
|
37
|
+
The GitHub Action's PR default changes in this release, which is why it is a
|
|
38
|
+
new major: workflows pinned to `davidteren/mutineer@v0` keep the old full-scan
|
|
39
|
+
behavior; upgrading to `@v1` opts into diff-scoped PR runs (details under
|
|
40
|
+
Changed).
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
- **The Action reports where CI readers look**: with the default JSON format it
|
|
44
|
+
writes a score/pass-fail table (plus the baseline delta, when `baseline` is
|
|
45
|
+
set) to the job step summary, and emits one `file=…,line=…` annotation per
|
|
46
|
+
surviving mutant (up to 50; the summary lists the first 20, the full set
|
|
47
|
+
stays in the JSON report) so results land on the PR diff instead of only in a
|
|
48
|
+
collapsed log group — `error` level when the gate failed, `warning` when it
|
|
49
|
+
passed. When `output` is unset the JSON report is routed to a temp file and
|
|
50
|
+
still printed to the log, and the `report` output now exposes the report
|
|
51
|
+
path in both cases so a later step can consume the JSON without scraping
|
|
52
|
+
the log (#86).
|
|
53
|
+
- **Progress during the run**: every backend prints `[mutineer] N/M mutants
|
|
54
|
+
(P%)` to stderr at each 10% step, so a long run is never silent between
|
|
55
|
+
config resolution and the report. Stdout stays byte-exact for `--format json`
|
|
56
|
+
and `--output`. `WorkerPool#run` gains an optional `on_result:` callback for
|
|
57
|
+
this (called in the parent per reaped result) (#86).
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
- **PR runs scope themselves in the GitHub Action**: on `pull_request` events
|
|
61
|
+
the `since` input now defaults to the PR base, so the action grades just the
|
|
62
|
+
diff out of the box. **Migration note (the reason for the major bump)**: a
|
|
63
|
+
PR gate that previously full-scanned now scores only the PR's changed lines,
|
|
64
|
+
so `threshold` applies to fewer mutants. Stay on `@v0` to keep the old
|
|
65
|
+
default, or pass `since: none` on `@v1` for full scans. Workflows that
|
|
66
|
+
already pass a non-empty `since` are unchanged (an explicit empty string is
|
|
67
|
+
indistinguishable from unset and picks up the new default). A PR with no
|
|
68
|
+
changed source lines (docs- or test-only) scores zero mutants and passes the
|
|
69
|
+
scoped gate vacuously; keep a full-scan baseline refresh on main as the
|
|
70
|
+
backstop. With `use-bundler: true` the caller's Gemfile picks the gem, and
|
|
71
|
+
the new default needs mutineer >= 1.0.0 (the action enforces the floor with
|
|
72
|
+
a clear error). The action scopes to the PR's exact base commit from the
|
|
73
|
+
event payload (immune to the
|
|
74
|
+
base branch advancing mid-job), falling back to a fresh fetch of the base
|
|
75
|
+
branch tip; when neither can be resolved it warns and runs without an
|
|
76
|
+
action-provided `--since` (a `.mutineer.yml` `since:` key, if any, still
|
|
77
|
+
applies). The default deliberately does NOT fire on `pull_request_target`:
|
|
78
|
+
checkout there defaults to the base branch, so auto-scoping would diff the
|
|
79
|
+
base against itself and green the gate on an empty run (#86).
|
|
80
|
+
- **`--baseline` on a diff-scoped run gates on new survivors only**: a
|
|
81
|
+
`--since` run's score covers only the changed-line mutants, a different
|
|
82
|
+
denominator from a full-run baseline, so comparing the two scores
|
|
83
|
+
manufactured false regressions (a 3/4-mutant PR at 75% "dropped" from a
|
|
84
|
+
92% whole-repo baseline with zero new survivors). With `--since`, the
|
|
85
|
+
score-drop half of the baseline gate is skipped; new-survivor detection by
|
|
86
|
+
stable id (and the reported before/after scores) are unchanged. The JSON
|
|
87
|
+
report records the scope in a new additive `summary.scoped` key
|
|
88
|
+
(`schema_version` 1.3), and the `baseline` block records `score_comparable`
|
|
89
|
+
so a consumer knows when not to render the two scores as a comparison
|
|
90
|
+
(`fixed_survivors` is likewise empty under a scoped side: an out-of-scope
|
|
91
|
+
survivor was never re-tested, so absence does not mean fixed). The reverse
|
|
92
|
+
direction is a hard guard: a scoped report is refused as a baseline (exit 2
|
|
93
|
+
with a regenerate hint), because survivors outside its diff would all read
|
|
94
|
+
as new regressions. A new `--no-since` flag disables diff scoping explicitly: a
|
|
95
|
+
typed no beats a `.mutineer.yml` `since:` key, and the action's
|
|
96
|
+
`since: none` passes it through (#86).
|
|
97
|
+
|
|
9
98
|
## [0.11.4] - 2026-07-29
|
|
10
99
|
|
|
11
100
|
### Fixed
|
|
@@ -318,6 +407,8 @@ Rails hardening + CI batch (issues #8–#13), all verified Rails-free.
|
|
|
318
407
|
- `.mutineer.yml` configuration (CLI > config > default precedence).
|
|
319
408
|
- Byte-correct source handling for multibyte (UTF-8) sources.
|
|
320
409
|
|
|
410
|
+
[1.0.1]: https://github.com/davidteren/mutineer/releases/tag/v1.0.1
|
|
411
|
+
[1.0.0]: https://github.com/davidteren/mutineer/releases/tag/v1.0.0
|
|
321
412
|
[0.11.4]: https://github.com/davidteren/mutineer/releases/tag/v0.11.4
|
|
322
413
|
[0.11.3]: https://github.com/davidteren/mutineer/releases/tag/v0.11.3
|
|
323
414
|
[0.11.2]: https://github.com/davidteren/mutineer/releases/tag/v0.11.2
|
data/README.md
CHANGED
|
@@ -51,7 +51,8 @@ mutineer run lib/calculator.rb --test test/calculator_test.rb --threshold 90
|
|
|
51
51
|
| `--only NAME` | Restrict to one fully-qualified subject, e.g. `Calculator#add` |
|
|
52
52
|
| `--framework NAME` | `minitest` (default) or `rspec`; auto-detected as rspec when most `--test` files end in `_spec.rb` |
|
|
53
53
|
| `--since REF` | Only mutate lines changed since git `REF` (e.g. `origin/main`) — ideal for PR CI |
|
|
54
|
-
| `--
|
|
54
|
+
| `--no-since` | Disable diff scoping; a typed no beats a `.mutineer.yml` `since:` key |
|
|
55
|
+
| `--baseline FILE` | Compare against a prior `--format json` run; exit 1 on new survivors / score drop (score drop is skipped under `--since`, whose score covers a different denominator; see [CI](#ci-gating)) |
|
|
55
56
|
| `--baseline-epsilon FLOAT` | Score-drop tolerance for `--baseline` (default: 0) |
|
|
56
57
|
| `--jobs N` | Parallel worker count (default: processor count; `1` under `--rails`) |
|
|
57
58
|
| `--verbose` | Surface the real error when a fork capture fails (alias `--debug`) |
|
|
@@ -233,17 +234,32 @@ This repo ships a composite action (`action.yml`) that wraps the CLI for CI:
|
|
|
233
234
|
|
|
234
235
|
```yaml
|
|
235
236
|
- uses: actions/checkout@v4
|
|
236
|
-
with: { fetch-depth: 0 } # --since needs full history
|
|
237
237
|
- uses: ruby/setup-ruby@v1
|
|
238
238
|
with: { ruby-version: "3.4", bundler-cache: true }
|
|
239
|
-
- uses: davidteren/mutineer@
|
|
239
|
+
- uses: davidteren/mutineer@v1
|
|
240
240
|
with:
|
|
241
241
|
sources: app/
|
|
242
|
-
since: origin/${{ github.base_ref }}
|
|
243
242
|
baseline: .mutineer/baseline.json
|
|
244
243
|
threshold: "90"
|
|
245
244
|
```
|
|
246
245
|
|
|
246
|
+
**Default change:** on `pull_request` events (not `pull_request_target`) the
|
|
247
|
+
action scopes the run to the PR's changed lines, diffing against the PR's exact
|
|
248
|
+
base commit (fetched by the action itself when the checkout is shallow; falls
|
|
249
|
+
back to the base branch tip). Pass `since: none` for a full scan, or an
|
|
250
|
+
explicit `since:` ref (which needs `fetch-depth: 0` on checkout).
|
|
251
|
+
|
|
252
|
+
With the default JSON format the action also:
|
|
253
|
+
|
|
254
|
+
- writes a score summary to the job's step summary;
|
|
255
|
+
- annotates surviving mutants on the PR diff, up to 50 (`error` level when the
|
|
256
|
+
gate failed, `warning` when it passed);
|
|
257
|
+
- exposes the report path via the `report` output for later steps (with
|
|
258
|
+
`format: human`/`html` this needs the `output` input).
|
|
259
|
+
|
|
260
|
+
The CLI prints a progress line to the log at every 10% of the run, whatever
|
|
261
|
+
the format.
|
|
262
|
+
|
|
247
263
|
## For AI agents & pipelines
|
|
248
264
|
|
|
249
265
|
Mutineer is built for programmatic use — versioned JSON, stable mutant ids,
|
data/lib/mutineer/baseline.rb
CHANGED
|
@@ -15,12 +15,19 @@ module Mutineer
|
|
|
15
15
|
# new_survivors - current Result objects whose stable id is absent from
|
|
16
16
|
# the baseline (the regressions to name).
|
|
17
17
|
# fixed_survivors - baseline survivor hashes absent from the current run
|
|
18
|
-
# (informational, never gates).
|
|
18
|
+
# (informational, never gates). Empty when either side
|
|
19
|
+
# is diff-scoped: an out-of-scope baseline survivor was
|
|
20
|
+
# never re-tested, so absence does not mean fixed.
|
|
19
21
|
# score_drop - current score < baseline score - epsilon. nil on
|
|
20
|
-
# either side skips the check
|
|
22
|
+
# either side skips the check, and a diff-scoped run
|
|
23
|
+
# (`scoped: true`) never sets it (see #diff).
|
|
24
|
+
# score_comparable - the two scores share a denominator (neither side was
|
|
25
|
+
# diff-scoped and both are non-nil), so a consumer may
|
|
26
|
+
# render them side by side. False means the score-drop
|
|
27
|
+
# check was skipped, not that it passed.
|
|
21
28
|
# regressed - any new survivors OR a score drop.
|
|
22
29
|
Delta = Data.define(:new_survivors, :fixed_survivors,
|
|
23
|
-
:score_before, :score_after, :score_drop, :regressed)
|
|
30
|
+
:score_before, :score_after, :score_drop, :score_comparable, :regressed)
|
|
24
31
|
|
|
25
32
|
# Load a prior --format json run. Raises ConfigError (NOT exit: a data class
|
|
26
33
|
# must never kill the host) on a missing/unreadable file, unparseable JSON,
|
|
@@ -35,6 +42,15 @@ module Mutineer
|
|
|
35
42
|
unless doc.is_a?(Hash) && doc["schema_version"] && doc["survivors"].is_a?(Array)
|
|
36
43
|
raise ConfigError, "not a Mutineer JSON report: #{path}"
|
|
37
44
|
end
|
|
45
|
+
# A diff-scoped report covers only that diff's mutants: used as a
|
|
46
|
+
# baseline, every survivor outside the original diff would read as a NEW
|
|
47
|
+
# regression, and its score shares no denominator with any other run.
|
|
48
|
+
# Refuse loudly (exit 2 via the CLI) rather than gate unreliably.
|
|
49
|
+
if doc.dig("summary", "scoped") == true
|
|
50
|
+
raise ConfigError, "#{path} was written by a --since run and covers only that diff's " \
|
|
51
|
+
"mutants; regenerate the baseline from a full run " \
|
|
52
|
+
"(use --no-since if .mutineer.yml sets since:)"
|
|
53
|
+
end
|
|
38
54
|
|
|
39
55
|
new(doc)
|
|
40
56
|
rescue JSON::ParserError => e
|
|
@@ -45,39 +61,61 @@ module Mutineer
|
|
|
45
61
|
|
|
46
62
|
# Builds a baseline from a JSON document.
|
|
47
63
|
#
|
|
48
|
-
# The baseline retains the survivor document
|
|
64
|
+
# The baseline retains the survivor document, score, and scope marker from
|
|
65
|
+
# the JSON report. A report whose `summary.scoped` is true came from a
|
|
66
|
+
# `--since` run: its score covers only changed-line mutants, so later diffs
|
|
67
|
+
# must not compare a full-run score against it.
|
|
49
68
|
#
|
|
50
69
|
# @param doc [Hash] parsed JSON document.
|
|
51
70
|
def initialize(doc)
|
|
52
71
|
@survivors = doc["survivors"] || []
|
|
53
72
|
@score = doc.dig("summary", "score")
|
|
73
|
+
# Strict literal true only: a malformed value (say the STRING "false" in a
|
|
74
|
+
# hand-edited baseline) must not silently disable the score-drop gate.
|
|
75
|
+
@scoped = doc.dig("summary", "scoped") == true
|
|
54
76
|
end
|
|
55
77
|
|
|
56
78
|
# Diff a current AggregateResult against this baseline by stable survivor id.
|
|
57
79
|
# `epsilon` tolerates float jitter on the score (default 0.0 = any drop
|
|
58
80
|
# gates).
|
|
59
81
|
#
|
|
82
|
+
# `scoped: true` marks the current run as diff-scoped (`--since`): its score
|
|
83
|
+
# is computed over only the changed-line mutants, a different denominator
|
|
84
|
+
# from a full-run baseline, so comparing the two scores manufactures false
|
|
85
|
+
# regressions. A scoped diff keeps the new-survivor gate (stable ids compare
|
|
86
|
+
# fine across scopes) and still reports both scores, but never sets
|
|
87
|
+
# score_drop.
|
|
88
|
+
#
|
|
60
89
|
# @param aggregate [Mutineer::AggregateResult] current results.
|
|
61
90
|
# @param epsilon [Float] score-drop tolerance.
|
|
91
|
+
# @param scoped [Boolean] current run was diff-scoped (`--since`).
|
|
62
92
|
# @return [Mutineer::Baseline::Delta] delta summary.
|
|
63
|
-
def diff(aggregate, epsilon: 0.0)
|
|
93
|
+
def diff(aggregate, epsilon: 0.0, scoped: false)
|
|
64
94
|
current = aggregate.surviving_mutants
|
|
65
95
|
current_ids = current.map(&:id)
|
|
66
96
|
baseline_ids = @survivors.map { |h| h["id"] }
|
|
67
97
|
|
|
68
98
|
new_survivors = current.reject { |r| baseline_ids.include?(r.id) }
|
|
69
|
-
|
|
99
|
+
# Under a diff-scoped side an out-of-scope baseline survivor was never
|
|
100
|
+
# re-tested, so reporting it "fixed" would be false: empty is honest.
|
|
101
|
+
fixed = if scoped || @scoped
|
|
102
|
+
[]
|
|
103
|
+
else
|
|
104
|
+
@survivors.reject { |h| current_ids.include?(h["id"]) }
|
|
105
|
+
end
|
|
70
106
|
|
|
71
107
|
current_score = aggregate.mutation_score
|
|
72
108
|
# nil-score discipline (mirrors Reporter#exit_code): a score absent on
|
|
73
109
|
# either side cannot be compared. Skip the drop check, keep the new-
|
|
74
|
-
# survivor check.
|
|
75
|
-
|
|
76
|
-
|
|
110
|
+
# survivor check. Same when EITHER side is diff-scoped (the current run
|
|
111
|
+
# via `scoped:`, or the stored baseline via its `summary.scoped` marker):
|
|
112
|
+
# the denominators differ, so the scores are not comparable.
|
|
113
|
+
comparable = !scoped && !@scoped && !@score.nil? && !current_score.nil?
|
|
114
|
+
score_drop = comparable && current_score < @score - epsilon
|
|
77
115
|
|
|
78
116
|
Delta.new(new_survivors: new_survivors, fixed_survivors: fixed,
|
|
79
117
|
score_before: @score, score_after: current_score,
|
|
80
|
-
score_drop: score_drop,
|
|
118
|
+
score_drop: score_drop, score_comparable: comparable,
|
|
81
119
|
regressed: !new_survivors.empty? || score_drop)
|
|
82
120
|
end
|
|
83
121
|
end
|
|
@@ -57,16 +57,24 @@ module Mutineer
|
|
|
57
57
|
|
|
58
58
|
# Returns the stdout of `git -C <root> diff --unified=0 <ref> -- <file>`.
|
|
59
59
|
#
|
|
60
|
+
# A failure is warned, never silent: an empty result means "no changed
|
|
61
|
+
# lines", which under `--since` removes every mutant for the file — a green
|
|
62
|
+
# gate must not be manufactured by a broken diff without a trace.
|
|
63
|
+
#
|
|
60
64
|
# @param ref [String] git ref to diff against.
|
|
61
65
|
# @param abs_file [String] absolute path of the file being diffed.
|
|
62
66
|
# @param project_root [String] repository root for `git -C`.
|
|
63
|
-
# @return [String] diff text, or `""` on failure.
|
|
67
|
+
# @return [String] diff text, or `""` on failure (after a stderr warning).
|
|
64
68
|
def git_diff(ref, abs_file, project_root)
|
|
65
69
|
out, _err, status = Open3.capture3(
|
|
66
70
|
"git", "-C", project_root, "diff", "--unified=0", ref, "--", abs_file
|
|
67
71
|
)
|
|
68
|
-
status.success?
|
|
69
|
-
|
|
72
|
+
return out if status.success?
|
|
73
|
+
|
|
74
|
+
warn "[mutineer] git diff failed for #{abs_file}; its lines will not be mutated (--since)"
|
|
75
|
+
""
|
|
76
|
+
rescue StandardError => e
|
|
77
|
+
warn "[mutineer] git diff failed for #{abs_file} (#{e.class}); its lines will not be mutated (--since)"
|
|
70
78
|
""
|
|
71
79
|
end
|
|
72
80
|
end
|
data/lib/mutineer/cli.rb
CHANGED
|
@@ -41,6 +41,7 @@ module Mutineer
|
|
|
41
41
|
--baseline-epsilon FLOAT Score-drop tolerance for --baseline (default: 0)
|
|
42
42
|
--only NAME Restrict to one fully-qualified subject
|
|
43
43
|
--since REF Only mutate lines changed since git REF (e.g. origin/main)
|
|
44
|
+
--no-since Disable diff scoping (a typed no beats a .mutineer.yml since:)
|
|
44
45
|
--jobs N Parallel worker count (default: processor count)
|
|
45
46
|
--strategy NAME reload (whole-file) or redefine (surgical); default: reload
|
|
46
47
|
--framework NAME minitest or rspec (default: auto-detect from --test names)
|
|
@@ -96,6 +97,9 @@ module Mutineer
|
|
|
96
97
|
o.on("--fail-fast") { opts[:fail_fast] = true; explicit << :fail_fast }
|
|
97
98
|
o.on("--only NAME") { |v| opts[:only] = v; explicit << :only }
|
|
98
99
|
o.on("--since REF") { |v| opts[:since] = v; explicit << :since }
|
|
100
|
+
# A typed "no" must beat a .mutineer.yml `since:` key (CLI-over-config
|
|
101
|
+
# precedence): marking :since explicit with a nil value blocks the fill.
|
|
102
|
+
o.on("--no-since") { opts[:since] = nil; explicit << :since }
|
|
99
103
|
o.on("--test FILE") { |v| (opts[:tests] ||= []) << v }
|
|
100
104
|
o.on("--operators LIST") { |v| opts[:operators] = v.split(",").map(&:strip); explicit << :operators }
|
|
101
105
|
o.on("--threshold FLOAT") do |v|
|
|
@@ -214,6 +218,11 @@ module Mutineer
|
|
|
214
218
|
# environment, not weak tests. Runtime error (exit 1), not usage (exit 2).
|
|
215
219
|
warn "mutineer: #{e.message}"
|
|
216
220
|
exit 1
|
|
221
|
+
rescue Mutineer::ConcurrentRunError => e
|
|
222
|
+
# Another process owns a source file. Runtime error (exit 1), not a
|
|
223
|
+
# backtrace: the working tree is still the other run's responsibility.
|
|
224
|
+
warn "mutineer: #{e.message}"
|
|
225
|
+
exit 1
|
|
217
226
|
rescue Mutineer::DaemonBootError => e
|
|
218
227
|
# The daemon is gone for good, so the run ended rather than scoring the rest
|
|
219
228
|
# against it. A deliberate stop deserves a message, not a raw backtrace.
|
|
@@ -465,11 +474,17 @@ module Mutineer
|
|
|
465
474
|
|
|
466
475
|
# Diff the current run against the baseline (preflighted above) by the
|
|
467
476
|
# stable survivor id. The delta is rendered inline (human section / additive
|
|
468
|
-
# json block) and gates exit independently of --threshold.
|
|
469
|
-
|
|
477
|
+
# json block) and gates exit independently of --threshold. A --since run is
|
|
478
|
+
# scoped: its score covers a different denominator than a full-run baseline,
|
|
479
|
+
# so only the new-survivor half of the gate applies (see Baseline#diff).
|
|
480
|
+
delta = if config.baseline
|
|
481
|
+
Baseline.load(config.baseline).diff(aggregate, epsilon: config.baseline_epsilon,
|
|
482
|
+
scoped: !config.since.nil?)
|
|
483
|
+
end
|
|
470
484
|
|
|
471
485
|
reporter.report(out: $stdout, err: $stderr, threshold: config.threshold,
|
|
472
|
-
format: config.format, output: config.output, baseline: delta
|
|
486
|
+
format: config.format, output: config.output, baseline: delta,
|
|
487
|
+
scoped: !config.since.nil?)
|
|
473
488
|
|
|
474
489
|
# Warn (stderr, so it never pollutes json/html) that an external run's score
|
|
475
490
|
# is not comparable to an in-process run: no coverage narrowing (uncovered
|
data/lib/mutineer/config.rb
CHANGED
|
@@ -182,6 +182,11 @@ module Mutineer
|
|
|
182
182
|
when "ignore" then Array(value).map(&:to_s)
|
|
183
183
|
when "baseline" then value.to_s
|
|
184
184
|
when "test_command" then value.to_s
|
|
185
|
+
when "since"
|
|
186
|
+
# false / empty normalize to nil ("no scoping"), so every consumer's
|
|
187
|
+
# nil-check (runner scoping, the report's scoped marker) agrees. A
|
|
188
|
+
# false left raw would skip scoping but still mark the report scoped.
|
|
189
|
+
value == false || value.to_s.empty? ? nil : value.to_s
|
|
185
190
|
else value
|
|
186
191
|
end
|
|
187
192
|
end
|