mutineer 1.7.0 → 1.8.0
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 +432 -0
- data/README.md +157 -747
- data/lib/mutineer/baseline.rb +1 -1
- data/lib/mutineer/changed_lines.rb +2 -1
- data/lib/mutineer/cli.rb +119 -27
- data/lib/mutineer/concern_blocks.rb +168 -5
- data/lib/mutineer/config.rb +6 -0
- data/lib/mutineer/coverage_map.rb +92 -17
- data/lib/mutineer/daemon_backend.rb +75 -32
- data/lib/mutineer/daemon_client.rb +50 -17
- data/lib/mutineer/daemon_server.rb +92 -40
- data/lib/mutineer/external_backend.rb +150 -17
- data/lib/mutineer/isolation.rb +67 -25
- data/lib/mutineer/job_plan.rb +117 -33
- data/lib/mutineer/kill_channel.rb +24 -14
- data/lib/mutineer/migrate.rb +10 -8
- data/lib/mutineer/minitest_integration/in_test_database_error.rb +184 -0
- data/lib/mutineer/minitest_integration/kill_recorder.rb +25 -17
- data/lib/mutineer/minitest_integration.rb +44 -14
- data/lib/mutineer/mutators/array_literal.rb +4 -2
- data/lib/mutineer/mutators/base.rb +22 -0
- data/lib/mutineer/mutators/call_to_receiver.rb +20 -7
- data/lib/mutineer/mutators/chain_link.rb +6 -6
- data/lib/mutineer/mutators/condition_forcing.rb +6 -3
- data/lib/mutineer/mutators/operand_removal.rb +4 -2
- data/lib/mutineer/mutators/range_literal.rb +105 -4
- data/lib/mutineer/mutators/return_nil.rb +1 -0
- data/lib/mutineer/orphan_guard.rb +18 -6
- data/lib/mutineer/pairing.rb +62 -1
- data/lib/mutineer/project.rb +226 -12
- data/lib/mutineer/rails_worker_db.rb +200 -16
- data/lib/mutineer/redaction.rb +112 -11
- data/lib/mutineer/reporter.rb +109 -21
- data/lib/mutineer/result.rb +33 -5
- data/lib/mutineer/runner.rb +126 -18
- data/lib/mutineer/statement_lines.rb +14 -8
- data/lib/mutineer/subject.rb +13 -5
- data/lib/mutineer/test_runners/minitest.rb +2 -1
- data/lib/mutineer/test_runners.rb +4 -2
- data/lib/mutineer/triage.rb +1 -1
- data/lib/mutineer/version.rb +1 -1
- data/lib/mutineer/worker_pool.rb +2 -1
- data/skills/mutineer/SKILL.md +84 -0
- metadata +5 -1
data/lib/mutineer/subject.rb
CHANGED
|
@@ -15,10 +15,17 @@ module Mutineer
|
|
|
15
15
|
# constant names, such as an `included` block that runs in each includer),
|
|
16
16
|
# so the redefine strategy cannot load a mutant onto it. `hook_block` is true
|
|
17
17
|
# for a def anywhere inside a concern's `included` or `prepended` block, which
|
|
18
|
-
# ActiveSupport runs only when a class first includes the concern, so
|
|
19
|
-
# reload
|
|
18
|
+
# ActiveSupport runs only when a class first includes the concern, so a
|
|
19
|
+
# reload of the file does not reach a class that already has it (#256).
|
|
20
|
+
# `reload_hook_call` is the `included` or `prepended` call node of such a
|
|
21
|
+
# block when reload can still place the def: the mutant file stores the
|
|
22
|
+
# mutated def just before that call, and the child loads it into each class
|
|
23
|
+
# that has the method (#297). It is nil when reload cannot place the def, even
|
|
24
|
+
# when `hook_block` is true (see docs/json-schema.md for the cases).
|
|
25
|
+
# `enclosing_subject` is the subject of the `def` whose body holds this one,
|
|
26
|
+
# or nil, so the redefine strategy can load each enclosing method too (#290).
|
|
20
27
|
Subject = Struct.new(:file, :namespace, :name, :singleton, :def_node, :lexical, :block_owner, :owner_unknown,
|
|
21
|
-
:hook_block, keyword_init: true) do
|
|
28
|
+
:hook_block, :reload_hook_call, :enclosing_subject, keyword_init: true) do
|
|
22
29
|
# Returns the fully-qualified subject name.
|
|
23
30
|
#
|
|
24
31
|
# @return [String] namespaced method name like `Billing::Invoice#total`.
|
|
@@ -38,12 +45,13 @@ module Mutineer
|
|
|
38
45
|
# call it, so the mutant is not run. Redefine needs a named owner. Reload
|
|
39
46
|
# loads the whole file again, which redefines everything except what a
|
|
40
47
|
# concern's `included` or `prepended` block defines: that block does not run
|
|
41
|
-
# again in a class that already includes the concern.
|
|
48
|
+
# again in a class that already includes the concern. Reload places such a
|
|
49
|
+
# def only when discovery found its {#reload_hook_call} (#297).
|
|
42
50
|
#
|
|
43
51
|
# @param strategy [String] `"reload"` or `"redefine"`.
|
|
44
52
|
# @return [Boolean]
|
|
45
53
|
def unplaceable?(strategy)
|
|
46
|
-
strategy == "redefine" ? owner_unknown == true : hook_block == true
|
|
54
|
+
strategy == "redefine" ? owner_unknown == true : hook_block == true && reload_hook_call.nil?
|
|
47
55
|
end
|
|
48
56
|
|
|
49
57
|
# Returns the body location for the subject, if any.
|
|
@@ -13,7 +13,8 @@ module Mutineer
|
|
|
13
13
|
# first failing test.
|
|
14
14
|
# @param record_to [IO, nil] a `--matrix` run: send each test's outcome to
|
|
15
15
|
# this channel and run every test.
|
|
16
|
-
# @return [Integer] 0 on success, 1 on failure
|
|
16
|
+
# @return [Integer] 0 on success, 1 on failure, 2 when the first failure
|
|
17
|
+
# was an Active Record connection or lock error.
|
|
17
18
|
def self.run(test_files, stop_at_first_failure: false, record_to: nil)
|
|
18
19
|
MinitestIntegration.run(test_files, stop_at_first_failure: stop_at_first_failure, record_to: record_to)
|
|
19
20
|
end
|
|
@@ -8,8 +8,10 @@ module Mutineer
|
|
|
8
8
|
#
|
|
9
9
|
# Each runner responds to
|
|
10
10
|
# `.run(files, stop_at_first_failure: false, record_to: nil) -> 0/1/2`
|
|
11
|
-
# (0 = all passed, 1 = any failure, 2 =
|
|
12
|
-
# the RSpec runner reports
|
|
11
|
+
# (0 = all passed, 1 = any failure, 2 = no verdict: an error outside tests,
|
|
12
|
+
# which the RSpec runner reports, or a Minitest run whose first failure was
|
|
13
|
+
# an Active Record connection or lock error, #296) and is called only inside
|
|
14
|
+
# a forked child.
|
|
13
15
|
# Only the mutant path passes `stop_at_first_failure: true`; a `--matrix`
|
|
14
16
|
# mutant run passes `record_to:` instead (a {KillChannel} pipe) and runs every
|
|
15
17
|
# test. The fork boundary silences stdout (see ChildStdout); the runners do not.
|
data/lib/mutineer/triage.rb
CHANGED
|
@@ -92,7 +92,7 @@ module Mutineer
|
|
|
92
92
|
return nil
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
-
doc = JSON.parse(File.read(path))
|
|
95
|
+
doc = JSON.parse(File.read(path, encoding: "UTF-8"))
|
|
96
96
|
rows = doc.is_a?(Hash) ? doc["survivors"] : nil
|
|
97
97
|
unless rows.is_a?(Array)
|
|
98
98
|
err.puts "mutineer: triage report has no survivors list"
|
data/lib/mutineer/version.rb
CHANGED
data/lib/mutineer/worker_pool.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "result"
|
|
4
|
+
require_relative "redaction"
|
|
4
5
|
|
|
5
6
|
module Mutineer
|
|
6
7
|
# Fixed-size fork pool. `run` forks up to `size` children at once; each child
|
|
@@ -74,7 +75,7 @@ module Mutineer
|
|
|
74
75
|
begin
|
|
75
76
|
yield(*items[idx])
|
|
76
77
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
77
|
-
Result.error("worker crashed: #{
|
|
78
|
+
Result.error("worker crashed: #{Redaction.describe(e)}")
|
|
78
79
|
end
|
|
79
80
|
begin
|
|
80
81
|
wr.write(Marshal.dump(payload))
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mutineer
|
|
3
|
+
description: Mutation testing for Ruby (Prism + stdlib, zero deps). Mutates source one change at a time, runs the suite against each mutant, and reports the mutants tests fail to catch, with a versioned JSON contract and CI/agent gating. Use when checking whether a Ruby test suite would catch a change, finding weak or missing tests, gating CI on a mutation score, or acting on mutineer's JSON report.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Mutineer
|
|
7
|
+
|
|
8
|
+
Mutation testing for Ruby, built for CI gates and AI coding agents. Line coverage tells
|
|
9
|
+
you which code *ran*; mutation testing tells you whether your tests would *notice if it
|
|
10
|
+
broke*.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
gem install mutineer
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Run
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
mutineer run <source...> [--test <test>...] [options] # no --test: pairs each source with its test by convention; unmatched sources are skipped; --dry-run with --rails/--boot needs --test
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Example — mutate `lib/calculator.rb`, gate CI below 90%:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
mutineer run lib/calculator.rb --test test/calculator_test.rb --threshold 90
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Agent loop
|
|
31
|
+
|
|
32
|
+
1. Edit code and tests on a branch. Fetch `origin/main` before using it below.
|
|
33
|
+
2. Run diff-scoped, as JSON:
|
|
34
|
+
```sh
|
|
35
|
+
mutineer run app/ --since origin/main --threshold 90 --format json --output .mutineer/run.json
|
|
36
|
+
```
|
|
37
|
+
3. Parse `survivors[]` — each carries a `diff` and an `id`. File moves, renames,
|
|
38
|
+
project-root changes, and changes to repeated-name or repeated-mutation order can
|
|
39
|
+
change ids; see [Mutant ids](https://github.com/davidteren/mutineer/blob/main/docs/results.md#mutant-ids). Review each change
|
|
40
|
+
against the intended behaviour. If review finds a test gap, write or strengthen
|
|
41
|
+
a test that fails under that change.
|
|
42
|
+
4. Re-run. Accept the result only when this run exits `0`, `summary.score` is not `null`,
|
|
43
|
+
and `summary.score >= 90`. If every scored mutant must be killed, also require `summary.survived == 0`.
|
|
44
|
+
Mutineer 1.7.0 and earlier gate on the rounded score, so a run just below the threshold can
|
|
45
|
+
still exit `0`. On those versions, also require
|
|
46
|
+
`summary.killed * 100 >= 90 * (summary.killed + summary.survived)`.
|
|
47
|
+
|
|
48
|
+
Stop after a few unsuccessful attempts and ask a person to review the survivor.
|
|
49
|
+
Failed attempts do not prove that a mutant is equivalent. To
|
|
50
|
+
suppress it, `mutineer triage .mutineer/run.json --reason TEXT --id ID` prints an
|
|
51
|
+
`ignore:` entry for `.mutineer.yml` (Mutineer 1.7 or later reads it). See
|
|
52
|
+
[Suppressing equivalent mutants](https://github.com/davidteren/mutineer/blob/main/docs/results.md#suppressing-equivalent-mutants).
|
|
53
|
+
|
|
54
|
+
Keep `fail_fast` disabled (the default). If the active `.mutineer.yml` sets it to
|
|
55
|
+
`true`, temporarily change that value to `false` for the loop. Restore the original
|
|
56
|
+
value when the loop ends, before committing. A fail-fast run is partial,
|
|
57
|
+
and its score and exit code do not cover the full scope.
|
|
58
|
+
|
|
59
|
+
Zero survivors alone is not success: an empty or fully suppressed scope, or a run
|
|
60
|
+
with no usable verdicts, also has zero survivors. If the score is null, stop and
|
|
61
|
+
report “no score”. Check `no_coverage[]` for test gaps and `no_verdict[]` for
|
|
62
|
+
harness failures, and fix them before starting a new run. A positive threshold fails when nothing can be scored and something
|
|
63
|
+
broke, or when more than one mutant has no verdict and they exceed 10% of those
|
|
64
|
+
attempted; it does not require zero errors. Set the threshold to your target.
|
|
65
|
+
|
|
66
|
+
An empty run (`summary.total` is 0) exits 0 in 1.x and prints a warning on stderr. Treat it as "no score". Pass `--allow-empty` only when an empty run is expected. A `--since` run whose changes hold no mutable code says "nothing to test in the changes since REF"; that is a valid success. Warnings that start with `mutineer:` or `[mutineer]` also go to stderr: the empty-run warning, "did you mean" hints, notes about Mutineer 2.0, the count of errored mutants, and a notice that a `PARALLEL_WORKERS` value you set was replaced with 1.
|
|
67
|
+
|
|
68
|
+
## Exit codes
|
|
69
|
+
|
|
70
|
+
<!-- contract:exit-codes -->
|
|
71
|
+
| Code | Meaning |
|
|
72
|
+
|------|---------|
|
|
73
|
+
| `0` | Score ≥ threshold (or no gate) **and** no baseline regression. |
|
|
74
|
+
| `1` | Score below `--threshold`, OR nothing could be scored and something broke, or more than one mutant produced no verdict and they exceed 10% of those attempted, OR a `--baseline` regression, OR a runtime error, including a boot, source or `require:` file that raises while loading. |
|
|
75
|
+
| `2` | Usage / invalid-flag error, including a mistyped flag, a bad path (such as a missing `--boot` or `require:` file), or an unreadable baseline. |
|
|
76
|
+
<!-- /contract:exit-codes -->
|
|
77
|
+
|
|
78
|
+
## References
|
|
79
|
+
|
|
80
|
+
- Docs: https://davidteren.github.io/mutineer/
|
|
81
|
+
- Agent & CI guide: https://davidteren.github.io/mutineer/agentic-coding.html
|
|
82
|
+
- JSON schema: https://davidteren.github.io/mutineer/json-schema.html
|
|
83
|
+
- Ruby API: https://davidteren.github.io/mutineer/api/
|
|
84
|
+
- Source: https://github.com/davidteren/mutineer
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mutineer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Teren
|
|
@@ -72,6 +72,7 @@ files:
|
|
|
72
72
|
- lib/mutineer/kill_matrix.rb
|
|
73
73
|
- lib/mutineer/migrate.rb
|
|
74
74
|
- lib/mutineer/minitest_integration.rb
|
|
75
|
+
- lib/mutineer/minitest_integration/in_test_database_error.rb
|
|
75
76
|
- lib/mutineer/minitest_integration/kill_recorder.rb
|
|
76
77
|
- lib/mutineer/minitest_integration/outer_reporter.rb
|
|
77
78
|
- lib/mutineer/minitest_integration/stop_at_first_failure.rb
|
|
@@ -119,6 +120,7 @@ files:
|
|
|
119
120
|
- lib/mutineer/triage.rb
|
|
120
121
|
- lib/mutineer/version.rb
|
|
121
122
|
- lib/mutineer/worker_pool.rb
|
|
123
|
+
- skills/mutineer/SKILL.md
|
|
122
124
|
homepage: https://github.com/davidteren/mutineer
|
|
123
125
|
licenses:
|
|
124
126
|
- MIT
|
|
@@ -128,6 +130,8 @@ metadata:
|
|
|
128
130
|
bug_tracker_uri: https://github.com/davidteren/mutineer/issues
|
|
129
131
|
documentation_uri: https://davidteren.github.io/mutineer/
|
|
130
132
|
rubygems_mfa_required: 'true'
|
|
133
|
+
hyperdrive_targets: "*"
|
|
134
|
+
hyperdrive_artifacts: skill
|
|
131
135
|
rdoc_options: []
|
|
132
136
|
require_paths:
|
|
133
137
|
- lib
|