evilution 0.30.4 → 0.32.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 +22 -0
- data/.rubocop_todo.yml +6 -0
- data/CHANGELOG.md +22 -0
- data/README.md +9 -7
- data/docs/integrations.md +126 -0
- data/docs/isolation.md +28 -0
- data/lib/evilution/cli/parser/options_builder.rb +6 -1
- data/lib/evilution/config/validators/integration.rb +5 -1
- data/lib/evilution/config.rb +14 -4
- data/lib/evilution/integration/loading/mutation_applier.rb +16 -8
- data/lib/evilution/integration/loading/source_evaluator.rb +4 -1
- data/lib/evilution/integration/minitest.rb +1 -1
- data/lib/evilution/integration/rspec/baseline_runner.rb +3 -1
- data/lib/evilution/integration/rspec/framework_loader.rb +5 -1
- data/lib/evilution/integration/rspec.rb +38 -1
- data/lib/evilution/integration/test_unit/dispatcher.rb +26 -0
- data/lib/evilution/integration/test_unit/framework_loader.rb +33 -0
- data/lib/evilution/integration/test_unit/result_builder.rb +53 -0
- data/lib/evilution/integration/test_unit/subject_class_registry.rb +26 -0
- data/lib/evilution/integration/test_unit/test_file_resolver.rb +48 -0
- data/lib/evilution/integration/test_unit.rb +124 -0
- data/lib/evilution/integration/test_unit_crash_detector.rb +61 -0
- data/lib/evilution/isolation/fork.rb +26 -1
- data/lib/evilution/isolation/in_process.rb +20 -3
- data/lib/evilution/mcp/info_tool.rb +2 -2
- data/lib/evilution/mcp/mutate_tool.rb +3 -2
- data/lib/evilution/runner/baseline_runner.rb +3 -1
- data/lib/evilution/runner/canary.rb +130 -0
- data/lib/evilution/runner.rb +24 -1
- data/lib/evilution/spec_ast_cache.rb +20 -3
- data/lib/evilution/spec_resolver.rb +16 -2
- data/lib/evilution/spec_selector.rb +14 -2
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +39 -0
- data/script/run_self_baseline +2 -2
- metadata +11 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dc948250a218198fef7420e04ab49e5a2f8e01ff50ae279f5299bb85db07ab77
|
|
4
|
+
data.tar.gz: df6e3a2ea4ee5512f9c946f4df514f8a1a2df8308ac77236ceafa3de81b435d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 43c08e6baa4904c1122d639c957cc1139c9e4760e3e38aed8664a70ff46388aab91b64924816739d4c7a9f00abb049f2da579c50f3b5a585c49b2175f3461dff
|
|
7
|
+
data.tar.gz: e30f5845c332781de9431c9a4238df1ac9048960d3ef36323dac0a8beaf2f5c5d56eb1d3132d618a7e076bb7547520286fc127347499b0b95640331e16912fb8
|
data/.beads/interactions.jsonl
CHANGED
|
@@ -387,3 +387,25 @@
|
|
|
387
387
|
{"id":"int-011584d4","kind":"field_change","created_at":"2026-05-15T15:55:27.437673072Z","actor":"Denis Kiselev","issue_id":"EV-vxgl","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed PR (merged). MutationApplier#mark_feature_loaded registers realpath in $LOADED_FEATURES post-eval — spec require() no longer reloads original. pagy jobs=4: 0% -> 82.81%. Residual gap vs jobs=1 is separate (EV-wu8w in_process inflation)."}}
|
|
388
388
|
{"id":"int-c44e7652","kind":"field_change","created_at":"2026-05-16T12:45:46.793709029Z","actor":"Denis Kiselev","issue_id":"EV-xfaj","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed in PR #1260 — run_minitest counts test methods from Minitest::Runnable.runnables registry instead of an evictable reporter"}}
|
|
389
389
|
{"id":"int-eea19850","kind":"field_change","created_at":"2026-05-16T14:22:49.671269232Z","actor":"Denis Kiselev","issue_id":"EV-wu8w","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed in PR #1264 — run_minitest reads verdict from evilution's own per-run SummaryReporter attached after plugin init"}}
|
|
390
|
+
{"id":"int-cf85d538","kind":"field_change","created_at":"2026-05-16T15:40:28.573223389Z","actor":"Denis Kiselev","issue_id":"EV-5dxk","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Part A (required guard: 0 dispatched test methods -> :error) shipped in 0.30.3 PR #1257. Acceptance met. Part B (test-unit integration) split into its own epic."}}
|
|
391
|
+
{"id":"int-6efa8613","kind":"field_change","created_at":"2026-05-16T15:55:20.799858411Z","actor":"Denis Kiselev","issue_id":"EV-174x","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed and merged 2026-05-12; GH #1194 / #1195 closed. RedefinitionRecovery widened (already registered/initialized/exists); splice approach reverted for memory leak. Bead-close missed at merge time — syncing now."}}
|
|
392
|
+
{"id":"int-ddb8370f","kind":"field_change","created_at":"2026-05-16T15:55:21.07310729Z","actor":"Denis Kiselev","issue_id":"EV-wwx3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed and merged 2026-05-12; GH #1194 / #1195 closed. RedefinitionRecovery widened (already registered/initialized/exists); splice approach reverted for memory leak. Bead-close missed at merge time — syncing now."}}
|
|
393
|
+
{"id":"int-4ea24fb5","kind":"field_change","created_at":"2026-05-16T16:04:54.395246914Z","actor":"Denis Kiselev","issue_id":"EV-s24s","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Already fixed — both named specs (unparseable_short_circuit_spec.rb, neutralization_integration_spec.rb) pass and destructure via execution.results. Stale in_progress; syncing."}}
|
|
394
|
+
{"id":"int-8b699158","kind":"field_change","created_at":"2026-05-16T17:22:07.345408768Z","actor":"Denis Kiselev","issue_id":"EV-kcuf","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Proof-of-life canary implemented + merged. Runner::Canary runs a synthetic unobservable mutation at session start; aborts if not scored :survived. --[no-]canary flag, config.canary default true."}}
|
|
395
|
+
{"id":"int-e4dcc2b3","kind":"field_change","created_at":"2026-05-27T04:19:00.738936734Z","actor":"Denis Kiselev","issue_id":"EV-qbd6","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fix landed on master in commit 0f1781f (require 'bundler/setup' at bin/evilution-self:16). Verified 2026-05-27: crash dir lib/evilution/result/*.rb scores 91.11% (744 muts, 656 killed, 64 survived, 24 equivalent, 0 errors); canary passes; dual-runtime harness measures crash dirs again."}}
|
|
396
|
+
{"id":"int-4cb27d5a","kind":"field_change","created_at":"2026-05-28T06:42:48.564599548Z","actor":"Denis Kiselev","issue_id":"EV-pyx6","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Fix merged on master in commit 72b43a3 (PR #1292). Added Evilution.project_base_dir helper + anchored spec/ via Evilution.project_base_dir in framework_loader.rb and baseline_runner.rb. Verified 2026-05-28: bin/evilution-self run lib/evilution/config/env_loader.rb scores 94.59% (was 0 before fix). 4705 examples pass, rubocop clean."}}
|
|
397
|
+
{"id":"int-4cd16cba","kind":"field_change","created_at":"2026-05-29T04:54:00.815774086Z","actor":"Denis Kiselev","issue_id":"EV-8tmc","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Partial fix landed in commit 525787b: 18 of 20 lib/ subdirs now measurable (was 11/20). Two follow-up causes diagnosed and addressed: (1) auto-isolation picking in_process for non-Rails projects + RSpec swallowing Timeout::Error per-example, fixed by script using --isolation=fork explicitly; (2) ChildOutput log_dir relative-path bug under EV-wqxu sandbox CWD, fixed by absolutizing in runner.rb#configure_child_output. Remaining 2 dirs (parallel + toplevel) hang via different root cause — grandchild fork leak — tracked separately as EV-dgjv (GH #1295)."}}
|
|
398
|
+
{"id":"int-991e5acf","kind":"field_change","created_at":"2026-05-30T15:56:24.341278813Z","actor":"Denis Kiselev","issue_id":"EV-auk5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
399
|
+
{"id":"int-df2bd661","kind":"field_change","created_at":"2026-05-30T15:56:24.686504141Z","actor":"Denis Kiselev","issue_id":"EV-9d62","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
400
|
+
{"id":"int-6a1dc0ce","kind":"field_change","created_at":"2026-05-30T15:56:25.04815512Z","actor":"Denis Kiselev","issue_id":"EV-unar","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
401
|
+
{"id":"int-defdc095","kind":"field_change","created_at":"2026-05-30T15:56:25.406639346Z","actor":"Denis Kiselev","issue_id":"EV-04sc","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
402
|
+
{"id":"int-bc5a7747","kind":"field_change","created_at":"2026-05-30T17:33:33.191722982Z","actor":"Denis Kiselev","issue_id":"EV-8qiy","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
403
|
+
{"id":"int-796e3efc","kind":"field_change","created_at":"2026-05-30T17:33:34.539604057Z","actor":"Denis Kiselev","issue_id":"EV-uv11","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
404
|
+
{"id":"int-5fb67d2b","kind":"field_change","created_at":"2026-05-30T17:46:39.956899342Z","actor":"Denis Kiselev","issue_id":"EV-bcjp","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
405
|
+
{"id":"int-6ba88696","kind":"field_change","created_at":"2026-05-30T17:59:44.432841837Z","actor":"Denis Kiselev","issue_id":"EV-aqxd","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
406
|
+
{"id":"int-34eb133e","kind":"field_change","created_at":"2026-05-31T02:20:26.61836178Z","actor":"Denis Kiselev","issue_id":"EV-vumz","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
407
|
+
{"id":"int-9527af14","kind":"field_change","created_at":"2026-05-31T02:47:48.910182696Z","actor":"Denis Kiselev","issue_id":"EV-zhqc","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
408
|
+
{"id":"int-dfe331ef","kind":"field_change","created_at":"2026-05-31T02:59:12.044281579Z","actor":"Denis Kiselev","issue_id":"EV-akt2","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
409
|
+
{"id":"int-c7bcc360","kind":"field_change","created_at":"2026-05-31T03:12:47.029582311Z","actor":"Denis Kiselev","issue_id":"EV-6az9","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
410
|
+
{"id":"int-fa3ade4f","kind":"field_change","created_at":"2026-05-31T08:47:32.000198989Z","actor":"Denis Kiselev","issue_id":"EV-d3av","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
411
|
+
{"id":"int-b1831323","kind":"field_change","created_at":"2026-05-31T08:47:50.244781995Z","actor":"Denis Kiselev","issue_id":"EV-d7re","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
|
data/.rubocop_todo.yml
CHANGED
|
@@ -8,7 +8,13 @@ Metrics/AbcSize:
|
|
|
8
8
|
|
|
9
9
|
Metrics/ClassLength:
|
|
10
10
|
Exclude:
|
|
11
|
+
- "lib/evilution/config.rb"
|
|
11
12
|
- "lib/evilution/isolation/fork.rb"
|
|
12
13
|
- "lib/evilution/integration/minitest.rb"
|
|
13
14
|
- "lib/evilution/mcp/mutate_tool.rb"
|
|
15
|
+
- "lib/evilution/runner.rb"
|
|
14
16
|
- "lib/evilution/runner/isolation_resolver.rb"
|
|
17
|
+
|
|
18
|
+
Metrics/MethodLength:
|
|
19
|
+
Exclude:
|
|
20
|
+
- "lib/evilution/runner.rb"
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
Versioning policy: see [docs/versioning.md](docs/versioning.md).
|
|
4
4
|
|
|
5
|
+
## [0.32.0] - 2026-05-31
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`--integration test-unit` runs mutations against projects whose suites use the `test-unit` gem** — Rails projects that `require "test/unit/rails/test_help"` make `ActiveSupport::TestCase` inherit from `Test::Unit::TestCase`, and the existing Minitest integration could not dispatch them (`Minitest::Runnable.runnables` is empty because the test classes are not Minitest runnables at all). A dedicated `Evilution::Integration::TestUnit` orchestrator now wires a framework loader (disables the `Test::Unit::AutoRunner` at_exit handler so it does not fire on evilution exit), a subject-class registry (diffs `Test::Unit::TestCase` descendants across `load` to scope each mutation to freshly-loaded suites without a public registry-clear API), a dispatcher (`Test::Unit::UI::Console::TestRunner` with output captured to a `StringIO`), a spec resolver (`test/foo_test.rb` convention, matching Minitest), and a result builder shaping the `passed`/`test_crashed`/`no_tests_ran`/`unresolved` hashes that `classify_status` consumes. `Evilution::Integration::TestUnitCrashDetector` mirrors the Minitest analogue, distinguishing `Test::Unit::Failure` (assertion) from `Test::Unit::Error` (exception/crash) via `TestResult.add_listener(FAULT, ...)`. CLI accepts both `--integration test-unit` and `--integration test_unit`; the MCP `evilution-mutate` tool advertises `test-unit` in its JSON schema enum. Kaminari canary (`kaminari-core/lib/kaminari/models/page_scope_methods.rb`) now scores 63.60% (272 mutations dispatched) where 0.30.x scored 0.00% with the Minitest integration. See [docs/integrations.md](docs/integrations.md). Suggest-tests templates for test-unit are not yet implemented; runs with `--suggest-tests --integration test-unit` fall back to the generic operator-level phrasing (EV-d7re epic + sub-beads EV-8qiy/EV-uv11/EV-bcjp/EV-aqxd/EV-vumz/EV-zhqc/EV-akt2/EV-6az9/EV-d3av, PRs #1314/#1315/#1316/#1318/#1320/#1321, GH #1267)
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Pool workers no longer hang indefinitely in `Process.wait` when a per-mutation child writes a payload but does not exit promptly** — `Evilution::Isolation::Fork#reap_and_decode` called unbounded `Process.wait(pid)` after reading a length-prefixed payload from the marshal pipe. If the per-mutation child had written the payload but was still stuck in `execute_in_child` waiting on a subject grandchild the mutation broke (or a grandchild had written garbage bytes that looked like a valid header+body to the parent's inherited write fd), the pool worker blocked forever; the per-mutation timeout had no effect because `wait_for_result` had already returned. `Evilution::Parallel::WorkQueue::Dispatcher`'s `item_timeout` then fired `"worker timed out after 30s"` and SIGKILLed the pool worker, leaving the per-mutation child orphaned to init. `reap_and_decode` now bounds the wait by `REAP_DEADLINE = 1.0`s and falls through to the TERM → GRACE_PERIOD → KILL → safe_wait ladder when the child has not exited, so a stuck child takes ~3s worst-case instead of indefinite. Previously-unmeasurable parallel/ + toplevel/ self-baseline directories (parallel/work_queue.rb, parallel/work_queue/dispatcher.rb, parallel/work_queue/worker.rb, isolation/, toplevel) now all produce real summaries (EV-dgjv, PR #1296, GH #1295)
|
|
14
|
+
- **`script/run_self_baseline` toplevel batch invocation now passes `--isolation=fork`** — every subdir invocation explicitly passed `--isolation=fork`, but the trailing toplevel batch (`lib/evilution/*.rb` files) omitted it. Default isolation auto-picked in_process for the non-Rails self-mutation, which (combined with RSpec's per-example `Timeout::Error` swallow) caused the toplevel batch to hit dispatcher item_timeout and report `NO_SUMMARY`. The toplevel cmd now matches the per-subdir cmd, so all 20 self-baseline dirs are measurable (EV-dgjv, PR #1296, GH #1295)
|
|
15
|
+
|
|
16
|
+
## [0.31.0] - 2026-05-27
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **Proof-of-life canary at session start** — before any real mutation runs, `Evilution::Runner::Canary` evals a synthetic, guaranteed-unobservable mutation through the configured integration + isolation. A healthy pipeline must score it `:survived`; any other status aborts the run with a diagnostic, so the user never sees a score that was produced by a broken pipeline (autoload mismatch, reporter-plugin eviction, isolation defect, `fail_if_no_examples` config drift, etc.). On by default; toggle with `--[no-]canary` or `canary: true|false` in `.evilution.yml`. Mirrors the configured `--isolation` so isolation-specific defects are caught too (EV-kcuf, PR #1268, GH #1233)
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **Mutation children no longer pollute the working tree with files written to relative paths** — every isolator (fork and in_process) now `Dir.chdir`s into a per-mutation sandbox directory for the duration of `test_command.call`, and removes the sandbox on the way out. Any mutation that turns an absolute path into a relative one — `argument_removal` on `File.join(dir, name)`, `method_call_removal` on `File.expand_path(rel, base)`, etc. — used to write `File.write(name, …)` into the parent process's CWD (typically the repo root) and leak past the run; baselining `lib/evilution/runner/canary.rb` deposited 43 stray files (`canary_<pid>_<hex>_spec.rb`, `evilutioncanary_<pid>_<hex>.rb`) before this fix. Spec resolution and source eval anchor project-relative paths to `Evilution::PROJECT_ROOT` (captured at load), so the sandbox CWD does not break `SpecResolver`, `SpecSelector`, `MutationApplier`, `SourceEvaluator`, or the `RSpec::Core::Runner` / `Minitest.load` invocation sites (EV-wqxu, PR #1281, GH #1278)
|
|
25
|
+
- **`MutationApplier` registers the mutation in `$LOADED_FEATURES` BEFORE evaluating the source, not after** — a sibling `require_relative` chain that loops back to the mutated file during the eval itself (e.g. `lib/evilution/mcp/*.rb` tools whose body requires a peer that requires this file back) re-read the *original* source from disk and clobbered the mutation mid-eval. Every such mutation silently survived. The feature-loaded marker now runs immediately before `@source_evaluator.call`, so the in-progress eval is the canonical source any concurrent require sees. Without this, fork workers started from the same pre-`require` snapshot and the whole file scored 0% (EV-ekax, PR #1272, GH #1269)
|
|
26
|
+
|
|
5
27
|
## [0.30.4] - 2026-05-16
|
|
6
28
|
|
|
7
29
|
### Fixed
|
data/README.md
CHANGED
|
@@ -101,12 +101,13 @@ Every command, subcommand, and flag listed in this section is part of evilution'
|
|
|
101
101
|
| `--example-targeting-fallback MODE` | String | `full_file` | Behavior when no example matches: `full_file` (run the whole spec file) or `unresolved` (skip the mutation as `:unresolved`). |
|
|
102
102
|
| `-j`, `--jobs N` | Integer | 1 | Number of parallel workers. Uses demand-driven work distribution with pipe-based IPC. |
|
|
103
103
|
| `--no-baseline` | Boolean | _(enabled)_ | Skip baseline test suite check. By default, a baseline run detects pre-existing failures and marks those mutations as `neutral`. |
|
|
104
|
+
| `--[no-]canary` | Boolean | _(enabled)_ | Run a proof-of-life synthetic mutation at session start; abort the run if the pipeline misreports it. Catches misconfigured isolation, broken autoload, and reporter-plugin eviction before any real score is produced. Pass `--no-canary` to skip (e.g. CI speed, or when the canary itself is the thing under test). |
|
|
104
105
|
| `--fail-fast [N]` | Integer | _(none)_ | Stop after N surviving mutants (default 1 if no value given). |
|
|
105
106
|
| `-v`, `--verbose` | Boolean | false | Verbose output with RSS memory and GC stats per phase and per mutation; also prints error class, message, and first 5 backtrace lines for errored mutations. |
|
|
106
|
-
| `--suggest-tests` | Boolean | false | Generate concrete test code in suggestions (RSpec or Minitest, based on `--integration`). |
|
|
107
|
+
| `--suggest-tests` | Boolean | false | Generate concrete test code in suggestions (RSpec or Minitest, based on `--integration`; falls back to generic phrasing for `test-unit`). |
|
|
107
108
|
| `-q`, `--quiet` | Boolean | false | Suppress output. |
|
|
108
109
|
| `--stdin` | Boolean | false | Read target file paths from stdin (one per line). |
|
|
109
|
-
| `--integration NAME` | String | `rspec` | Test framework integration: `rspec` or `
|
|
110
|
+
| `--integration NAME` | String | `rspec` | Test framework integration: `rspec`, `minitest`, or `test-unit`. See [docs/integrations.md](docs/integrations.md). |
|
|
110
111
|
| `--[no-]incremental` | Boolean | false | Cache killed/timeout results; skip unchanged mutations on re-runs. Pass `--no-incremental` to override `incremental: true` from the config file for one invocation (e.g. cold-cache debugging). Last flag wins when both are given. |
|
|
111
112
|
| `--save-session` | Boolean | false | Persist results as timestamped JSON under `.evilution/results/`. |
|
|
112
113
|
| `--no-progress` | Boolean | _(enabled)_ | Disable the TTY progress bar. |
|
|
@@ -168,10 +169,11 @@ schema_version: 1 # opts into strict validation (rejects unknown keys
|
|
|
168
169
|
# timeout: 30 # seconds per mutation
|
|
169
170
|
# format: text # text | json | html
|
|
170
171
|
# min_score: 0.0 # 0.0–1.0
|
|
171
|
-
# integration: rspec # test framework: rspec, minitest
|
|
172
|
+
# integration: rspec # test framework: rspec, minitest, test_unit
|
|
172
173
|
# suggest_tests: false # concrete test code in suggestions (matches integration)
|
|
173
174
|
# save_session: false # persist results under .evilution/results/
|
|
174
175
|
# isolation: auto # auto | fork | in_process (auto selects fork for Rails)
|
|
176
|
+
# canary: true # proof-of-life synthetic mutation at session start (false to skip)
|
|
175
177
|
# preload: null # path to preload before forking; false to disable; auto-detects for Rails
|
|
176
178
|
# skip_heredoc_literals: false # skip string literal mutations inside heredocs (recommended for Rails: heredoc SQL/templates rarely have test coverage)
|
|
177
179
|
# show_disabled: false # report mutations skipped by disable comments
|
|
@@ -211,7 +213,7 @@ All keys recognised under `schema_version: 1`:
|
|
|
211
213
|
| `format` | String | `text` | Output format: `text`, `json`, `html`. |
|
|
212
214
|
| `target` | String / null | `null` | Filter expression: method (`Foo#bar`), class (`Foo`), namespace (`Foo*`), descendants (`descendants:Foo`), source glob (`source:**/*.rb`). |
|
|
213
215
|
| `min_score` | Float | `0.0` | Minimum mutation score (0.0–1.0) for exit code 0. |
|
|
214
|
-
| `integration` | String | `rspec` | Test framework: `rspec` or `
|
|
216
|
+
| `integration` | String | `rspec` | Test framework: `rspec`, `minitest`, or `test_unit`. |
|
|
215
217
|
| `verbose` | Boolean | `false` | Verbose output (RSS/GC stats per phase, error details for errored mutations). |
|
|
216
218
|
| `quiet` | Boolean | `false` | Suppress output. |
|
|
217
219
|
| `jobs` | Integer | `1` | Number of parallel workers. |
|
|
@@ -515,7 +517,7 @@ These fields are added in addition to the existing `operator`, `file`, `line`, `
|
|
|
515
517
|
|
|
516
518
|
The `evilution-mutate` tool accepts a `suggest_tests` boolean parameter (default: `false`). When enabled, survived mutation suggestions contain concrete test code that an agent can drop into a test file, instead of static description text. It currently generates RSpec-style suggestions (`it`/`expect` blocks).
|
|
517
519
|
|
|
518
|
-
Pass `suggest_tests: true` in the `evilution-mutate` call to activate this mode. The CLI also supports `--suggest-tests`; when using the CLI, generated suggestions match the `--integration` setting (RSpec `it`/`expect` blocks or Minitest `def test_`/`assert_equal` methods).
|
|
520
|
+
Pass `suggest_tests: true` in the `evilution-mutate` call to activate this mode. The CLI also supports `--suggest-tests`; when using the CLI, generated suggestions match the `--integration` setting (RSpec `it`/`expect` blocks or Minitest `def test_`/`assert_equal` methods). Concrete templates for `test-unit` are not yet implemented — `test-unit` runs fall back to generic suggestion text.
|
|
519
521
|
|
|
520
522
|
### Project Config File
|
|
521
523
|
|
|
@@ -530,7 +532,7 @@ Pass `skip_config: true` to ignore the project config file. This skips loading `
|
|
|
530
532
|
| Parameter | Purpose |
|
|
531
533
|
|---|---|
|
|
532
534
|
| `incremental` | Cache killed/timeout results across runs — set `true` when iterating on the same files |
|
|
533
|
-
| `integration` | `rspec` or `
|
|
535
|
+
| `integration` | `rspec`, `minitest`, or `test_unit` |
|
|
534
536
|
| `isolation` | `auto`, `fork`, or `in_process` |
|
|
535
537
|
| `baseline` | `false` to skip the baseline suite check when you already know it's green |
|
|
536
538
|
| `save_session` | Persist results to `.evilution/results/` for inspection via `evilution-session` |
|
|
@@ -772,7 +774,7 @@ Tests 4 paths (InProcess isolation, Fork isolation, mutation generation + stripp
|
|
|
772
774
|
3. **Filter** — Disable comments, Sorbet `sig` blocks, and AST ignore patterns exclude mutations before execution
|
|
773
775
|
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
|
|
774
776
|
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
|
|
775
|
-
6. **Test** — The configured test framework (RSpec or
|
|
777
|
+
6. **Test** — The configured test framework (RSpec, Minitest, or Test::Unit) executes against the mutated source
|
|
776
778
|
7. **Collect** — Source strings and AST nodes are released after use to minimize memory retention
|
|
777
779
|
8. **Report** — Results aggregated into text, JSON, or HTML, including efficiency metrics and peak memory usage
|
|
778
780
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Test Framework Integrations
|
|
2
|
+
|
|
3
|
+
Evilution supports three test framework integrations, selected via the
|
|
4
|
+
`--integration NAME` CLI flag (or the `integration:` key in `.evilution.yml`).
|
|
5
|
+
Each integration owns its own framework loader, dispatcher, spec resolver,
|
|
6
|
+
and result builder so they can evolve independently.
|
|
7
|
+
|
|
8
|
+
| Integration | Flag value | Default test dir | Default test suffix | Gem dep |
|
|
9
|
+
|---------------|---------------|------------------|---------------------|------------------------|
|
|
10
|
+
| RSpec | `rspec` | `spec/` | `_spec.rb` | `rspec-core` |
|
|
11
|
+
| Minitest | `minitest` | `test/` | `_test.rb` | `minitest` |
|
|
12
|
+
| Test::Unit | `test-unit` | `test/` | `_test.rb` | `test-unit` |
|
|
13
|
+
|
|
14
|
+
## When to pick which
|
|
15
|
+
|
|
16
|
+
- **`rspec`** (default) — Suites built on `RSpec.describe` / `it` / `expect`.
|
|
17
|
+
- **`minitest`** — Suites that subclass `Minitest::Test` or use
|
|
18
|
+
`Minitest::Spec`'s `describe` / `it`. Includes Rails apps where
|
|
19
|
+
`ActiveSupport::TestCase` inherits from `Minitest::Test`.
|
|
20
|
+
- **`test-unit`** — Suites that subclass `Test::Unit::TestCase`, including
|
|
21
|
+
Rails projects that `require "test/unit/rails/test_help"` (which makes
|
|
22
|
+
`ActiveSupport::TestCase < Test::Unit::TestCase`). The `test-unit` gem's
|
|
23
|
+
`TestCase` classes are *not* `Minitest::Runnable` subclasses, so the
|
|
24
|
+
`minitest` integration cannot dispatch them — pick this integration whenever
|
|
25
|
+
your test helper pulls in `test/unit/rails/test_help`, `test-unit-activerecord`,
|
|
26
|
+
or similar test-unit-specific glue.
|
|
27
|
+
|
|
28
|
+
## CLI examples
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# RSpec — default; specs in spec/, named *_spec.rb
|
|
32
|
+
bundle exec evilution run lib/foo.rb
|
|
33
|
+
|
|
34
|
+
# Minitest
|
|
35
|
+
bundle exec evilution run lib/foo.rb \
|
|
36
|
+
--integration minitest --spec test/foo_test.rb
|
|
37
|
+
|
|
38
|
+
# Test::Unit (gem name uses hyphen; symbol value uses underscore)
|
|
39
|
+
bundle exec evilution run lib/foo.rb \
|
|
40
|
+
--integration test-unit --spec test/foo_test.rb
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The CLI accepts both `test-unit` and `test_unit` strings; the internal config
|
|
44
|
+
symbol is always `:test_unit`.
|
|
45
|
+
|
|
46
|
+
## MCP examples
|
|
47
|
+
|
|
48
|
+
The `evilution-mutate` MCP tool exposes `integration` as a JSON schema enum
|
|
49
|
+
of `["rspec", "minitest", "test-unit"]`:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"tool": "evilution-mutate",
|
|
54
|
+
"args": {
|
|
55
|
+
"files": ["lib/foo.rb"],
|
|
56
|
+
"integration": "test-unit",
|
|
57
|
+
"spec": ["test/foo_test.rb"]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Spec resolution conventions
|
|
63
|
+
|
|
64
|
+
When `--spec` is not supplied, evilution resolves a test file from the source
|
|
65
|
+
path using the integration's spec resolver. Strips `lib/` or `app/` prefix
|
|
66
|
+
and rewrites the suffix:
|
|
67
|
+
|
|
68
|
+
| Source | rspec | minitest / test-unit |
|
|
69
|
+
|-------------------------------------|-------------------------------------|------------------------------------|
|
|
70
|
+
| `lib/foo.rb` | `spec/foo_spec.rb` | `test/foo_test.rb` |
|
|
71
|
+
| `lib/foo/bar.rb` | `spec/foo/bar_spec.rb` | `test/foo/bar_test.rb` |
|
|
72
|
+
| `app/models/user.rb` | `spec/models/user_spec.rb` | `test/models/user_test.rb` |
|
|
73
|
+
| `app/controllers/users_controller.rb` | `spec/requests/users_spec.rb` *or* `spec/controllers/users_controller_spec.rb` | `test/integration/users_test.rb` *or* `test/controllers/users_controller_test.rb` |
|
|
74
|
+
|
|
75
|
+
For controllers, the resolver tries the request-spec / integration-test
|
|
76
|
+
location first, then falls back to the controller-spec / controller-test
|
|
77
|
+
location.
|
|
78
|
+
|
|
79
|
+
## Suggest-tests caveat
|
|
80
|
+
|
|
81
|
+
The `--suggest-tests` mode emits ready-to-paste test snippets for survived
|
|
82
|
+
mutations. Concrete templates currently exist for **rspec** and **minitest**
|
|
83
|
+
only; the **test-unit** integration falls back to the generic operator-level
|
|
84
|
+
suggestion text. Adding test-unit templates is a small follow-up.
|
|
85
|
+
|
|
86
|
+
## Other Ruby test frameworks
|
|
87
|
+
|
|
88
|
+
No additional integrations are planned at the moment. The three integrations
|
|
89
|
+
above cover the overwhelming majority of Ruby projects with unit / integration
|
|
90
|
+
test suites. Frameworks we considered and deferred:
|
|
91
|
+
|
|
92
|
+
- **Cucumber / Spinach** — BDD scenarios over Gherkin step definitions, a
|
|
93
|
+
fundamentally coarser granularity than mutation testing rewards. Not a
|
|
94
|
+
natural fit; no plans to add.
|
|
95
|
+
- **Sus** — Socketry's async-focused framework, small but actively maintained
|
|
96
|
+
user base. Could be added under the same orchestrator/collaborator pattern
|
|
97
|
+
if a real project surfaces needing it.
|
|
98
|
+
- **Bacon, Test::Spec** — early RSpec-style clones, effectively dormant.
|
|
99
|
+
- **Minitest::Spec** — already covered by the `minitest` integration.
|
|
100
|
+
|
|
101
|
+
If you maintain a project on another framework and would benefit from
|
|
102
|
+
mutation-testing it through evilution, open an issue describing the project
|
|
103
|
+
and the framework's dispatch entry-point — the existing integration layout
|
|
104
|
+
makes new entries cheap to add.
|
|
105
|
+
|
|
106
|
+
## Behind the scenes
|
|
107
|
+
|
|
108
|
+
Each integration has an orchestrator class at
|
|
109
|
+
`lib/evilution/integration/<name>.rb`. RSpec and Test::Unit additionally
|
|
110
|
+
decompose their collaborators into a sibling directory of the same name
|
|
111
|
+
(`lib/evilution/integration/rspec/...`,
|
|
112
|
+
`lib/evilution/integration/test_unit/...`); Minitest remains a single file.
|
|
113
|
+
The test-unit collaborators are:
|
|
114
|
+
|
|
115
|
+
- `framework_loader.rb` — `require "test-unit"` + disables the at_exit
|
|
116
|
+
auto-run handler so it doesn't fire on evilution exit.
|
|
117
|
+
- `subject_class_registry.rb` — ObjectSpace tracking of newly-loaded
|
|
118
|
+
`Test::Unit::TestCase` subclasses (test-unit has no public
|
|
119
|
+
registry-clear analog to `Minitest::Runnable.runnables`).
|
|
120
|
+
- `dispatcher.rb` — assembles a `Test::Unit::TestSuite` from the new
|
|
121
|
+
classes and runs it via `Test::Unit::UI::Console::TestRunner` with
|
|
122
|
+
output captured to a `StringIO`.
|
|
123
|
+
- `test_file_resolver.rb` — explicit-override + spec-selector +
|
|
124
|
+
fallback glob + warn-once for unresolved sources.
|
|
125
|
+
- `result_builder.rb` — shapes the `passed`/`test_crashed`/
|
|
126
|
+
`no_tests_ran`/`unresolved` Hash that flows into `classify_status`.
|
data/docs/isolation.md
CHANGED
|
@@ -103,6 +103,33 @@ the MCP server is a long-lived process that handles runs from different
|
|
|
103
103
|
projects — preloading one project's Rails stack into a shared process would
|
|
104
104
|
poison subsequent runs.
|
|
105
105
|
|
|
106
|
+
## Sandboxed working directory
|
|
107
|
+
|
|
108
|
+
Every isolator runs `test_command.call` inside a per-mutation scratch
|
|
109
|
+
directory (`Dir.mktmpdir("evilution-run")`) that is removed on the way out.
|
|
110
|
+
Mutations that turn an absolute path into a relative one — for example
|
|
111
|
+
`argument_removal` on `File.join(dir, name)` collapsing to `File.write(name,
|
|
112
|
+
…)` — would otherwise drop files into the parent process's CWD (typically
|
|
113
|
+
the repo root) and leak past the run.
|
|
114
|
+
|
|
115
|
+
Project-relative paths used by `SpecResolver`, `SpecSelector`, the
|
|
116
|
+
`RSpec::Core::Runner` / `Minitest.load` invocation sites, and the mutated
|
|
117
|
+
file's `__FILE__` in `SourceEvaluator` are anchored at
|
|
118
|
+
`Evilution::PROJECT_ROOT` (captured at load time), so the sandbox CWD does
|
|
119
|
+
not break spec lookup or `require_relative` chains inside the mutant.
|
|
120
|
+
|
|
121
|
+
## Proof-of-life canary
|
|
122
|
+
|
|
123
|
+
Before any real mutation runs, `Evilution::Runner::Canary` evals a
|
|
124
|
+
synthetic, guaranteed-unobservable mutation through the configured
|
|
125
|
+
integration + isolation. A healthy pipeline must score it `:survived`; any
|
|
126
|
+
other status aborts the run with a diagnostic so the user never sees a
|
|
127
|
+
score produced by a broken pipeline (autoload mismatch, reporter-plugin
|
|
128
|
+
eviction, isolation defect, `fail_if_no_examples` config drift, etc.).
|
|
129
|
+
On by default; toggle with `--[no-]canary` or `canary: true|false` in
|
|
130
|
+
`.evilution.yml`. The canary mirrors the configured `--isolation` so
|
|
131
|
+
isolation-specific defects are caught too.
|
|
132
|
+
|
|
106
133
|
## Related flags
|
|
107
134
|
|
|
108
135
|
- `--timeout N` sets the per-mutation time limit. Under `fork`, this drives
|
|
@@ -111,5 +138,6 @@ poison subsequent runs.
|
|
|
111
138
|
- `--jobs N` runs N workers in parallel. The parallel pool respects the
|
|
112
139
|
configured isolation strategy, so `--jobs 4 --isolation fork` uses fork
|
|
113
140
|
isolation per-mutation inside each worker.
|
|
141
|
+
- `--[no-]canary` toggles the proof-of-life pre-flight check (see above).
|
|
114
142
|
|
|
115
143
|
[rails-handle-interrupt]: https://github.com/rails/rails/blob/main/activesupport/lib/active_support/concurrency/thread_monitor.rb
|
|
@@ -86,11 +86,16 @@ class Evilution::CLI::Parser::OptionsBuilder
|
|
|
86
86
|
"Use --no-incremental to override `incremental: true` from the config file for one run.") do |v|
|
|
87
87
|
@options[:incremental] = v
|
|
88
88
|
end
|
|
89
|
+
opts.on("--[no-]canary",
|
|
90
|
+
"Run a proof-of-life synthetic mutation at session start; abort if " \
|
|
91
|
+
"the pipeline misreports it (default: enabled). Use --no-canary to skip.") do |v|
|
|
92
|
+
@options[:canary] = v
|
|
93
|
+
end
|
|
89
94
|
opts.on("--stdin", "Read target file paths from stdin (one per line)") { @options[:stdin] = true }
|
|
90
95
|
end
|
|
91
96
|
|
|
92
97
|
def add_runner_mode_options(opts)
|
|
93
|
-
opts.on("--integration NAME", "Test integration: rspec, minitest (default: rspec)") { |i| @options[:integration] = i }
|
|
98
|
+
opts.on("--integration NAME", "Test integration: rspec, minitest, test-unit (default: rspec)") { |i| @options[:integration] = i }
|
|
94
99
|
opts.on("--isolation STRATEGY", "Isolation: auto, fork, in_process (default: auto)") { |s| @options[:isolation] = s }
|
|
95
100
|
opts.on("--preload FILE", "Preload FILE in the parent process before forking " \
|
|
96
101
|
"(default: auto-detect spec/rails_helper.rb -> spec/spec_helper.rb -> " \
|
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
require_relative "base"
|
|
4
4
|
|
|
5
5
|
class Evilution::Config::Validators::Integration < Evilution::Config::Validators::Base
|
|
6
|
-
ALLOWED = %i[rspec minitest].freeze
|
|
6
|
+
ALLOWED = %i[rspec minitest test_unit].freeze
|
|
7
7
|
|
|
8
|
+
# CLI users naturally write the gem name `test-unit`; the internal symbol
|
|
9
|
+
# uses underscore form to match the file path and registry key. Normalize
|
|
10
|
+
# hyphenated string input before coercion.
|
|
8
11
|
def self.call(value)
|
|
12
|
+
value = value.tr("-", "_") if value.is_a?(String)
|
|
9
13
|
coerce_symbol!(value, allowed: ALLOWED, name: "integration")
|
|
10
14
|
end
|
|
11
15
|
end
|
data/lib/evilution/config.rb
CHANGED
|
@@ -20,7 +20,7 @@ class Evilution::Config
|
|
|
20
20
|
example_targeting_fallback: :full_file,
|
|
21
21
|
example_targeting_cache: { max_files: 50, max_blocks: 10_000 },
|
|
22
22
|
quiet_children: false, quiet_children_dir: "tmp/evilution_children",
|
|
23
|
-
profile: :default
|
|
23
|
+
profile: :default, canary: true
|
|
24
24
|
}.freeze
|
|
25
25
|
|
|
26
26
|
attr_reader :target_files, :schema_version, :timeout, :format,
|
|
@@ -31,7 +31,7 @@ class Evilution::Config
|
|
|
31
31
|
:skip_heredoc_literals, :related_specs_heuristic,
|
|
32
32
|
:fallback_to_full_suite, :preload, :spec_mappings, :spec_pattern,
|
|
33
33
|
:example_targeting, :example_targeting_fallback, :example_targeting_cache,
|
|
34
|
-
:spec_selector, :quiet_children, :quiet_children_dir, :profile
|
|
34
|
+
:spec_selector, :quiet_children, :quiet_children_dir, :profile, :canary
|
|
35
35
|
|
|
36
36
|
def initialize(**options)
|
|
37
37
|
skip_file = options.delete(:skip_config_file) ? true : false
|
|
@@ -68,6 +68,10 @@ class Evilution::Config
|
|
|
68
68
|
baseline
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
def canary?
|
|
72
|
+
canary
|
|
73
|
+
end
|
|
74
|
+
|
|
71
75
|
def incremental?
|
|
72
76
|
incremental
|
|
73
77
|
end
|
|
@@ -130,7 +134,12 @@ class Evilution::Config
|
|
|
130
134
|
# Minimum mutation score to pass (0.0 to 1.0, default: 0.0)
|
|
131
135
|
# min_score: 0.0
|
|
132
136
|
|
|
133
|
-
#
|
|
137
|
+
# Proof-of-life canary: run a synthetic, guaranteed-unobservable
|
|
138
|
+
# mutation at session start and abort if the pipeline misreports it
|
|
139
|
+
# (default: true). Set false to skip (e.g. for CI speed).
|
|
140
|
+
# canary: true
|
|
141
|
+
|
|
142
|
+
# Test integration: rspec, minitest, test_unit (default: rspec)
|
|
134
143
|
# integration: rspec
|
|
135
144
|
|
|
136
145
|
# Number of parallel workers (default: 1)
|
|
@@ -237,7 +246,8 @@ class Evilution::Config
|
|
|
237
246
|
related_specs_heuristic: nil,
|
|
238
247
|
fallback_to_full_suite: nil,
|
|
239
248
|
quiet_children: nil,
|
|
240
|
-
quiet_children_dir: nil
|
|
249
|
+
quiet_children_dir: nil,
|
|
250
|
+
canary: nil
|
|
241
251
|
}.freeze
|
|
242
252
|
private_constant :SIMPLE_ATTR_TRANSFORMS
|
|
243
253
|
|
|
@@ -56,21 +56,29 @@ class Evilution::Integration::Loading::MutationApplier
|
|
|
56
56
|
def apply(mutation, eval_target)
|
|
57
57
|
@constant_pinner.call(mutation.original_source)
|
|
58
58
|
@concern_state_cleaner.call(mutation.file_path)
|
|
59
|
+
mark_feature_loaded(mutation.file_path)
|
|
59
60
|
@redefinition_recovery.call(mutation.original_source) do
|
|
60
61
|
@source_evaluator.call(eval_target, mutation.file_path)
|
|
61
62
|
end
|
|
62
|
-
mark_feature_loaded(mutation.file_path)
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
# The mutated source is eval'd straight into the VM — `eval` does not register
|
|
66
|
-
# a `$LOADED_FEATURES` entry.
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
66
|
+
# a `$LOADED_FEATURES` entry. Any later `require`/`require_relative` of the
|
|
67
|
+
# same file then reloads the ORIGINAL from disk and clobbers the mutation, so
|
|
68
|
+
# every mutation silently survives. Two paths trigger that reload:
|
|
69
|
+
# - the spec `require`s the file (it lazy-loads it and only the test
|
|
70
|
+
# references it);
|
|
71
|
+
# - the mutated source's OWN body `require_relative`s a sibling whose body
|
|
72
|
+
# `require_relative`s this file back (e.g. lib/evilution/mcp/*.rb tools).
|
|
73
|
+
# The second reload happens DURING the eval, so registration must precede it:
|
|
74
|
+
# `mark_feature_loaded` runs before `@source_evaluator.call`, not after. Under
|
|
75
|
+
# fork isolation each worker starts from the same pre-`require` snapshot, so
|
|
76
|
+
# without this the whole file scores 0%.
|
|
72
77
|
def mark_feature_loaded(file_path)
|
|
73
|
-
|
|
78
|
+
# When the isolator has chdir'd into a per-mutation sandbox (EV-wqxu /
|
|
79
|
+
# GH #1278), anchor against PROJECT_ROOT so File.realpath does not chase
|
|
80
|
+
# file_path into a non-existent /tmp path.
|
|
81
|
+
absolute = File.realpath(File.expand_path(file_path, Evilution.project_base_dir))
|
|
74
82
|
$LOADED_FEATURES << absolute unless $LOADED_FEATURES.include?(absolute)
|
|
75
83
|
rescue Errno::ENOENT
|
|
76
84
|
nil
|
|
@@ -13,7 +13,10 @@ require_relative "../loading"
|
|
|
13
13
|
# substitute the mutated bytes — the privilege level is identical.
|
|
14
14
|
class Evilution::Integration::Loading::SourceEvaluator
|
|
15
15
|
def call(source, file_path)
|
|
16
|
-
|
|
16
|
+
# When the isolator has chdir'd into a per-mutation sandbox (EV-wqxu /
|
|
17
|
+
# GH #1278), anchor the eval __FILE__ against PROJECT_ROOT so siblings
|
|
18
|
+
# `require_relative` can find each other from the real source tree.
|
|
19
|
+
absolute = File.expand_path(file_path, Evilution.project_base_dir)
|
|
17
20
|
eval(source, TOPLEVEL_BINDING, absolute, 1)
|
|
18
21
|
end
|
|
19
22
|
end
|
|
@@ -128,7 +128,7 @@ class Evilution::Integration::Minitest < Evilution::Integration::Base
|
|
|
128
128
|
end
|
|
129
129
|
|
|
130
130
|
def execute_minitest(mutation, files, command)
|
|
131
|
-
files.each { |f| load(File.expand_path(f)) }
|
|
131
|
+
files.each { |f| load(File.expand_path(f, Evilution.project_base_dir)) }
|
|
132
132
|
|
|
133
133
|
detector = reset_crash_detector
|
|
134
134
|
run = run_minitest(build_args(mutation), detector)
|
|
@@ -5,7 +5,9 @@ require_relative "../rspec"
|
|
|
5
5
|
class Evilution::Integration::RSpec::BaselineRunner
|
|
6
6
|
def call(spec_file)
|
|
7
7
|
require "rspec/core"
|
|
8
|
-
|
|
8
|
+
# Anchor against PROJECT_ROOT under EV-wqxu sandbox CWD; see
|
|
9
|
+
# FrameworkLoader#add_spec_load_path for rationale.
|
|
10
|
+
spec_dir = File.expand_path("spec", Evilution.project_base_dir)
|
|
9
11
|
$LOAD_PATH.unshift(spec_dir) unless $LOAD_PATH.include?(spec_dir)
|
|
10
12
|
::RSpec.reset
|
|
11
13
|
status = ::RSpec::Core::Runner.run(
|
|
@@ -22,7 +22,11 @@ class Evilution::Integration::RSpec::FrameworkLoader
|
|
|
22
22
|
private
|
|
23
23
|
|
|
24
24
|
def add_spec_load_path
|
|
25
|
-
|
|
25
|
+
# Anchor against PROJECT_ROOT inside an isolated worker (EV-wqxu /
|
|
26
|
+
# GH #1278) so the project's spec/ dir lands on $LOAD_PATH — otherwise
|
|
27
|
+
# `require "spec_helper"` resolves to a non-existent sandbox/spec and
|
|
28
|
+
# every mutation errors as "loaded 0 examples".
|
|
29
|
+
spec_dir = File.expand_path("spec", Evilution.project_base_dir)
|
|
26
30
|
$LOAD_PATH.unshift(spec_dir) unless $LOAD_PATH.include?(spec_dir)
|
|
27
31
|
end
|
|
28
32
|
end
|
|
@@ -70,13 +70,50 @@ class Evilution::Integration::RSpec < Evilution::Integration::Base
|
|
|
70
70
|
targets = @example_filter_applier.call(mutation, files)
|
|
71
71
|
return @result_builder.unresolved_example(mutation) if targets.nil?
|
|
72
72
|
|
|
73
|
-
args = ["--format", "progress", "--no-color", "--order", "defined", *targets]
|
|
73
|
+
args = ["--format", "progress", "--no-color", "--order", "defined", *resolve_targets(targets)]
|
|
74
74
|
command = "rspec #{args.join(" ")}"
|
|
75
75
|
|
|
76
76
|
reset_examples
|
|
77
77
|
execute_run(args, command)
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
+
# Targets are passed straight through when they resolve against Dir.pwd,
|
|
81
|
+
# preserving the relative-path contract for callers/tests. When the CWD
|
|
82
|
+
# cannot find them — workers chdir'd into a per-mutation sandbox by the
|
|
83
|
+
# isolator (EV-wqxu / GH #1278) — the targets are expanded against
|
|
84
|
+
# Evilution::PROJECT_ROOT so RSpec::Core::Runner can still load the files.
|
|
85
|
+
def resolve_targets(targets)
|
|
86
|
+
targets.map { |target| resolve_target(target) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Splits a target into (path, line). RSpec example locations end in
|
|
90
|
+
# `:LINE` (or `:LINE:LINE...` for nested groups), so the suffix is
|
|
91
|
+
# anchored to the END of the string — that way path components which
|
|
92
|
+
# themselves contain colons (e.g. a Windows-style `C:/proj/spec/x.rb`)
|
|
93
|
+
# are not mis-split by a naive `partition(":")`.
|
|
94
|
+
TARGET_LINE_SUFFIX = /\A(.+?)(:\d+(?::\d+)*)\z/
|
|
95
|
+
private_constant :TARGET_LINE_SUFFIX
|
|
96
|
+
|
|
97
|
+
def resolve_target(target)
|
|
98
|
+
return target if target.start_with?("/")
|
|
99
|
+
|
|
100
|
+
if (match = TARGET_LINE_SUFFIX.match(target))
|
|
101
|
+
path = match[1]
|
|
102
|
+
line_suffix = match[2]
|
|
103
|
+
else
|
|
104
|
+
path = target
|
|
105
|
+
line_suffix = ""
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
return target if File.exist?(path)
|
|
109
|
+
return target unless Evilution.in_isolated_worker?
|
|
110
|
+
|
|
111
|
+
absolute = File.expand_path(path, Evilution::PROJECT_ROOT)
|
|
112
|
+
return target unless File.exist?(absolute)
|
|
113
|
+
|
|
114
|
+
"#{absolute}#{line_suffix}"
|
|
115
|
+
end
|
|
116
|
+
|
|
80
117
|
def execute_run(args, command)
|
|
81
118
|
detector = @crash_detector_lifecycle.current
|
|
82
119
|
snapshot = @state_guard.snapshot
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
require_relative "../test_unit"
|
|
5
|
+
|
|
6
|
+
# Builds a Test::Unit::TestSuite from a list of TestCase subclasses and runs
|
|
7
|
+
# it via the console runner with output captured to a StringIO. Owning this
|
|
8
|
+
# responsibility separately keeps the runner library require + suite assembly
|
|
9
|
+
# in one place — used by both the baseline path (Evilution::Integration::TestUnit
|
|
10
|
+
# .run_baseline_test_file) and the per-mutation path (#run_tests).
|
|
11
|
+
module Evilution::Integration::TestUnit::Dispatcher
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def call(test_case_classes, name: "evilution")
|
|
15
|
+
require "test/unit/ui/console/testrunner"
|
|
16
|
+
suite = ::Test::Unit::TestSuite.new(name)
|
|
17
|
+
test_case_classes.each { |klass| suite << klass.suite }
|
|
18
|
+
out = StringIO.new
|
|
19
|
+
runner = ::Test::Unit::UI::Console::TestRunner.new(suite, output: out)
|
|
20
|
+
runner.start
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_method_count(test_case_classes)
|
|
24
|
+
test_case_classes.sum { |klass| klass.suite.tests.length }
|
|
25
|
+
end
|
|
26
|
+
end
|