saboteur 0.2.0 → 0.4.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +153 -1
  3. data/README.md +526 -73
  4. data/lib/generators/saboteur/install/install_generator.rb +21 -0
  5. data/lib/saboteur/agent_skill.rb +40 -0
  6. data/lib/saboteur/baseline_health.rb +116 -0
  7. data/lib/saboteur/cli.rb +305 -30
  8. data/lib/saboteur/config.rb +65 -19
  9. data/lib/saboteur/config_file.rb +97 -0
  10. data/lib/saboteur/custom_map.rb +170 -0
  11. data/lib/saboteur/doctor.rb +88 -0
  12. data/lib/saboteur/engine/baseline_guard.rb +116 -0
  13. data/lib/saboteur/engine/boot.rb +9 -0
  14. data/lib/saboteur/engine/candidate_selection.rb +35 -0
  15. data/lib/saboteur/engine/coverage_store.rb +40 -6
  16. data/lib/saboteur/engine/data_inputs.rb +43 -0
  17. data/lib/saboteur/engine/diagnostics.rb +28 -0
  18. data/lib/saboteur/engine/diff_scope.rb +8 -0
  19. data/lib/saboteur/engine/distribution.rb +4 -2
  20. data/lib/saboteur/engine/run_fingerprint.rb +344 -0
  21. data/lib/saboteur/engine/scheduler.rb +7 -16
  22. data/lib/saboteur/engine/suite_files.rb +8 -1
  23. data/lib/saboteur/engine/verdict_check.rb +4 -2
  24. data/lib/saboteur/engine.rb +125 -25
  25. data/lib/saboteur/execution/fork_readiness.rb +46 -0
  26. data/lib/saboteur/execution/reevaluator.rb +7 -4
  27. data/lib/saboteur/execution/supervisor.rb +23 -11
  28. data/lib/saboteur/frameworks/rails.rb +329 -13
  29. data/lib/saboteur/help.rb +210 -0
  30. data/lib/saboteur/ignore_list.rb +71 -17
  31. data/lib/saboteur/install.rb +132 -0
  32. data/lib/saboteur/integration/base.rb +8 -0
  33. data/lib/saboteur/integration/rspec.rb +69 -4
  34. data/lib/saboteur/mutation_source/argument_removal.rb +86 -0
  35. data/lib/saboteur/mutation_source/builtin.rb +14 -3
  36. data/lib/saboteur/mutation_source/filter_replacement.rb +60 -0
  37. data/lib/saboteur/mutation_source/literal_replacement.rb +74 -0
  38. data/lib/saboteur/mutation_source/pair_removal.rb +101 -0
  39. data/lib/saboteur/replay.rb +33 -0
  40. data/lib/saboteur/reporter/agent.rb +87 -0
  41. data/lib/saboteur/reporter/agent_clipping.rb +29 -0
  42. data/lib/saboteur/reporter/agent_commands.rb +61 -0
  43. data/lib/saboteur/reporter/agent_incomplete.rb +37 -0
  44. data/lib/saboteur/reporter/agent_summary.rb +131 -0
  45. data/lib/saboteur/reporter/findings.rb +131 -0
  46. data/lib/saboteur/reporter/html.rb +213 -0
  47. data/lib/saboteur/reporter/replay.rb +23 -0
  48. data/lib/saboteur/reporter/stryker.rb +21 -4
  49. data/lib/saboteur/reporter/terminal.rb +76 -2
  50. data/lib/saboteur/result.rb +61 -9
  51. data/lib/saboteur/score.rb +89 -0
  52. data/lib/saboteur/version.rb +1 -1
  53. data/lib/saboteur/working_tree.rb +48 -0
  54. data/lib/saboteur.rb +7 -0
  55. metadata +30 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6007efb7a83e5669822a1622876457e9e654187acc923981e641835258c7637d
4
- data.tar.gz: 373680f8e52d7cbd24f1eb3dde2322101eccf565c2752e10a174bd5ff9e6d8e2
3
+ metadata.gz: a5c52cb7485073b569359bf940160095370103747d55a6065b7f067726a1a53c
4
+ data.tar.gz: 40fca0c1c0f8391de86d7f17960d908add226682d93be2610775fc888ab36f60
5
5
  SHA512:
6
- metadata.gz: c0d37ea099ca68c5dc3608e2229446cb4e4494c623e63bb8033eb85a8a10bd7953e2cc84031678a15adda5da0b0da1f6bb1177badde04fa06dbd472f2d674534
7
- data.tar.gz: 7484ed59697969071030c78ab81b8a716da93b09d4e95900b69b110f087809a6e1293153375104732457205af0e0cfb94f64eb34810242febd1e63be613fb97a
6
+ metadata.gz: 061e0b5a97c95048048767d3724ea883ea144e7530b9a52631d52c896f4154e1a63f7b65ef7b5817053a42ab31eb97c168aa3079c87e9d9a3e8be1ae949d7bdf
7
+ data.tar.gz: e29bcfafdaf2fc7ecffbc4f8746349d79c908dfc2de1530998b525c691135c2bfb4ade67575374178742b1fddb29ab96a7d9ff67656d29dbd7c096ba0b50d0dc
data/CHANGELOG.md CHANGED
@@ -6,6 +6,158 @@ All notable changes are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.4.0] - 2026-10-12
10
+
11
+ ### Changed
12
+ - **Breaking:** configuration moved from YAML to Ruby. `.saboteur.yml` becomes
13
+ `config/saboteur.rb` (`Saboteur.configure do |c| ... end`, same key names),
14
+ and `.saboteur-ignore.yml` becomes `config/saboteur_ignore.rb`
15
+ (`Saboteur.ignore do |i| i.mutation "<digest>", subject:, reason: end`,
16
+ reason still mandatory). A legacy YAML file stops the run with a pointer to
17
+ the new file instead of being read. Unknown settings fail with the line
18
+ number and a did-you-mean suggestion. `rails generate saboteur:install`
19
+ writes `config/saboteur.rb` (never overwriting without `--force`);
20
+ `config/saboteur_ignore.rb` and `config/saboteur_custom_map.rb` are optional
21
+ and created by hand when needed.
22
+ - A run in which more than half of the executed mutants errored now exits 3
23
+ (the tool failed). Before, only a run where every executed mutant errored
24
+ did. Errored mutants are outside the score, so such a run tested almost
25
+ nothing. This applies with and without `--threshold`. The JSON report records the reason under
26
+ `saboteur.exitCause` (`ok`, `survivors`, `belowThreshold`, `errorMajority`,
27
+ `incompleteUnderThreshold`; in `--agent` mode `ok`, `survivors`,
28
+ `errorMajority` or `incomplete`).
29
+ - A whole-suite coverage build that records no lines for any included file
30
+ now exits 3 and the empty map is not cached, instead of testing nothing.
31
+ With `--test`, an empty map stays a legitimate "no coverage" outcome.
32
+ - Rails fixtures (`spec/fixtures`, `test/fixtures`), `db/structure.sql`,
33
+ `db/schema.rb` and `config/*.yml` are part of the coverage and verdict
34
+ cache fingerprint, so a changed fixture or schema invalidates cached
35
+ results. The new `fingerprint_extra` setting adds other globs whose
36
+ contents should invalidate caches, for example `data/*.csv`.
37
+ - The baseline and coverage phases show progress and write real logs, so a
38
+ slow or failing phase can be diagnosed from the output.
39
+ - When the baseline child is killed by a signal, the message names it
40
+ (`SIGKILL` adds a possible-OOM hint) instead of reporting a bare failure.
41
+
42
+ ### Removed
43
+ - The coarse-survivor gate: a surviving whole-body mutant no longer skips the
44
+ method's fine mutants; every eligible fine mutant runs.
45
+
46
+ ### Fixed
47
+ - Mutations inside private `module_function` helpers are preserved when the
48
+ method is re-evaluated; they previously reverted and reported as false
49
+ survivors.
50
+ - Rails apps that test against in-memory SQLite (`:memory:`) no longer fail
51
+ in forked workers with "Could not find table": saboteur never disconnects,
52
+ renames or copies an in-memory pool. Each in-memory database is
53
+ snapshotted to a temporary file after boot, and every forked child
54
+ restores the snapshot into its own fresh database.
55
+ - With a Rails app under Bootsnap, Coverage no longer sees an empty map:
56
+ saboteur sets `DISABLE_BOOTSNAP_COMPILE_CACHE=1` for its own process when
57
+ the variable is unset, because bytecode from Bootsnap's compile cache
58
+ carries no coverage counters.
59
+ - A relative SQLite `database:` path resolves against `Rails.root` (as Rails
60
+ 7.1+ does) for the per-worker copy and its cleanup. The copy uses SQLite's
61
+ backup API after a WAL checkpoint, refuses to run when a reader keeps the
62
+ WAL busy, and a missing source file is an error rather than a skipped copy.
63
+ - Per-worker database copies (`jobs` above 1) are dropped when the run ends,
64
+ best effort: a failed drop is a warning and never changes the outcome. A
65
+ copy left by a killed run is reclaimed by the next run. A failed copy
66
+ names the sessions blocking it.
67
+ - For `rails: true`, the app's `spec/` directory is on the load path before
68
+ `require` is processed, as with rspec, so a boot file such as a dummy app's
69
+ `spec/rails_helper.rb` can require its siblings.
70
+ - When Rails did not load, the error points engine users at the dummy app's
71
+ environment file or helper (`DummyApp.setup` in `spec/rails_helper.rb`).
72
+
73
+ ### Added
74
+ - `rails generate saboteur:install` and a Rails quickstart in the README. The
75
+ generator writes `config/saboteur.rb` and never overwrites without `--force`.
76
+ - The warning about threads alive when a worker forks names those threads,
77
+ falling back to their backtraces for unnamed ones.
78
+ - `--test` no longer requires source operands when `--since` is given: the
79
+ changed files are the sources, so `saboteur run --since origin/main --test
80
+ spec/billing_spec.rb` scopes a pull-request run to chosen specs. A `--since`
81
+ run without `--test` now warns before it starts that the two baseline passes
82
+ run the loaded suite (a cold coverage cache adds one more pass), with the
83
+ spec-file count and how to scope them, instead of going quiet for hours on a
84
+ large app.
85
+ - `config/saboteur_custom_map.rb` declares explicit source-file to spec-file
86
+ mappings (`SABOTEUR_CUSTOM_MAP`): exact paths, globs (the more specific glob
87
+ wins; equal specificity is an error) and Regexps with capture substitution.
88
+ Invalid maps fail the run with the file and key; stale and duplicate keys
89
+ warn on runs, and `saboteur doctor` fails on stale keys. The map is parsed
90
+ and validated only for now - it does not change which specs run yet.
91
+
92
+ - An integration target can declare a known error profile
93
+ (`allow_error_fraction` per strategy in integration/targets.yml): the
94
+ majority-error exit is accepted only when the run's own fresh report
95
+ carries the CLI's `errorMajority` marker, the errored fraction stays
96
+ within the bound, and no mutant hit a resource limit. The JSON report
97
+ records the run's exit cause under `saboteur.exitCause`.
98
+ - A survivor lists the first five tests that covered its line and still passed
99
+ with the mutant applied, in the terminal and the HTML page.
100
+ - In diff mode (`--since`), each survivor names the changed line it relates to
101
+ (`changed line: path:N` in the terminal, `changedLines` in each JSON finding),
102
+ because a multi-line statement is reported at its first line.
103
+ - The score is split into assertion kills and crash kills, in the terminal,
104
+ the JSON report (`saboteur.scores`) and the HTML page. A second score leaves
105
+ out whole-body mutants (`body_nil`, `body_true`, `body_false`, `body_raise`). The headline score and
106
+ `--threshold` are unchanged.
107
+ - `saboteur run --agent` (`--format agent`): one bounded JSON summary (under 8 KiB) on stdout
108
+ with status, verdict, provenance, the first three findings and the commands for full
109
+ detail. Exit 0 means a complete, clean run only: 1 survivors, 2 usage or config error,
110
+ 3 incomplete or failed. `--threshold` cannot be combined with it. The next commands are shell-escaped and keep the
111
+ full selection; the working-tree state is read before the run; target output stays on stderr. `saboteur agent-skill`
112
+ prints an AGENTS.md block for coding agents (also in `docs/agent-skill.md`). Classic
113
+ exit codes are unchanged.
114
+ - Fine `pair_removal` mutants for hash literals and keyword lists, including
115
+ sole associations, with token-based separators and the normal method cap.
116
+ - Fine `argument_removal` mutants: remove one ordinary argument from a
117
+ parenthesized call while preserving keywords and blocks. Token-based comma
118
+ edits respect comments and strings. Existing candidate validation, order and
119
+ per-method caps apply. Unparenthesized calls and heredoc arguments are skipped.
120
+
121
+ ## [0.3.0] - 2026-10-11
122
+
123
+ ### Added
124
+ - Fine `literal_boolean` and `literal_string` mutants: true/false swaps and
125
+ nonempty static string replacement with `""`. Body-only traversal excludes
126
+ arid/nested scopes, interpolation, heredocs and non-string text literals.
127
+ New proposals can change scores and per-method-cap coverage.
128
+ - `saboteur run --id DIGEST`: replay one exact current mutant with selected tests,
129
+ independent of caps, coarse gating, ignore suppression and incremental reuse.
130
+ Prints the mutation diff and selected test ids for every replay result.
131
+ - Review findings in the JSON report (`saboteur.findings`,
132
+ `Saboteur::Reporter::Findings`): a bounded, deterministically ordered list
133
+ of survivors (coarse first, then file, line and digest) with the file,
134
+ line, digest, subject, operator, original and replacement a note on a
135
+ pull-request diff needs. Survivors accepted on the ignore list are listed
136
+ separately with their reason; `survivorsTruncated` counts the rest, which
137
+ stay in the full `files` data.
138
+ - Self-contained HTML report (`Saboteur::Reporter::Html`): one file with inline
139
+ style and no script or network use, written to `.saboteur/report.html`.
140
+ Change the path with `--html PATH` or `html_report:`. It shows the score,
141
+ per-file mutants, survivors, warnings and skipped subjects.
142
+ - A GitHub Action (`uses: vamsipavanmahesh/saboteur`) that runs diff mode on pull
143
+ requests. Advisory by default, with a job summary, outputs and a cached
144
+ coverage map. See `docs/github-action.md`.
145
+ - Built-in `filter_replacement` mutants: `select` / `reject` and
146
+ `select!` / `reject!`, for explicit-receiver calls with a block and no ordinary
147
+ arguments. For unchanged subjects, existing mutants retain their digests and order.
148
+ Runs may now include more mutants and report a different score.
149
+ - `saboteur doctor [--config PATH] [--seed N]`: environment checks and two fresh
150
+ unchanged-suite checks, without mutation reports or coverage/verdict cache writes.
151
+ - `saboteur run SOURCE... --test TEST...`: mutate the named source files and
152
+ run only the named spec files or directories. Positional sources replace
153
+ `include` and `exclude`; `--test` replaces the spec paths in `rspec_args`.
154
+ - `--threshold PERCENT` (and `threshold:` in `.saboteur.yml`): exit 1 when the
155
+ score is below PERCENT. The score decides the exit code instead of survivors.
156
+ It fails closed: no score exits 1, and errored or resource-limited mutants
157
+ exit 3.
158
+ - Positional sources must exist (exit 2 otherwise), and the selected tests are
159
+ part of the coverage-map and `--incremental` cache keys.
160
+
9
161
  ## [0.2.0] - 2026-10-10
10
162
 
11
163
  ### Added
@@ -24,7 +176,7 @@ First release that does mutation testing. MIT licensed.
24
176
  ### Added
25
177
  - `saboteur run` with `--since`, `--subject`, `--timeout-factor`, `--jobs`,
26
178
  `--reeval`, `--json`, `--no-cache`, `--incremental`, `--config`.
27
- - Subjects found with Prism; mutations are byte-range edits: whole-method
179
+ - Subjects found with Prism; mutations are byte-range edits: whole-body
28
180
  `nil`/`true`/`false`/`raise` first, then comparison, boolean, arithmetic,
29
181
  numeric literal, statement removal, condition, negation, safe navigation
30
182
  and `||=`.