evilution 0.28.0 → 0.30.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/.beads/interactions.jsonl +106 -0
- data/.rubocop_todo.yml +7 -0
- data/CHANGELOG.md +49 -0
- data/README.md +194 -8
- data/docs/versioning.md +53 -0
- data/lib/evilution/ast/constant_names.rb +28 -11
- data/lib/evilution/ast/heredoc_span.rb +99 -0
- data/lib/evilution/ast/pattern/parser.rb +29 -17
- data/lib/evilution/baseline.rb +15 -2
- data/lib/evilution/cli/commands/compare.rb +13 -0
- data/lib/evilution/cli/commands/session_diff.rb +6 -4
- data/lib/evilution/cli/commands/subjects.rb +6 -3
- data/lib/evilution/cli/commands/util_mutation.rb +24 -19
- data/lib/evilution/cli/parser/command_extractor.rb +12 -12
- data/lib/evilution/cli/parser/file_args.rb +3 -1
- data/lib/evilution/cli/parser/options_builder.rb +31 -3
- data/lib/evilution/cli/parser/stdin_reader.rb +2 -2
- data/lib/evilution/cli/parser.rb +18 -20
- data/lib/evilution/cli/printers/environment.rb +19 -19
- data/lib/evilution/cli/printers/session_diff.rb +8 -8
- data/lib/evilution/compare/normalizer.rb +10 -5
- data/lib/evilution/config/file_loader.rb +40 -1
- data/lib/evilution/config.rb +21 -11
- data/lib/evilution/disable_comment.rb +21 -12
- data/lib/evilution/equivalent/heuristic/dead_code.rb +8 -1
- data/lib/evilution/feedback/setup_warning.rb +79 -0
- data/lib/evilution/gem_detector.rb +132 -0
- data/lib/evilution/integration/loading/body_call_neutralizer.rb +190 -0
- data/lib/evilution/integration/loading/mutation_applier.rb +35 -15
- data/lib/evilution/integration/loading/redefinition_recovery.rb +58 -1
- data/lib/evilution/integration/minitest.rb +60 -16
- data/lib/evilution/integration/rspec/result_builder.rb +20 -1
- data/lib/evilution/integration/rspec.rb +20 -1
- data/lib/evilution/isolation/fork.rb +104 -27
- data/lib/evilution/mcp/info_tool/actions/subjects.rb +32 -23
- data/lib/evilution/mcp/info_tool/actions/tests.rb +22 -12
- data/lib/evilution/mcp/info_tool/request_parser.rb +3 -1
- data/lib/evilution/mcp/info_tool/response_formatter.rb +14 -1
- data/lib/evilution/mcp/info_tool.rb +10 -2
- data/lib/evilution/mcp/mutate_tool/option_parser.rb +4 -2
- data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +58 -1
- data/lib/evilution/mcp/mutate_tool/survived_enricher.rb +19 -9
- data/lib/evilution/mcp/mutate_tool.rb +49 -17
- data/lib/evilution/mcp/session_tool.rb +34 -22
- data/lib/evilution/mcp.rb +6 -0
- data/lib/evilution/mutation.rb +26 -16
- data/lib/evilution/mutator/base.rb +66 -16
- data/lib/evilution/mutator/operator/argument_method_call_replacement.rb +59 -0
- data/lib/evilution/mutator/operator/argument_nil_substitution.rb +11 -14
- data/lib/evilution/mutator/operator/argument_removal.rb +11 -14
- data/lib/evilution/mutator/operator/begin_unwrap.rb +17 -5
- data/lib/evilution/mutator/operator/bitwise_complement.rb +26 -19
- data/lib/evilution/mutator/operator/block_param_removal.rb +50 -8
- data/lib/evilution/mutator/operator/block_pass_removal.rb +19 -15
- data/lib/evilution/mutator/operator/case_when.rb +7 -5
- data/lib/evilution/mutator/operator/conditional_branch.rb +22 -22
- data/lib/evilution/mutator/operator/equality_to_identity.rb +8 -3
- data/lib/evilution/mutator/operator/explicit_super_mutation.rb +36 -14
- data/lib/evilution/mutator/operator/index_to_at.rb +18 -5
- data/lib/evilution/mutator/operator/index_to_dig.rb +12 -6
- data/lib/evilution/mutator/operator/index_to_fetch.rb +5 -4
- data/lib/evilution/mutator/operator/keyword_argument.rb +30 -25
- data/lib/evilution/mutator/operator/last_expression_removal.rb +46 -0
- data/lib/evilution/mutator/operator/mixin_removal.rb +20 -14
- data/lib/evilution/mutator/operator/multiple_assignment.rb +12 -13
- data/lib/evilution/mutator/operator/receiver_replacement.rb +38 -7
- data/lib/evilution/mutator/operator/regex_simplification.rb +62 -67
- data/lib/evilution/mutator/operator/rescue_body_replacement.rb +9 -8
- data/lib/evilution/mutator/operator/rescue_removal.rb +58 -12
- data/lib/evilution/mutator/operator/splat_operator.rb +28 -1
- data/lib/evilution/mutator/operator/string_literal.rb +83 -6
- data/lib/evilution/mutator/operator/superclass_removal.rb +21 -15
- data/lib/evilution/mutator/registry.rb +2 -0
- data/lib/evilution/parallel/work_queue/dispatcher.rb +15 -8
- data/lib/evilution/parallel/work_queue/worker.rb +10 -7
- data/lib/evilution/parallel/work_queue.rb +35 -18
- data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +13 -8
- data/lib/evilution/reporter/cli/line_formatters/error_rate_warning.rb +29 -0
- data/lib/evilution/reporter/cli/line_formatters/mutations.rb +17 -8
- data/lib/evilution/reporter/cli/metrics_block.rb +2 -0
- data/lib/evilution/reporter/json.rb +54 -18
- data/lib/evilution/reporter/suggestion/diff_helpers.rb +0 -13
- data/lib/evilution/reporter/suggestion/diff_lines.rb +28 -0
- data/lib/evilution/reporter/suggestion/templates/minitest.rb +20 -14
- data/lib/evilution/reporter/suggestion/templates/rspec.rb +19 -13
- data/lib/evilution/result/mutation_result.rb +12 -6
- data/lib/evilution/runner/baseline_runner.rb +20 -9
- data/lib/evilution/runner/diagnostics.rb +13 -9
- data/lib/evilution/runner/isolation_resolver.rb +75 -12
- data/lib/evilution/runner/mutation_executor/result_cache.rb +3 -1
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +32 -10
- data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +1 -1
- data/lib/evilution/runner/mutation_executor.rb +2 -0
- data/lib/evilution/runner/mutation_planner.rb +53 -16
- data/lib/evilution/runner/subject_pipeline.rb +21 -11
- data/lib/evilution/runner.rb +3 -3
- data/lib/evilution/session/diff.rb +15 -6
- data/lib/evilution/session/schema.rb +44 -0
- data/lib/evilution/session/store.rb +5 -1
- data/lib/evilution/spec_ast_cache.rb +26 -12
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +2 -0
- data/schema/evilution.config.schema.json +205 -0
- data/script/build_runtime_snapshot +88 -0
- data/script/memory_check +11 -5
- data/script/run_self_baseline +79 -0
- data/script/run_self_validation +54 -0
- data/scripts/benchmark_density +10 -9
- data/scripts/compare_mutations +38 -21
- data/scripts/mutant_json_adapter +7 -4
- metadata +16 -2
data/README.md
CHANGED
|
@@ -67,11 +67,13 @@ evilution [command] [options] [files...]
|
|
|
67
67
|
|
|
68
68
|
The shorter alias `evil` ships alongside `evilution` and accepts identical arguments (handy with `alias be='bundle exec'` → `be evil run ...`).
|
|
69
69
|
|
|
70
|
+
Every command, subcommand, and flag listed in this section is part of evilution's public CLI contract; see [docs/versioning.md](docs/versioning.md) for stability and deprecation rules.
|
|
71
|
+
|
|
70
72
|
### Commands
|
|
71
73
|
|
|
72
74
|
| Command | Description | Default |
|
|
73
75
|
|----------------------|----------------------------------------------------|---------|
|
|
74
|
-
| `run`
|
|
76
|
+
| `run` (alias `mutate`) | Execute mutation testing against files | Yes |
|
|
75
77
|
| `init` | Generate `.evilution.yml` config file | |
|
|
76
78
|
| `version` | Print version string | |
|
|
77
79
|
| `subjects [files]` | List mutation subjects with locations and counts | |
|
|
@@ -116,16 +118,33 @@ The shorter alias `evil` ships alongside `evilution` and accepts identical argum
|
|
|
116
118
|
| `--skip-heredoc-literals` | Boolean | false | Skip all string literal mutations inside heredocs. |
|
|
117
119
|
| `--show-disabled` | Boolean | false | Report mutations skipped by `# evilution:disable` comments. |
|
|
118
120
|
| `--fallback-full-suite` | Boolean | false | When no matching spec/test resolves for a mutation, run the whole test suite instead of marking it `:unresolved` and skipping. |
|
|
121
|
+
| `--related-specs-heuristic` | Boolean | false | When a mutation removes an `includes(...)` call, also run matching specs from `spec/{requests,integration,features,system}` (Rails-style domain match on the source file's basename). Trades extra spec runs for higher kill rate on ORM mutations. |
|
|
119
122
|
| `--baseline-session PATH` | String | _(none)_ | Saved session file for HTML report comparison. |
|
|
120
123
|
| `-e CODE`, `--eval CODE` | String | _(none)_ | Inline Ruby code for `util mutation` command. |
|
|
121
124
|
| `--profile NAME` | String | `default` | Operator profile: `default` or `strict`. `strict` adds aggressive truthiness mutators (e.g. replaces `x.predicate?` with `nil`) intended for pre-merge audits. |
|
|
122
125
|
| `--strict` | Boolean | false | Shortcut for `--profile=strict`. |
|
|
123
126
|
|
|
127
|
+
### Options (for `session` subcommands)
|
|
128
|
+
|
|
129
|
+
| Flag | Type | Default | Description |
|
|
130
|
+
|---------------------|---------|----------------------|----------------------------------------------------------------------------------------------|
|
|
131
|
+
| `--results-dir DIR` | String | `.evilution/results` | Directory containing session result JSON files. Honored by `session list` and `session gc`. |
|
|
132
|
+
| `--limit N` | Integer | _(none)_ | (`session list`) Show only the N most recent sessions. |
|
|
133
|
+
| `--since DATE` | String | _(none)_ | (`session list`) Show only sessions created on or after `DATE` (`YYYY-MM-DD`). |
|
|
134
|
+
| `--older-than D` | String | _(required)_ | (`session gc`) Delete sessions older than `D` (e.g. `30d`, `24h`, `1w`). |
|
|
135
|
+
|
|
136
|
+
### Options (for `compare` command)
|
|
137
|
+
|
|
138
|
+
| Flag | Type | Default | Description |
|
|
139
|
+
|------------------|--------|-----------|------------------------------------------------------------------------------------------------------|
|
|
140
|
+
| `--against PATH` | String | _(none)_ | Prior (older) session JSON to diff against. Positional first argument is accepted as a fallback. |
|
|
141
|
+
| `--current PATH` | String | _(none)_ | Current (newer) session JSON. Positional second argument is accepted as a fallback. |
|
|
142
|
+
|
|
124
143
|
### Operator Profiles
|
|
125
144
|
|
|
126
145
|
Two profiles ship out of the box:
|
|
127
146
|
|
|
128
|
-
- **`default`** — the
|
|
147
|
+
- **`default`** — the 74 stable operators registered in `Mutator::Registry.default`. Suitable for everyday CI runs; balances coverage signal against survivor noise.
|
|
129
148
|
- **`strict`** — adds extra truthiness mutators on top of `default`. Currently `PredicateToNil` (replaces every `x.predicate?` call with `nil` to surface tests that only assert truthiness rather than exact return values). Use for pre-merge audits where you want maximum sensitivity at the cost of more survivors.
|
|
130
149
|
|
|
131
150
|
Set via `--profile=strict`, the `--strict` shortcut, or `profile: strict` in `.evilution.yml`.
|
|
@@ -145,6 +164,7 @@ Generate default config: `bundle exec evilution init`
|
|
|
145
164
|
Creates `.evilution.yml`:
|
|
146
165
|
|
|
147
166
|
```yaml
|
|
167
|
+
schema_version: 1 # opts into strict validation (rejects unknown keys, refuses future versions)
|
|
148
168
|
# timeout: 30 # seconds per mutation
|
|
149
169
|
# format: text # text | json | html
|
|
150
170
|
# min_score: 0.0 # 0.0–1.0
|
|
@@ -165,6 +185,62 @@ Creates `.evilution.yml`:
|
|
|
165
185
|
|
|
166
186
|
**Precedence**: CLI flags override `.evilution.yml` values.
|
|
167
187
|
|
|
188
|
+
### Schema versioning
|
|
189
|
+
|
|
190
|
+
`.evilution.yml` may declare an integer `schema_version` (currently `1`). Behavior:
|
|
191
|
+
|
|
192
|
+
- **When declared** — strict mode. Unknown top-level keys raise `Evilution::ConfigError`. A `schema_version` greater than the installed gem supports is rejected so an old gem cannot silently misread a newer config.
|
|
193
|
+
- **When omitted** — legacy lenient mode. Unknown keys are ignored.
|
|
194
|
+
|
|
195
|
+
Compatibility policy for the `1.x` gem line:
|
|
196
|
+
|
|
197
|
+
- New configuration keys are added in MINOR releases (additive only). Each new key takes a default that preserves prior behavior.
|
|
198
|
+
- Existing keys are not removed, renamed, or have their semantics changed in any `1.x` release. Deprecated keys keep working through the entire `1.x` line; see [docs/versioning.md](docs/versioning.md).
|
|
199
|
+
- `schema_version` is bumped only on incompatible changes — i.e. only at the next MAJOR release. `schema_version: 2` will ship with `evilution 2.0`.
|
|
200
|
+
|
|
201
|
+
A JSON Schema covering every supported key lives at [`schema/evilution.config.schema.json`](schema/evilution.config.schema.json). Point editor / IDE YAML extensions at it for autocomplete and inline validation (e.g. VS Code `yaml.schemas`, JetBrains "Custom JSON Schema").
|
|
202
|
+
|
|
203
|
+
### Configuration reference
|
|
204
|
+
|
|
205
|
+
All keys recognised under `schema_version: 1`:
|
|
206
|
+
|
|
207
|
+
| Key | Type | Default | Description |
|
|
208
|
+
|------------------------------|-------------------------------|----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------|
|
|
209
|
+
| `schema_version` | Integer | `1` | Config schema version. Declaring it enables strict validation; omit for lenient mode. |
|
|
210
|
+
| `timeout` | Integer | `30` | Per-mutation timeout in seconds. |
|
|
211
|
+
| `format` | String | `text` | Output format: `text`, `json`, `html`. |
|
|
212
|
+
| `target` | String / null | `null` | Filter expression: method (`Foo#bar`), class (`Foo`), namespace (`Foo*`), descendants (`descendants:Foo`), source glob (`source:**/*.rb`). |
|
|
213
|
+
| `min_score` | Float | `0.0` | Minimum mutation score (0.0–1.0) for exit code 0. |
|
|
214
|
+
| `integration` | String | `rspec` | Test framework: `rspec` or `minitest`. |
|
|
215
|
+
| `verbose` | Boolean | `false` | Verbose output (RSS/GC stats per phase, error details for errored mutations). |
|
|
216
|
+
| `quiet` | Boolean | `false` | Suppress output. |
|
|
217
|
+
| `jobs` | Integer | `1` | Number of parallel workers. |
|
|
218
|
+
| `fail_fast` | Integer / null | `null` | Stop after N surviving mutants. `null` = disabled. |
|
|
219
|
+
| `baseline` | Boolean | `true` | Run baseline test suite to detect pre-existing failures (marked `:neutral`). |
|
|
220
|
+
| `isolation` | String | `auto` | Isolation strategy: `auto`, `fork`, `in_process`. `auto` selects `fork` for Rails projects. |
|
|
221
|
+
| `incremental` | Boolean | `false` | Cache killed/timeout results across runs. |
|
|
222
|
+
| `suggest_tests` | Boolean | `false` | Generate concrete test code in survivor suggestions (matches `integration`). |
|
|
223
|
+
| `progress` | Boolean | `true` | TTY progress bar. |
|
|
224
|
+
| `save_session` | Boolean | `false` | Save session JSON under `.evilution/results/`. |
|
|
225
|
+
| `line_ranges` | Hash | `{}` | Per-file line-range constraints. Typically set via CLI; rare in YAML. |
|
|
226
|
+
| `spec_files` | Array<String> | `[]` | Explicit spec files to run. Bypasses auto-detection when non-empty. |
|
|
227
|
+
| `ignore_patterns` | Array<String> | `[]` | AST patterns to skip during mutation generation. See [docs/ast_pattern_syntax.md](docs/ast_pattern_syntax.md). |
|
|
228
|
+
| `show_disabled` | Boolean | `false` | Report mutations skipped by `# evilution:disable` comments. |
|
|
229
|
+
| `baseline_session` | String / null | `null` | Saved session file path for HTML report comparison. |
|
|
230
|
+
| `skip_heredoc_literals` | Boolean | `false` | Skip string literal mutations inside heredocs. |
|
|
231
|
+
| `related_specs_heuristic` | Boolean | `false` | Append related request/integration/feature/system specs for `includes(...)` mutations. |
|
|
232
|
+
| `fallback_to_full_suite` | Boolean | `false` | When no matching spec resolves, run the entire suite instead of marking the mutation `:unresolved`. |
|
|
233
|
+
| `preload` | String / Boolean / null | `null` | File to preload in parent before forking. `false` to disable. `null` to auto-detect for Rails. |
|
|
234
|
+
| `spec_mappings` | Hash<String, String/Array> | `{}` | Custom mapping from source path to spec path(s). |
|
|
235
|
+
| `spec_pattern` | String / null | `null` | Glob restricting resolved spec candidates. |
|
|
236
|
+
| `example_targeting` | Boolean | `true` | Per-mutation example-level targeting. |
|
|
237
|
+
| `example_targeting_fallback` | String | `full_file` | When targeting finds no example: `full_file` or `unresolved`. |
|
|
238
|
+
| `example_targeting_cache` | Hash | `{ max_files: 50, max_blocks: 10000 }` | LRU cache bounds for the example-targeting AST parser. |
|
|
239
|
+
| `quiet_children` | Boolean | `false` | Redirect each worker's stdout/stderr to per-pid files under `quiet_children_dir`. |
|
|
240
|
+
| `quiet_children_dir` | String | `tmp/evilution_children` | Directory for `--quiet-children` per-pid log files. |
|
|
241
|
+
| `profile` | String | `default` | Operator profile: `default` or `strict`. |
|
|
242
|
+
| `hooks` | Hash<String, String> | `{}` | Lifecycle hooks: event name → path to a Ruby file returning a `Proc`. |
|
|
243
|
+
|
|
168
244
|
## Disable Comments
|
|
169
245
|
|
|
170
246
|
Suppress mutations on specific code with inline comments:
|
|
@@ -190,10 +266,13 @@ Use `--show-disabled` to see which mutations were skipped.
|
|
|
190
266
|
|
|
191
267
|
## JSON Output Schema
|
|
192
268
|
|
|
193
|
-
Use `--format json` for machine-readable output.
|
|
269
|
+
Use `--format json` for machine-readable output. The same shape is used for both stdout reports (`--format json`) and saved session files (`--save-session` → `.evilution/results/*.json`); session files add a small set of extra top-level fields described under [Session JSON files](#session-json-files) below.
|
|
270
|
+
|
|
271
|
+
Schema:
|
|
194
272
|
|
|
195
273
|
```json
|
|
196
274
|
{
|
|
275
|
+
"schema_version": "integer — schema version of this JSON document (current: 1)",
|
|
197
276
|
"version": "string — gem version",
|
|
198
277
|
"timestamp": "string — ISO 8601 timestamp of the report",
|
|
199
278
|
"summary": {
|
|
@@ -251,6 +330,36 @@ Use `--format json` for machine-readable output. Schema:
|
|
|
251
330
|
|
|
252
331
|
**Key metric**: `summary.score` — the mutation score. Higher is better. 1.0 means all mutations were caught.
|
|
253
332
|
|
|
333
|
+
### Session JSON files
|
|
334
|
+
|
|
335
|
+
Sessions saved by `--save-session` (under `.evilution/results/*.json`) and consumed by `evilution session show`, `evilution session diff`, `evilution compare`, and the HTML reporter share the schema above with these additions:
|
|
336
|
+
|
|
337
|
+
| Field | Type | Description |
|
|
338
|
+
|----------------------|---------|---------------------------------------------------------------------------------------------------|
|
|
339
|
+
| `git` | Object | `{ "sha": "<full SHA or null>", "branch": "<branch name or null>" }` captured at run time. |
|
|
340
|
+
| `killed_count` | Integer | Top-level convenience counter; mirrors `summary.killed`. |
|
|
341
|
+
| `timed_out_count` | Integer | Mirrors `summary.timed_out`. |
|
|
342
|
+
| `error_count` | Integer | Mirrors `summary.errors`. |
|
|
343
|
+
| `neutral_count` | Integer | Mirrors `summary.neutral`. |
|
|
344
|
+
| `equivalent_count` | Integer | Mirrors `summary.equivalent`. |
|
|
345
|
+
| `skipped_count` | Integer | Mutations skipped by `# evilution:disable` (omitted from `summary` unless positive). |
|
|
346
|
+
|
|
347
|
+
Saved sessions also omit the per-status arrays (`killed`, `neutral`, `equivalent`, `unresolved`, `unparseable`, `timed_out`, `errors`) — only `survived` and `coverage_gaps` are persisted. The score, totals, and timestamps are stable for diff/compare consumers.
|
|
348
|
+
|
|
349
|
+
#### Schema versioning
|
|
350
|
+
|
|
351
|
+
Every session and stdout JSON document carries a top-level `schema_version` integer (currently `1`). On read:
|
|
352
|
+
|
|
353
|
+
- **`schema_version` matches what this gem supports** — proceed normally.
|
|
354
|
+
- **`schema_version` is omitted** — treated as version `1` (the JSON shape that defined version 1). Sessions written before this field existed continue to load.
|
|
355
|
+
- **`schema_version` is greater than what this gem supports** — `Evilution::Session::Store#load`, `evilution compare`, `evilution session show`, `evilution session diff`, and the HTML reporter raise `Evilution::Error` with the offending file path and a "Upgrade the evilution gem" message. We refuse to silently misread a newer document.
|
|
356
|
+
|
|
357
|
+
Compatibility policy for the `1.x` gem line:
|
|
358
|
+
|
|
359
|
+
- New top-level fields are added in MINOR releases (additive only). Consumers that ignore unknown fields keep working without changes.
|
|
360
|
+
- Existing fields are not removed, renamed, or have their semantics changed in any `1.x` release.
|
|
361
|
+
- `schema_version` is bumped only on incompatible changes — i.e. only at the next MAJOR release. `schema_version: 2` will ship with `evilution 2.0`. See [docs/versioning.md](docs/versioning.md) for the umbrella SemVer policy.
|
|
362
|
+
|
|
254
363
|
### Mutation Statuses
|
|
255
364
|
|
|
256
365
|
| Status | Meaning | Counted in score? |
|
|
@@ -266,7 +375,7 @@ Use `--format json` for machine-readable output. Schema:
|
|
|
266
375
|
|
|
267
376
|
Unresolved mutations indicate a missing test mapping — the file has no corresponding test file that the resolver could find (for example, an RSpec `_spec.rb` file or a Minitest `_test.rb` file, depending on configuration). They are reported separately so you can act on them (add a test, adjust test naming, or opt in to the full-suite fallback) without inflating the error count.
|
|
268
377
|
|
|
269
|
-
## Mutation Operators (
|
|
378
|
+
## Mutation Operators (74 total)
|
|
270
379
|
|
|
271
380
|
Each operator name is stable and appears in JSON output under `survived[].operator`.
|
|
272
381
|
|
|
@@ -344,6 +453,8 @@ Each operator name is stable and appears in JSON output under `survived[].operat
|
|
|
344
453
|
| `lambda_body` | Replace lambda body with nil | `-> { expr }` -> `-> { nil }` |
|
|
345
454
|
| `begin_unwrap` | Remove begin/end wrapper | `begin; expr; end` -> `expr` |
|
|
346
455
|
| `block_param_removal` | Remove explicit block parameter | `def foo(&block)` -> `def foo` |
|
|
456
|
+
| `last_expression_removal` | Strip trailing literal return from method body | `def foo?; warn; true; end` -> `def foo?; warn; end` |
|
|
457
|
+
| `argument_method_call_replacement` | Replace a method-call argument with its receiver | `fn(x.attr)` -> `fn(x)` |
|
|
347
458
|
|
|
348
459
|
## MCP Server (AI Agent Integration)
|
|
349
460
|
|
|
@@ -382,11 +493,11 @@ The `evilution-mutate` tool accepts a `verbosity` parameter to control response
|
|
|
382
493
|
|
|
383
494
|
| Level | Default | What's included |
|
|
384
495
|
|-------------|---------|--------------------------------------------------------------|
|
|
385
|
-
| `summary` | Yes | `summary` + `survived` + `timed_out` + `errors`
|
|
496
|
+
| `summary` | Yes | `summary` + `survived` + `timed_out` + `errors` + `unresolved` (non-survived entries shed `diff` and `error_backtrace` to bound payload size) |
|
|
386
497
|
| `full` | | All entries (killed/neutral/equivalent diffs stripped) |
|
|
387
|
-
| `minimal` | | `summary` + `survived`
|
|
498
|
+
| `minimal` | | `summary` + `survived` (plus a trimmed sample of up to 3 errored entries when `errors > 0`) |
|
|
388
499
|
|
|
389
|
-
Use `minimal` when context window budget is tight and you only need to see what survived. Use `full` when you need to inspect killed/neutral/equivalent entries for debugging.
|
|
500
|
+
Use `minimal` when context window budget is tight and you only need to see what survived. The trimmed `errors` sample (each entry: `error_message`, `error_class`, location, plus the first 5 backtrace lines) is added so a partly-broken run is still self-diagnosable without escalating verbosity. Use `full` when you need to inspect killed/neutral/equivalent entries for debugging.
|
|
390
501
|
|
|
391
502
|
### Enriched Survived Entries
|
|
392
503
|
|
|
@@ -440,6 +551,62 @@ When evilution causes friction (errors, usage problems, missing capabilities you
|
|
|
440
551
|
|
|
441
552
|
Discussion URL: <https://github.com/marinazzio/evilution/discussions>
|
|
442
553
|
|
|
554
|
+
### Contract stability
|
|
555
|
+
|
|
556
|
+
The three MCP tools (`evilution-mutate`, `evilution-session`, `evilution-info`) form evilution's public contract for AI agents. From `1.0.0` onwards the following are governed by the gem's [SemVer policy](docs/versioning.md):
|
|
557
|
+
|
|
558
|
+
- **Tool names**: `evilution-mutate`, `evilution-session`, `evilution-info`.
|
|
559
|
+
- **Input schemas**: every parameter listed in each tool's `input_schema` (name, type, enum values, `required`).
|
|
560
|
+
- **Action enumerations**: the action enum on `evilution-session` (`list`, `show`, `diff`) and `evilution-info` (`subjects`, `tests`, `environment`, `statuses`, `feedback`).
|
|
561
|
+
- **Output payload top-level shape**: the keys and value types documented per action below.
|
|
562
|
+
- **Error envelope**: an error response is a single text content with the response's `error` flag set to true. The body is a JSON object with at minimum an `error` key shaped `{ "type": <string>, "message": <string> }`; tools may add additional top-level keys (e.g. `evilution-mutate` includes `feedback_url` and `feedback_hint` to point agents at the public Discussions channel). Consumers must read the `error.type` discriminator and ignore unknown extras. The error type strings — currently `config_error`, `parse_error`, `not_found`, and `runtime_error` — are part of the contract; new types may be added in MINOR releases (additive).
|
|
563
|
+
|
|
564
|
+
#### Output `schema_version`
|
|
565
|
+
|
|
566
|
+
Successful MCP responses carry a top-level `schema_version` integer. Two distinct version spaces are in play; both happen to be `1` today and either may be bumped independently at the next MAJOR release:
|
|
567
|
+
|
|
568
|
+
- **MCP contract `schema_version`** — `Evilution::MCP::CONTRACT_VERSION` (currently `1`). Stamped on envelopes that exist solely to wrap MCP tool output: `evilution-info` action responses, `evilution-session list`, `evilution-session diff`. Bumped only when the MCP envelope shape itself changes incompatibly.
|
|
569
|
+
- **Session JSON `schema_version`** — `Evilution::Session::Schema::CURRENT_VERSION` (currently `1`). Embedded inside payloads whose shape is also written to disk and consumed elsewhere: `evilution-mutate` (returns a mutation report) and `evilution-session show` (returns a session JSON document). Bumped only when the report/session shape itself changes incompatibly.
|
|
570
|
+
|
|
571
|
+
Per-tool placement:
|
|
572
|
+
|
|
573
|
+
| Tool / action | `schema_version` source | Location in payload |
|
|
574
|
+
|--------------------------------|------------------------------------------|------------------------------------------------------------------------------------------------------|
|
|
575
|
+
| `evilution-mutate` | `Session::Schema::CURRENT_VERSION` | Top-level of the mutation report JSON (same shape as `--save-session` output). |
|
|
576
|
+
| `evilution-session` `list` | `MCP::CONTRACT_VERSION` | Top-level of envelope: `{ "schema_version": 1, "sessions": [...] }`. |
|
|
577
|
+
| `evilution-session` `show` | `Session::Schema::CURRENT_VERSION` | Inside the returned session JSON document. |
|
|
578
|
+
| `evilution-session` `diff` | `MCP::CONTRACT_VERSION` | Top-level alongside `summary` / `fixed` / `new_survivors` / `persistent`. |
|
|
579
|
+
| `evilution-info` (all actions) | `MCP::CONTRACT_VERSION` | Top-level of every successful response, injected by the action response formatter. |
|
|
580
|
+
|
|
581
|
+
#### Per-tool output shapes
|
|
582
|
+
|
|
583
|
+
- **`evilution-mutate`** — full schema in the [JSON Output Schema](#json-output-schema) section. MCP-specific additions to each `survived` entry are documented in [Enriched Survived Entries](#enriched-survived-entries).
|
|
584
|
+
- **`evilution-session` `list`** — `{ "schema_version": Integer, "sessions": Array<{ file, timestamp, total, killed, survived, score, duration }> }`. Sessions are reverse-chronological; the array is filtered by `limit` when provided.
|
|
585
|
+
- **`evilution-session` `show`** — the parsed session JSON document, exactly as written under `.evilution/results/*.json`. Field reference: see [Session JSON files](#session-json-files).
|
|
586
|
+
- **`evilution-session` `diff`** — `{ "schema_version": Integer, "summary": { base_score, head_score, score_delta, base_survived, head_survived, base_total, head_total, base_killed, head_killed }, "fixed": Array, "new_survivors": Array, "persistent": Array }`. The mutation arrays carry the same per-mutation fields the session `survived` list uses (`operator`, `file`, `line`, `subject`, `diff`).
|
|
587
|
+
- **`evilution-info` `subjects`** — `{ "schema_version": Integer, "subjects": Array<{ name, file, line, mutations }>, "total_subjects": Integer, "total_mutations": Integer }`.
|
|
588
|
+
- **`evilution-info` `tests`** — `{ "schema_version": Integer, "specs": Array<{ source, spec }>, "unresolved": Array<String>, "total_sources": Integer, "total_specs": Integer }`.
|
|
589
|
+
- **`evilution-info` `environment`** — `{ "schema_version": Integer, "version": String, "ruby": String, "config_file": String|null, ... }` mirroring the effective `Evilution::Config`.
|
|
590
|
+
- **`evilution-info` `statuses`** — `{ "schema_version": Integer, "statuses": Array<{ name, meaning, in_score }> }`.
|
|
591
|
+
- **`evilution-info` `feedback`** — `{ "schema_version": Integer, "discussion_url": String, "consent": String, "privacy": String }`.
|
|
592
|
+
|
|
593
|
+
#### Deprecation cycle
|
|
594
|
+
|
|
595
|
+
When a parameter, action, or output field on the public MCP contract is deprecated:
|
|
596
|
+
|
|
597
|
+
1. The deprecation is announced in the CHANGELOG and the tool's `description` text gains a deprecation note.
|
|
598
|
+
2. The deprecated form remains functional for the entire `1.x` line — a deprecation introduced in `1.X` continues to work in every subsequent `1.X+N` release.
|
|
599
|
+
3. The earliest release that may remove the deprecated form is `2.0`. Removals are listed in the major-release migration guide.
|
|
600
|
+
4. Adding new parameters, new actions, or new top-level output fields is additive and ships in MINOR releases (existing consumers continue to work).
|
|
601
|
+
|
|
602
|
+
#### Not covered by the contract
|
|
603
|
+
|
|
604
|
+
- The exact wording of error `message` strings (the `type` is contract; the message is a hint that may be reworded).
|
|
605
|
+
- The exact ordering of arrays whose contract calls only for membership (e.g. `unresolved` source files).
|
|
606
|
+
- Progress-stream notification payloads sent through `server_context.notifications` — these are best-effort UI signals, not a stable API.
|
|
607
|
+
- Performance characteristics (request latency, memory, parallelism) — improvements ship in any release; regressions are bugs but not contract violations.
|
|
608
|
+
- Default values that are part of the gem's user-facing semantics (timeout, integration default, etc.) — these follow the umbrella SemVer policy and may be tuned.
|
|
609
|
+
|
|
443
610
|
## Recommended Workflows for AI Agents
|
|
444
611
|
|
|
445
612
|
### 1. Full project scan
|
|
@@ -516,6 +683,21 @@ For each entry in `survived[]`:
|
|
|
516
683
|
|
|
517
684
|
Entries in the JSON `errors[]` array represent mutations that raised an exception (syntax error, load failure, or runtime crash) rather than producing a test outcome. Each entry includes `error_class`, `error_message`, and the first 5 `error_backtrace` lines. Use these fields to decide whether the error is a bug in the mutation operator (file an issue), a load-time problem in the mutated source (often `NoMethodError: super called outside of method` or constant-redefinition issues), or a genuine crash that the original tests should have caught. Run with `--verbose` to stream the same error details to stderr during the run.
|
|
518
685
|
|
|
686
|
+
### Long Minitest fork runs — not a hang
|
|
687
|
+
|
|
688
|
+
Minitest projects under `--isolation=fork` re-bootstrap the test environment (`test_helper.rb`, plugins, runnable state) once per mutation. On constant-heavy files (e.g. Shopify/liquid's `lib/liquid/lexer.rb`, ~270 mutations) the wall-clock cost is dominated by that per-fork bootstrap and any mutations that hit a `--timeout` rather than killing the test fast. A single-worker run (`-j 1`) on a few hundred mutations can take 4+ minutes; combined with `--no-progress` and a non-TTY stderr (CI, redirected logs) the run looks silent the entire time.
|
|
689
|
+
|
|
690
|
+
Recommended invocation for Minitest fork canaries:
|
|
691
|
+
|
|
692
|
+
```bash
|
|
693
|
+
RUBYOPT="-Itest" bundle exec evilution mutate lib/<file>.rb \
|
|
694
|
+
-j 4 -t 10 \
|
|
695
|
+
--integration=minitest --isolation=fork \
|
|
696
|
+
--spec test/<dir>/<file>_test.rb
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
`-j 4` parallelises across workers, `-t 10` caps any mutation that pathologically loops at 10 s. Expect the run to print progress only when stderr is a TTY (use `bundle exec evilution mutate ... 2>&1 | tee log` to get progress while still saving output). The historical "Minitest fork hangs on liquid" report (EV-blnq / GH #1211) turned out to be a slow run + silent UX, not an actual deadlock — the worker logs show steady forward progress when captured via `--quiet-children --quiet-children-dir DIR`.
|
|
700
|
+
|
|
519
701
|
### 8. CI gate
|
|
520
702
|
|
|
521
703
|
```bash
|
|
@@ -588,12 +770,16 @@ Tests 4 paths (InProcess isolation, Fork isolation, mutation generation + stripp
|
|
|
588
770
|
1. **Parse** — Prism parses Ruby files into ASTs with exact byte offsets
|
|
589
771
|
2. **Extract** — Methods are identified as mutation subjects
|
|
590
772
|
3. **Filter** — Disable comments, Sorbet `sig` blocks, and AST ignore patterns exclude mutations before execution
|
|
591
|
-
4. **Mutate** —
|
|
773
|
+
4. **Mutate** — 74 operators produce text replacements at precise byte offsets (source-level surgery, no AST unparsing); heredoc literal text is skipped by default. Identical byte-mutations from different operators are deduplicated by `(file_path, mutated_source)` so the count is not inflated by overlap
|
|
592
774
|
5. **Isolate** — Mutations are applied to temporary file copies (never modifying originals); load-path redirection ensures `require` resolves the mutated copy. Default isolation is in-process for plain Ruby projects and fork for Rails projects (auto-detected); `--isolation fork` forces forked child processes. Both sequential and parallel (`--jobs N`) modes respect the configured isolation strategy
|
|
593
775
|
6. **Test** — The configured test framework (RSpec or Minitest) executes against the mutated source
|
|
594
776
|
7. **Collect** — Source strings and AST nodes are released after use to minimize memory retention
|
|
595
777
|
8. **Report** — Results aggregated into text, JSON, or HTML, including efficiency metrics and peak memory usage
|
|
596
778
|
|
|
779
|
+
## Versioning
|
|
780
|
+
|
|
781
|
+
`evilution` follows [Semantic Versioning](https://semver.org). The full policy — what counts as the public contract, what triggers a major bump, how deprecations work — is documented in [docs/versioning.md](docs/versioning.md).
|
|
782
|
+
|
|
597
783
|
## Repository
|
|
598
784
|
|
|
599
785
|
https://github.com/marinazzio/evilution
|
data/docs/versioning.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Versioning & Upgrade Policy
|
|
2
|
+
|
|
3
|
+
This document defines what `evilution` promises across releases.
|
|
4
|
+
|
|
5
|
+
## SemVer interpretation (1.x)
|
|
6
|
+
|
|
7
|
+
| Bump | Triggered by |
|
|
8
|
+
|---------------|-------------------------------------------------------------------------------|
|
|
9
|
+
| MAJOR (`2.0`) | Removing or renaming anything in the public contract; changing semantics; tightening input validation. |
|
|
10
|
+
| MINOR (`1.X`) | Adding a new CLI flag, config key, mutation operator, public Ruby method, or session/MCP field; relaxing validation; adding an operator to the `default` profile (whether brand-new or promoted from `strict`). |
|
|
11
|
+
| PATCH (`1.X.Y`) | Bug fix, performance improvement, documentation, internal refactor with no observable contract effect. |
|
|
12
|
+
|
|
13
|
+
## Public contract surface
|
|
14
|
+
|
|
15
|
+
The following surfaces are covered by the SemVer guarantees above:
|
|
16
|
+
|
|
17
|
+
- **Public Ruby API** — classes and methods explicitly documented as public. Everything else is internal and may change in any release.
|
|
18
|
+
- **CLI flags and commands** — the README "Command Reference" tables are the authoritative list.
|
|
19
|
+
- **`.evilution.yml` configuration keys** — see the README "Configuration" section.
|
|
20
|
+
- **Session JSON files** (`.evilution/results/*.json`) — see the README "JSON Output Schema" section.
|
|
21
|
+
- **MCP tool input/output schemas** (`evilution-mutate`, `evilution-session`, `evilution-info`) — see the README "MCP Server" section.
|
|
22
|
+
- **Process exit codes** — `0` pass, `1` fail, `2` error. Documented in the README "Exit Codes" section.
|
|
23
|
+
|
|
24
|
+
Anything not on this list is internal. It can change in any release without a deprecation cycle.
|
|
25
|
+
|
|
26
|
+
## Deprecation cycle
|
|
27
|
+
|
|
28
|
+
When a feature on the public contract surface is deprecated:
|
|
29
|
+
|
|
30
|
+
1. It is marked with the YARD `@deprecated` tag (Ruby API), or with a deprecation note in the relevant doc table (CLI flags, config keys, MCP fields).
|
|
31
|
+
2. Where the call site is reachable at runtime, a one-line warning is emitted to stderr.
|
|
32
|
+
3. The deprecated form remains functional for the entire `1.x` line. A feature deprecated in any `1.X` release continues to work in every subsequent `1.X+N` release.
|
|
33
|
+
4. The earliest release that may remove the feature is the next major (`2.0`), per the SemVer table above.
|
|
34
|
+
5. Each removal is recorded in the CHANGELOG under the major-release entry.
|
|
35
|
+
|
|
36
|
+
## Explicitly NOT contract
|
|
37
|
+
|
|
38
|
+
The following are not part of the versioned contract and may change in any release, including patches:
|
|
39
|
+
|
|
40
|
+
- **Mutation score values.** The score depends on the registered operator set, the operator profile, and your test suite. Adding a new operator to the `default` profile is a MINOR change (additive feature) but will shift scores. Pin both the gem version and the operator profile (`profile: default` or `profile: strict`) if you need a stable score across runs.
|
|
41
|
+
- **Mutation operator output text.** Operator *names* (the `operator` field in JSON output, e.g. `arithmetic_replacement`) are part of the contract. The exact mutated source string an operator emits is diagnostic and may change to fix bugs or improve clarity.
|
|
42
|
+
- **Internal classes** (any class not explicitly documented as part of the public Ruby API).
|
|
43
|
+
- **Log lines, progress output, and human-readable report wording.**
|
|
44
|
+
- **Performance characteristics** (timing, memory, parallel scheduling). Improvements ship in any release; regressions are bugs but not contract violations.
|
|
45
|
+
|
|
46
|
+
## Upgrading
|
|
47
|
+
|
|
48
|
+
- **Patch (`1.X.Y` → `1.X.Y+1`)** and **minor (`1.X` → `1.X+1`)**: drop in. Read the CHANGELOG for new flags or config keys you may want to opt into.
|
|
49
|
+
- **Major (`1.X` → `2.0`)**: a migration guide ships with the release, listing every removed contract surface and the replacement path.
|
|
50
|
+
|
|
51
|
+
## References
|
|
52
|
+
|
|
53
|
+
- [CHANGELOG](../CHANGELOG.md) — chronological list of changes per release.
|
|
@@ -17,18 +17,35 @@ class Evilution::AST::ConstantNames
|
|
|
17
17
|
private
|
|
18
18
|
|
|
19
19
|
def collect(node, nesting = [])
|
|
20
|
-
names = []
|
|
21
20
|
case node
|
|
22
|
-
when Prism::ModuleNode, Prism::ClassNode
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
names.concat(collect(node.body, nesting + [const])) if node.body
|
|
27
|
-
when Prism::ProgramNode
|
|
28
|
-
names.concat(collect(node.statements, nesting)) if node.statements
|
|
29
|
-
when Prism::StatementsNode
|
|
30
|
-
node.body.each { |child| names.concat(collect(child, nesting)) }
|
|
21
|
+
when Prism::ModuleNode, Prism::ClassNode then collect_class(node, nesting)
|
|
22
|
+
when Prism::ProgramNode then collect_program(node, nesting)
|
|
23
|
+
when Prism::StatementsNode then collect_statements(node, nesting)
|
|
24
|
+
else []
|
|
31
25
|
end
|
|
32
|
-
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def collect_class(node, nesting)
|
|
29
|
+
const = node.constant_path.full_name
|
|
30
|
+
qualified = qualify(const, nesting)
|
|
31
|
+
return [qualified] if node.body.nil?
|
|
32
|
+
|
|
33
|
+
[qualified] + collect(node.body, nesting + [const])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def collect_program(node, nesting)
|
|
37
|
+
return [] if node.statements.nil?
|
|
38
|
+
|
|
39
|
+
collect(node.statements, nesting)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def collect_statements(node, nesting)
|
|
43
|
+
node.body.flat_map { |child| collect(child, nesting) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def qualify(const, nesting)
|
|
47
|
+
return const if nesting.empty? || const.include?("::")
|
|
48
|
+
|
|
49
|
+
"#{nesting.join("::")}::#{const}"
|
|
33
50
|
end
|
|
34
51
|
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../ast"
|
|
6
|
+
|
|
7
|
+
# Computes the byte-length needed for a mutation whose target range contains
|
|
8
|
+
# heredoc anchors (`<<~MARKER` / `<<-MARKER` / `<<MARKER`).
|
|
9
|
+
#
|
|
10
|
+
# Prism reports a heredoc anchor's `location` as the inline range of just
|
|
11
|
+
# `<<~MARKER` — the body lines and the closing terminator live in `closing_loc`
|
|
12
|
+
# which is on a later line. An operator that builds a byte edit from the
|
|
13
|
+
# anchor's inline range (e.g. `argument_removal` using
|
|
14
|
+
# `node.arguments.location`) covers the anchor but leaves the body+terminator
|
|
15
|
+
# in place, producing an orphaned heredoc fragment that the parser rejects.
|
|
16
|
+
#
|
|
17
|
+
# `extend_length` walks the supplied AST node for heredoc descendants whose
|
|
18
|
+
# anchor falls inside `[offset, offset + length)` and returns a length wide
|
|
19
|
+
# enough to also cover those descendants' `closing_loc.end_offset` — so the
|
|
20
|
+
# mutation's `replacement` replaces the heredoc body and terminator along with
|
|
21
|
+
# the anchor.
|
|
22
|
+
module Evilution::AST::HeredocSpan
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def extend_length(node:, offset:, length:)
|
|
26
|
+
return length if node.nil?
|
|
27
|
+
|
|
28
|
+
end_offset = offset + length
|
|
29
|
+
max_end = end_offset
|
|
30
|
+
Walker.new(offset, end_offset) do |closing_end|
|
|
31
|
+
max_end = closing_end if closing_end > max_end
|
|
32
|
+
end.visit(node)
|
|
33
|
+
max_end - offset
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class Walker < Prism::Visitor
|
|
37
|
+
def initialize(start_offset, end_offset, &block)
|
|
38
|
+
super()
|
|
39
|
+
@start_offset = start_offset
|
|
40
|
+
@end_offset = end_offset
|
|
41
|
+
@block = block
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def visit_string_node(node)
|
|
45
|
+
record_if_heredoc(node)
|
|
46
|
+
super
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def visit_interpolated_string_node(node)
|
|
50
|
+
record_if_heredoc(node)
|
|
51
|
+
super
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def visit_x_string_node(node)
|
|
55
|
+
record_if_heredoc(node)
|
|
56
|
+
super
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def visit_interpolated_x_string_node(node)
|
|
60
|
+
record_if_heredoc(node)
|
|
61
|
+
super
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def record_if_heredoc(node)
|
|
67
|
+
return unless heredoc?(node)
|
|
68
|
+
|
|
69
|
+
closing = node.closing_loc
|
|
70
|
+
return unless anchor_in_range?(node) && closing
|
|
71
|
+
|
|
72
|
+
@block.call(closing_end_excluding_trailing_newline(closing))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def heredoc?(node)
|
|
76
|
+
node.respond_to?(:heredoc?) && node.heredoc?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Anchor must be inside the mutation's target range; only then does its
|
|
80
|
+
# heredoc body sit outside the range and need pulling in.
|
|
81
|
+
def anchor_in_range?(node)
|
|
82
|
+
opening = node.opening_loc
|
|
83
|
+
return false if opening.nil?
|
|
84
|
+
|
|
85
|
+
opening.start_offset >= @start_offset && opening.start_offset < @end_offset
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Prism's closing_loc covers the terminator including the trailing
|
|
89
|
+
# newline. Excluding that newline preserves line structure after the
|
|
90
|
+
# replacement (any code that follows lands on its own line).
|
|
91
|
+
def closing_end_excluding_trailing_newline(closing)
|
|
92
|
+
end_off = closing.end_offset
|
|
93
|
+
slice = closing.slice
|
|
94
|
+
end_off -= 1 if slice && slice.end_with?("\n")
|
|
95
|
+
end_off
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
private_constant :Walker
|
|
99
|
+
end
|
|
@@ -75,24 +75,36 @@ class Evilution::AST::Pattern::Parser
|
|
|
75
75
|
|
|
76
76
|
def parse_value
|
|
77
77
|
skip_whitespace
|
|
78
|
+
parse_negation || parse_deep_wildcard || parse_single_wildcard || parse_any_node || parse_value_or_nested
|
|
79
|
+
end
|
|
78
80
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
81
|
+
def parse_negation
|
|
82
|
+
return nil unless current_char == "!"
|
|
83
|
+
|
|
84
|
+
advance(1)
|
|
85
|
+
skip_whitespace
|
|
86
|
+
Evilution::AST::Pattern::NegationMatcher.new(parse_value)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def parse_deep_wildcard
|
|
90
|
+
return nil unless peek_string("**")
|
|
91
|
+
|
|
92
|
+
advance(2)
|
|
93
|
+
Evilution::AST::Pattern::DeepWildcardMatcher.new
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def parse_single_wildcard
|
|
97
|
+
return nil unless current_char == "*"
|
|
98
|
+
|
|
99
|
+
advance(1)
|
|
100
|
+
Evilution::AST::Pattern::WildcardValueMatcher.new
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def parse_any_node
|
|
104
|
+
return nil unless current_char == "_" && !identifier_continues?(1)
|
|
105
|
+
|
|
106
|
+
advance(1)
|
|
107
|
+
Evilution::AST::Pattern::AnyNodeMatcher.new
|
|
96
108
|
end
|
|
97
109
|
|
|
98
110
|
def parse_value_or_nested
|
data/lib/evilution/baseline.rb
CHANGED
|
@@ -15,16 +15,18 @@ class Evilution::Baseline
|
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def initialize(spec_resolver: Evilution::SpecResolver.new, timeout: 30, runner: nil,
|
|
18
|
+
def initialize(spec_resolver: Evilution::SpecResolver.new, timeout: 30, runner: nil,
|
|
19
|
+
fallback_dir: "spec", test_files: nil)
|
|
19
20
|
@spec_resolver = spec_resolver
|
|
20
21
|
@timeout = timeout
|
|
21
22
|
@runner = runner
|
|
22
23
|
@fallback_dir = fallback_dir
|
|
24
|
+
@test_files = test_files
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
def call(subjects)
|
|
26
28
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
27
|
-
spec_files =
|
|
29
|
+
spec_files = baseline_spec_files(subjects)
|
|
28
30
|
failed = Set.new
|
|
29
31
|
|
|
30
32
|
spec_files.each do |spec_file|
|
|
@@ -96,6 +98,17 @@ class Evilution::Baseline
|
|
|
96
98
|
|
|
97
99
|
private
|
|
98
100
|
|
|
101
|
+
# When --spec was provided, run those files only. Auto-discovery is skipped
|
|
102
|
+
# entirely — the user has declared what covers their subjects and any
|
|
103
|
+
# mismatch between auto-discovery and their declaration is what produced
|
|
104
|
+
# the misleading "No matching test found" warning users have reported even
|
|
105
|
+
# while passing --spec.
|
|
106
|
+
def baseline_spec_files(subjects)
|
|
107
|
+
return Array(@test_files).uniq if @test_files && !@test_files.empty?
|
|
108
|
+
|
|
109
|
+
resolve_unique_spec_files(subjects)
|
|
110
|
+
end
|
|
111
|
+
|
|
99
112
|
def resolve_unique_spec_files(subjects)
|
|
100
113
|
warned = Set.new
|
|
101
114
|
subjects.map do |s|
|