saboteur 0.4.0 → 0.5.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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +167 -0
  3. data/README.md +393 -20
  4. data/exe/saboteur +6 -2
  5. data/lib/saboteur/agent_skill.rb +6 -0
  6. data/lib/saboteur/baseline/acceptance.rb +218 -0
  7. data/lib/saboteur/baseline/command.rb +80 -0
  8. data/lib/saboteur/baseline/comparison.rb +105 -0
  9. data/lib/saboteur/baseline/completeness.rb +53 -0
  10. data/lib/saboteur/baseline/evidence.rb +114 -0
  11. data/lib/saboteur/baseline/identity.rb +78 -0
  12. data/lib/saboteur/baseline/manifest.rb +164 -0
  13. data/lib/saboteur/baseline/run_evidence.rb +117 -0
  14. data/lib/saboteur/baseline/workflow.rb +121 -0
  15. data/lib/saboteur/baseline_health.rb +1 -1
  16. data/lib/saboteur/cli/exit_status.rb +93 -0
  17. data/lib/saboteur/cli/run_reporting.rb +75 -0
  18. data/lib/saboteur/cli.rb +101 -121
  19. data/lib/saboteur/config.rb +76 -6
  20. data/lib/saboteur/doctor.rb +40 -8
  21. data/lib/saboteur/engine/baseline_guard.rb +33 -11
  22. data/lib/saboteur/engine/bytecode_equivalence.rb +95 -0
  23. data/lib/saboteur/engine/canary.rb +74 -0
  24. data/lib/saboteur/engine/candidate_selection.rb +18 -5
  25. data/lib/saboteur/engine/candidates.rb +59 -7
  26. data/lib/saboteur/engine/coverage_store.rb +34 -14
  27. data/lib/saboteur/engine/diagnostics.rb +150 -4
  28. data/lib/saboteur/engine/diff_scope.rb +10 -0
  29. data/lib/saboteur/engine/distribution.rb +54 -13
  30. data/lib/saboteur/engine/result_cache.rb +10 -5
  31. data/lib/saboteur/engine/run_fingerprint.rb +6 -4
  32. data/lib/saboteur/engine/sandbox_policy.rb +108 -0
  33. data/lib/saboteur/engine/scheduler.rb +92 -20
  34. data/lib/saboteur/engine/subject_selection.rb +24 -1
  35. data/lib/saboteur/engine/test_guess.rb +52 -0
  36. data/lib/saboteur/engine/unchanged_syntax.rb +87 -0
  37. data/lib/saboteur/engine/verdict_check.rb +7 -6
  38. data/lib/saboteur/engine.rb +192 -18
  39. data/lib/saboteur/execution/protocol.rb +2 -1
  40. data/lib/saboteur/execution/reevaluator.rb +72 -10
  41. data/lib/saboteur/execution/runner.rb +120 -17
  42. data/lib/saboteur/execution/sandbox.rb +271 -0
  43. data/lib/saboteur/execution/stale_captures.rb +43 -0
  44. data/lib/saboteur/execution/supervisor.rb +101 -9
  45. data/lib/saboteur/execution/worker_pool.rb +20 -4
  46. data/lib/saboteur/execution/wrapper.rb +45 -0
  47. data/lib/saboteur/gates.rb +83 -0
  48. data/lib/saboteur/help.rb +31 -7
  49. data/lib/saboteur/ignore_list.rb +5 -0
  50. data/lib/saboteur/inline_suppression.rb +65 -0
  51. data/lib/saboteur/integration/base.rb +1 -0
  52. data/lib/saboteur/integration/focus_guard.rb +83 -0
  53. data/lib/saboteur/integration/rspec.rb +21 -1
  54. data/lib/saboteur/mutation_source/builtin.rb +22 -3
  55. data/lib/saboteur/mutation_source/chain_removal.rb +80 -0
  56. data/lib/saboteur/mutation_source/condition_negation.rb +50 -0
  57. data/lib/saboteur/mutation_source/endpoint_replacement.rb +61 -0
  58. data/lib/saboteur/mutation_source/hazard_guard.rb +136 -0
  59. data/lib/saboteur/mutation_source/operator_assignment.rb +52 -0
  60. data/lib/saboteur/mutation_source/operator_registry.rb +57 -0
  61. data/lib/saboteur/mutation_source/range_replacement.rb +43 -0
  62. data/lib/saboteur/preview.rb +106 -0
  63. data/lib/saboteur/reporter/agent_commands.rb +7 -1
  64. data/lib/saboteur/reporter/agent_incomplete.rb +24 -1
  65. data/lib/saboteur/reporter/agent_summary.rb +11 -5
  66. data/lib/saboteur/reporter/findings.rb +4 -19
  67. data/lib/saboteur/reporter/history_paths.rb +38 -0
  68. data/lib/saboteur/reporter/history_record.rb +132 -0
  69. data/lib/saboteur/reporter/html.rb +12 -1
  70. data/lib/saboteur/reporter/mutation_text.rb +36 -0
  71. data/lib/saboteur/reporter/next_steps.rb +30 -0
  72. data/lib/saboteur/reporter/palette.rb +37 -0
  73. data/lib/saboteur/reporter/score_history.rb +123 -0
  74. data/lib/saboteur/reporter/score_table.rb +57 -0
  75. data/lib/saboteur/reporter/stryker.rb +15 -2
  76. data/lib/saboteur/reporter/survivor_hints.rb +68 -0
  77. data/lib/saboteur/reporter/survivor_list.rb +126 -0
  78. data/lib/saboteur/reporter/survivor_ranking.rb +34 -0
  79. data/lib/saboteur/reporter/terminal.rb +131 -79
  80. data/lib/saboteur/reporter/test_matrix.rb +57 -0
  81. data/lib/saboteur/result.rb +41 -6
  82. data/lib/saboteur/selection/coverage_map.rb +119 -13
  83. data/lib/saboteur/selection/git_diff.rb +28 -1
  84. data/lib/saboteur/selection/recorder.rb +55 -13
  85. data/lib/saboteur/subject.rb +40 -4
  86. data/lib/saboteur/subject_finder.rb +62 -12
  87. data/lib/saboteur/version.rb +1 -1
  88. data/lib/saboteur/working_tree.rb +8 -0
  89. data/lib/saboteur.rb +6 -0
  90. metadata +43 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a5c52cb7485073b569359bf940160095370103747d55a6065b7f067726a1a53c
4
- data.tar.gz: 40fca0c1c0f8391de86d7f17960d908add226682d93be2610775fc888ab36f60
3
+ metadata.gz: a9d8e69906c1a27f726dea87fb72eae3a92a6dfc4e3d10a5fbe903c0f8cd4e38
4
+ data.tar.gz: f6c85551dd523d8926c3ac05313f1dc0edea86aa84d321a8b4e05d7f84ca4706
5
5
  SHA512:
6
- metadata.gz: 061e0b5a97c95048048767d3724ea883ea144e7530b9a52631d52c896f4154e1a63f7b65ef7b5817053a42ab31eb97c168aa3079c87e9d9a3e8be1ae949d7bdf
7
- data.tar.gz: e29bcfafdaf2fc7ecffbc4f8746349d79c908dfc2de1530998b525c691135c2bfb4ade67575374178742b1fddb29ab96a7d9ff67656d29dbd7c096ba0b50d0dc
6
+ metadata.gz: 8b4bddc7c4b7b621082a6ad986e49e3e4a113ae1100f4cfb18f6a649d6ac8f852da486f110bcb61da0af3d8f588c172970e1526454d857d7ca6fb1411bde2c48
7
+ data.tar.gz: 4b8ab1f9f45dd75506b7e5a388a85af5a2fa46cbe6b70bc885ef1dc615988e4ef01ca4815fe67b2f8fd62cb5aa07511bf733da8e4f21775e6325080f478f2f06
data/CHANGELOG.md CHANGED
@@ -6,9 +6,176 @@ All notable changes are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.0] - 2026-10-14
10
+
11
+ ### Added
12
+ - Opt-in extended `range`, `operator_assignment` and `condition_negation` operators (#135). They run only
13
+ when selected with `--operators`; the default core set does gain the new `chain_removal`
14
+ operator (below).
15
+ - `whole_file` config: an opt-in list of subject ids or globs (for example
16
+ `"Point#distance"`, `"Geometry::*"`). For those subjects the child evaluates the
17
+ whole mutated file instead of re-evaluating the one method, so methods
18
+ surgical re-evaluation refuses can be mutated. It applies to methods already
19
+ found as subjects. The file's top-level code runs again in the child: load-time
20
+ side effects repeat and constants it assigns are rebuilt. A surviving
21
+ mutant whose definition a later definition in the file replaces is reported as
22
+ unreachable, not survived. Off by default (Refs #138).
23
+ - `--matrix` (#132) runs every covering test per mutant and lists blind and
24
+ redundant tests after the survivors.
25
+ - The 30-second heartbeat of the baseline and coverage phases now shows the examples finished so far, the percentage and a rough time left, read from the phase log, instead of only the elapsed time. (#56)
26
+ - Each finding in `--agent` output carries a `testHint` (`kind`, `text`, and for a
27
+ comparison swap that differs only for equal or unequal operands, a `case`
28
+ such as `left == right`) saying where a test could look (#154). It is a
29
+ suggestion, not a diagnosis: a survivor can also be an equivalent mutant.
30
+ - `--max-timeouts N` and `--max-errors N` CI gates (count or percent of executed
31
+ mutants). Exceeding one exits 1 with `exitCause` `maxTimeouts` or `maxErrors`.
32
+ The covered score is already gated by `--threshold`. No Action input yet.
33
+ - A run that leaves a live survivor ends with a "Next steps" footer of command
34
+ templates: replay (`saboteur run --id DIGEST --seed N`, to be combined with the
35
+ run's own selection options), an ignore entry with the survivor's subject, and
36
+ `saboteur run --agent` (#147).
37
+ - **Safety:** mutant children now run under a Landlock sandbox on Linux
38
+ 6.2+ (Landlock ABI 3) (#152): the child cannot create, write, rename,
39
+ unlink or truncate files in the project tree except `tmp/`, the cache dir
40
+ and the system temp dir, and inherited descriptors already open for
41
+ writing elsewhere are closed before the sandbox engages. This is the
42
+ third layer behind the hazard guard and the canary, and the first one
43
+ that stops a destructive call before it acts. Landlock covers the
44
+ filesystem only. On kernels 5.13-6.1 (ABI 1-2) `truncate()` cannot be
45
+ restricted, and on kernels without Landlock there is no sandbox at all:
46
+ both run with the guard and canary only, noted once. `c.sandbox = false`
47
+ opts out (`/dev/shm` and `/dev/tty` are denied like any other
48
+ ungranted path; `/dev/null` stays writable); `c.sandbox = "strict"`
49
+ refuses to run degraded;
50
+ `c.sandbox_writable` grants extra directories (suites writing `db/`,
51
+ `log/`, ... otherwise fail with EACCES in mutant children) and warns when
52
+ a grant contains the project root or another grant. The root check covers
53
+ the effective write set - every grant realpath'd, the cache dir and the
54
+ system temp dir included - so `cache_dir = "."`, a symlinked `tmp`, a
55
+ `/` grant or a checkout inside the system temp dir cannot silently leave
56
+ the tree writable; `c.sandbox = "strict"` refuses to run then. A kernel probe that
57
+ errors (rather than cleanly reporting Landlock absent) refuses the run.
58
+ The descriptor sweep also closes stdio redirected to a writable regular
59
+ file (terminal, pipe and `/dev/null` stdio stays), inherited files
60
+ already deleted from the tree, and any descriptor it cannot inspect -
61
+ fail closed. The C library lookup tolerates musl (soname fallbacks, then
62
+ the process symbol table) and degrades to the one-time unsandboxed note
63
+ when no libc resolves, instead of crashing.
64
+ - Audited inline suppression comments with required reasons (#133). Inspired by mutineer
65
+ (github.com/davidteren/mutineer, MIT, by David Teren).
66
+ - `--dry-run` previews planned mutants per method and the max_mutants_per_method
67
+ omissions without booting the app, running tests, forking or writing reports
68
+ (#134). A file that does not parse marks the total INCOMPLETE. A cached
69
+ estimate is baseline covering-test work, not a bound on mutant runtime.
70
+ Custom sources are still required during a dry run. `--list-operators` and
71
+ `--operators` list and select builtin operators. Inspired by mutineer
72
+ (github.com/davidteren/mutineer, MIT, by David Teren).
73
+ - [docs/minitest.md](docs/minitest.md) records that Minitest is unsupported and how much
74
+ of the engine depends on RSpec (#200).
75
+ - [docs/scores.md](docs/scores.md) defines the covered score, the overall score a
76
+ Stryker dashboard shows, assertion and crash kills, and what makes a run
77
+ incomplete (#198).
78
+ - A startup warning on stderr when the working tree has uncommitted changes,
79
+ because mutants run your code and could alter or delete files. Saboteur's own
80
+ output paths do not count as changes.
81
+ - Terminal report rewrite: survivors grouped by file and method with
82
+ evidence-scoped hints (suggestions, not diagnoses), a score table, a
83
+ progress counter with ETA, `--verbose`, `--[no-]color` and `--fail-fast`
84
+ (serial only; the run is marked incomplete with exit cause `failFast` and
85
+ lists the subjects it never reached). The CI progress line-break can
86
+ misalign by a mark when parallel workers finish at the same moment.
87
+ - Sorbet signature text does not prune nil-producing mutants. Runtime enforcement
88
+ is not verified: checked levels, custom handlers and shims can allow nil to
89
+ reach assertions. Type-aware pruning (#153) is deferred until enforcement can
90
+ be established without guessing from return-type text.
91
+ - `endpoint_replacement` swaps explicit no-argument `first` and `last` calls,
92
+ including safe navigation. Calls with arguments or blocks, bare calls,
93
+ arid statements and nested scopes are excluded.
94
+ - The `chain_removal` operator drops one link from the middle of a method chain:
95
+ `a.b.c` becomes `a.c`. The first receiver and the last call stay, so the result
96
+ is still a call on the original object. It ships in the default core set; links
97
+ that hand back their receiver (`freeze`, `dup`, `clone`, `itself`, `tap`) and
98
+ chains that are assignment targets are left alone (#183).
99
+ - `saboteur baseline check|propose|accept --baseline PATH`: record today's
100
+ survivors as a baseline file and fail only on new ones, so an existing project
101
+ adopts mutation testing without a cleanup week. `check --epsilon POINTS` also
102
+ fails when the score drops more than POINTS percentage points below the
103
+ baseline's, and the failure detail prints inline. The GitHub Action exposes the
104
+ drop as the `baseline-score-drop` output (#131, #148).
105
+ - `--guess-tests` scopes a `--since` run to the specs named after the changed
106
+ files, for suites where full selection costs too much. A spec that reaches a
107
+ changed file only through another file does not run, so a mutant only it
108
+ would kill can survive, and a line no guessed spec executes is reported as
109
+ NoCoverage. A changed source file with no matching spec (or no changed source
110
+ at all) falls back to the whole suite with a warning. It cannot be combined
111
+ with `--test` (#185).
112
+ - `--allow-focus` (#108): without it, a suite narrowed by a stray `fit`,
113
+ `fdescribe` or `:focus` filter stops the run, because mutation testing a
114
+ focused slice would report a score for the wrong suite.
115
+ - Survivor findings are ranked in the JSON and `--agent` findings: coarse
116
+ mutants first, then more covering tests, a larger edited-line span and nearer
117
+ changed lines (#191). The terminal report keeps its file/method/line groups,
118
+ with the ranking applied within each live or accepted line group.
119
+ - `--score-history PATH` appends each run's scores to a JSONL file. Opt-in (#145).
120
+ - [docs/guides/reading-survivors.md](docs/guides/reading-survivors.md) is a guide
121
+ to reading survivors, and [docs/recipes/README.md](docs/recipes/README.md)
122
+ collects setups for factories, fixtures, DatabaseCleaner, Capybara and test
123
+ adapters.
124
+ ### Changed
125
+ - The text run prints one `Reports:` line naming the JSON and HTML reports, with
126
+ paths relative to the project root when they are under it, instead of two
127
+ lines with a blank line between them (#150). Scripts that read the old
128
+ `JSON report:` and `HTML report:` lines must read `Reports:` instead.
129
+ - `saboteur doctor` checks the environment only by default (Ruby version,
130
+ coverage setup and SimpleCov conflicts, boot files, the custom map and ignore
131
+ subjects). The two unchanged baseline suite passes are slow on a large suite
132
+ and now run only with `doctor --suite` (or `--test`, which implies it) (#53).
133
+
134
+ ### Fixed
135
+ - `--since` with a ref that is not a commit exits 2 as a usage error, naming the
136
+ ref, before the target boots. It used to exit 3 (tool failure) after boot (#115).
137
+ - Diff mode skips a method whose only changes are comments, blank lines or
138
+ spacing, because its syntax tree matches the merge base. The run prints how many
139
+ it skipped. A method that uses `__LINE__` is compared by the line it stands
140
+ for, so moving it or adding a line above that use keeps it in the run.
141
+ Ambiguous cases (a new file or method, a name defined twice, a base
142
+ that does not parse) still run (#115).
143
+ - The default arid logger pattern also matches `@logger`, `self.logger` and
144
+ `::Rails.logger`, so those calls no longer produce noise survivors (#111).
145
+ - The unscoped `--since` warning now says what `--test` drops: the consumer specs of a changed method no longer run, which can turn kills into survivors. (#113)
146
+ - **Safety:** mutants that would leave a directly-written destructive call
147
+ running with altered inputs are suppressed (#102). A syntactic hazard
148
+ guard matches calls written directly - `FileUtils.rm*`/`remove_entry*`
149
+ (bare names included), `File.delete`/`unlink`, `Dir.rmdir`/`delete`/
150
+ `unlink`, `rmtree`, `system`/`exec`/`spawn` (bare or on `Kernel`/
151
+ `Process`), backticks/`%x`, `IO.popen`, `Open3.*`, piped `open` - and
152
+ suppresses mutants inside such a call or elsewhere in a method that still
153
+ holds one. Calls behind aliases, variables or `send` are not recognized.
154
+ A canary re-checks the tracked tree after each method's mutants and aborts
155
+ with exit 3 when a file vanishes: detection after the damage, per method,
156
+ with no restoration. Configure the list via `c.hazard`; `--allow-hazardous`
157
+ opts out.
158
+ - Methods defined inside blocks (`Class.new do`, `Data.define do`, `included do`) are listed as
159
+ skipped ("defined inside a block") instead of being silently absent. They are not mutated yet. `X = Data.define` / `Struct.new` / `Class.new` / `Module.new` with a block name X as the owner; any other block keeps the enclosing owner or "(anonymous block)". (#114)
160
+ - Endless defs (`def x = ...`) are no longer skipped as "no coverage": the
161
+ coverage map now also records per-test method calls, which is how Ruby
162
+ reports endless-def bodies. A whole-suite build that records neither a line
163
+ hit nor a method call still stops with a clearer cause message. (#107)
164
+ - Mutants that compile to the same bytecode as the original method are dropped
165
+ before they run, instead of surviving as untestable noise (#151).
166
+ - Mutants on branch arms that no covering test takes are skipped, using per-test
167
+ branch coverage, instead of surviving as findings no test could kill (#184).
168
+ - Diff mode lists every survivor on a changed line instead of only one (#146).
169
+ - Methods with heredoc bodies select all of the method's tests again, and edits
170
+ to pattern-matching arms map to the line that executes, so `--since` selects
171
+ their killers (#130, #110).
172
+ - The empty-coverage error names Bootsnap only when Bootsnap is loaded (#149).
173
+
9
174
  ## [0.4.0] - 2026-10-12
10
175
 
11
176
  ### Changed
177
+ - The terminal report lists every survivor on a changed line in `--since` mode, instead of one
178
+ plus "N more survivors on this line in the JSON report" (#146).
12
179
  - **Breaking:** configuration moved from YAML to Ruby. `.saboteur.yml` becomes
13
180
  `config/saboteur.rb` (`Saboteur.configure do |c| ... end`, same key names),
14
181
  and `.saboteur-ignore.yml` becomes `config/saboteur_ignore.rb`