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