evilution 0.30.0 → 0.30.2
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 +13 -0
- data/.rubocop_todo.yml +1 -0
- data/CHANGELOG.md +14 -0
- data/lib/evilution/integration/minitest.rb +14 -0
- data/lib/evilution/integration/minitest_crash_detector.rb +4 -0
- data/lib/evilution/mutator/operator/method_body_replacement.rb +19 -3
- data/lib/evilution/runner/isolation_resolver.rb +14 -0
- data/lib/evilution/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: af414a5edcfb3e25beea8ef2dea4235f7f9cadc1d8e086a526846ff456639235
|
|
4
|
+
data.tar.gz: a32bc1f4dcf1898dc8be020356ca746354384894994cab0dac5e8d0b25692ebb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b65c3fd7ef73fccbf08a9d3dcf63c6e2160fa4ef3625139c7a8c4be137b52dcf55bd4b84da58d80862bf2f34cbf022bd2a1c937f474a292eb5368dde816fd0fd
|
|
7
|
+
data.tar.gz: 80b60c39df9dadb4193a7d65ff0fe22bfa3e395f658b13da53a8042699e7e9d869920a4ab127759d3e975cecdcd0821bc3bdfff6c2f482f6672383b2e924c3a4
|
data/.beads/interactions.jsonl
CHANGED
|
@@ -364,3 +364,16 @@
|
|
|
364
364
|
{"id":"int-78870411","kind":"field_change","created_at":"2026-05-14T07:28:17.94685909Z","actor":"Denis Kiselev","issue_id":"EV-05tp","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"PR merged: explicit_super_mutation extends byte range structurally (block start / rparen / args end). Short-circuit spec switched to synthetic in-test operator."}}
|
|
365
365
|
{"id":"int-36e5c972","kind":"field_change","created_at":"2026-05-14T08:23:06.045690955Z","actor":"Denis Kiselev","issue_id":"EV-b0ee","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"PR merged: GemDetector multi-gemspec disambiguation via exact-entry, lib-subdir, then shortest-name fallback."}}
|
|
366
366
|
{"id":"int-00368bf4","kind":"field_change","created_at":"2026-05-14T09:07:21.14041556Z","actor":"Denis Kiselev","issue_id":"EV-blnq","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Not a hang — slow run with no progress feedback. Investigated 2026-05-14: 269 mutations / 202 killed / 42 survived / 25 timed_out / 0 errors / score 0.75 in 4min with -j 4 -t 10 on Shopify/liquid v5.12.0. Per-mutation Minitest test_helper re-bootstrap + 25 worker timeouts caused the perceived 10-min hang. Combined with --no-progress + non-TTY stderr (no parent output) the run looks silent until the JSON dump. Worker logs (--quiet-children --quiet-children-dir DIR) show steady progress. README updated with a 'Long Minitest fork runs — not a hang' section under §7 explaining the timing math + recommended -j/-t flags. No code change needed."}}
|
|
367
|
+
{"id":"int-00d1068f","kind":"field_change","created_at":"2026-05-15T04:10:03.085170043Z","actor":"Denis Kiselev","issue_id":"EV-lqpn","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
|
|
368
|
+
{"id":"int-0f978a1b","kind":"field_change","created_at":"2026-05-15T04:10:03.499346683Z","actor":"Denis Kiselev","issue_id":"EV-720r","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
|
|
369
|
+
{"id":"int-27d908cc","kind":"field_change","created_at":"2026-05-15T04:10:03.883249046Z","actor":"Denis Kiselev","issue_id":"EV-70hd","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
|
|
370
|
+
{"id":"int-5c439789","kind":"field_change","created_at":"2026-05-15T04:10:04.253005681Z","actor":"Denis Kiselev","issue_id":"EV-m47s","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
|
|
371
|
+
{"id":"int-9c968bc0","kind":"field_change","created_at":"2026-05-15T04:10:02.826901138Z","actor":"Denis Kiselev","issue_id":"EV-74e3","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
|
|
372
|
+
{"id":"int-4677d561","kind":"field_change","created_at":"2026-05-15T05:18:42.843114482Z","actor":"Denis Kiselev","issue_id":"EV-ju3o","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
373
|
+
{"id":"int-f977cb7c","kind":"field_change","created_at":"2026-05-15T06:37:09.06468763Z","actor":"Denis Kiselev","issue_id":"EV-mn3p","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Canary PASS 0.30.1. client.rb 882 mutations 100% score, 3 equivalent. capsule.rb 6.85% — upstream test debt, not evilution bug. No evilution bugs surfaced. Artifact .artifacts/sidekiq_sidekiq.yml."}}
|
|
374
|
+
{"id":"int-1b07bd8c","kind":"field_change","created_at":"2026-05-15T06:50:28.638714936Z","actor":"Denis Kiselev","issue_id":"EV-7764","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Canary infra mismatch — pagy uses thor-driven multi-file test orchestration that single-spec evilution invocation can't replicate. No evilution bug surfaced. Documented in artifact."}}
|
|
375
|
+
{"id":"int-47573a1f","kind":"field_change","created_at":"2026-05-15T06:51:44.810876255Z","actor":"Denis Kiselev","issue_id":"EV-ff13","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Canary PASS 0.30.1+EV-5nxs. Baseline 509/0/0. object_utils.rb 57 mutations 100%, candidates.rb 100% (21 muts/s). No evilution bugs. Artifact .artifacts/norman_friendly_id.yml."}}
|
|
376
|
+
{"id":"int-296f712f","kind":"field_change","created_at":"2026-05-15T06:57:54.666163064Z","actor":"Denis Kiselev","issue_id":"EV-9cd2","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Canary smoke_fail 2026-05-15. Baseline 431/0/0. All targeted mutations 0% kill. Same pattern as pagy. Suspected ActiveSupport::TestCase + reset_state + class-reopen gap in evilution Minitest integration but no minimal repro yet. Not filed as separate bug — needs smaller reproducer. Artifact .artifacts/kaminari_kaminari.yml."}}
|
|
377
|
+
{"id":"int-90ffe424","kind":"field_change","created_at":"2026-05-15T06:58:55.664447898Z","actor":"Denis Kiselev","issue_id":"EV-225l","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Canary SKIPPED 2026-05-15. test_helper.rb spawns its own redis-server (no env override); host has no redis-server binary. Sidekiq EV-mn3p already covers concurrency tier signal (100% PASS on client.rb). Resque revisit only justified if a resque-specific bug surfaces later. Artifact .artifacts/resque_resque.yml."}}
|
|
378
|
+
{"id":"int-0648236b","kind":"field_change","created_at":"2026-05-15T07:00:59.017162278Z","actor":"Denis Kiselev","issue_id":"EV-ks6i","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Canary PASS 0.30.1+EV-5nxs. configuration.rb 40.69% upstream gap. body.rb 66.78% upstream gap. 0 errored mutations across both targets. No evilution bugs. Artifact .artifacts/mikel_mail.yml."}}
|
|
379
|
+
{"id":"int-8a14f28f","kind":"field_change","created_at":"2026-05-15T08:24:58.326924498Z","actor":"Denis Kiselev","issue_id":"EV-5nxs","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
data/.rubocop_todo.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
Versioning policy: see [docs/versioning.md](docs/versioning.md).
|
|
4
4
|
|
|
5
|
+
## [0.30.2] - 2026-05-15
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`method_body_replacement` no longer corrupts methods with a method-level `rescue`/`ensure`** — for the shorthand form `def foo; stmts; rescue => e; ...; end` (no explicit `begin`), Prism makes `DefNode#body` a `BeginNode` whose `location` spans the *entire* `def...end` block — the `def` keyword and matching `end` included. The operator replaced that whole range with `nil` / `self` / `super`, obliterating the method framing and leaving the replacement dangling at the enclosing class/module scope; `super` replacements then raised `NoMethodError: super called outside of method` at load time, and the run miscounted every such mutation as an error. The operator now targets only the leading statements (`body.statements`) when the body is a `BeginNode`, preserving the `rescue`/`else`/`ensure` clauses and the `def` framing; methods with a rescue/ensure-only body (no leading statements) emit no mutation. Super *detection* still scans the full body, so a `super` call in any clause keeps the bare-super replacement candidate. Surfaced by the EV-5rtm redis-rb stability canary — 5 methods in `lib/redis/client.rb` (`ensure_connected`, `call_v`, `blocking_call_v`, `pipelined`, `multi`) all-errored before this fix (EV-9a6c, PR #1251, GH #1247)
|
|
10
|
+
- **`Minitest.autorun` stub now runs before the user `--preload` file** — the EV-7u9c fix (0.30.1) stubbed `Minitest.autorun` from `Integration::Minitest#ensure_framework_loaded` and `run_baseline_test_file`, both of which execute *after* `Runner#perform_preload`. When the user passed `--preload <file>` and that file required `minitest/autorun` (typical for `spec_helper.rb` / `test_helper.rb`), the `at_exit` handler was installed during preload — before the stub took effect — and the misleading "invalid option: --integration" banner returned at process exit. `Evilution::Runner::IsolationResolver#perform_preload` now invokes `Evilution::Integration::Minitest.stub_autorun!` immediately before requiring the preload file when `config.integration == :minitest`; it is a no-op for the RSpec integration. Surfaced by the EV-xqv3 rouge stability canary, whose `spec/spec_helper.rb` requires `minitest/autorun` (EV-5nxs, PR #1249, GH #1248)
|
|
11
|
+
|
|
12
|
+
## [0.30.1] - 2026-05-15
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **Minitest 5.11+ / 6.x reporter contract: `MinitestCrashDetector` now implements `prerecord(klass, name)`** — `Minitest::AbstractReporter` calls `prerecord` on every reporter immediately before each test runs; the crash detector implemented `start` / `report` / `record` / `passed?` but not `prerecord`, so on any project using Minitest ≥ 5.11 every mutation aborted with `undefined method 'prerecord' for an instance of Evilution::Integration::MinitestCrashDetector` and the run reported score 0.0 / all-errored regardless of actual test behavior. PR #1207 (EV-l6gx) addressed the `run_all_suites` vs `__run` dispatch gap but did not audit the reporter interface; this patch closes the remaining hole. Surfaced by the EV-5rtm redis-rb stability canary against Minitest 6.0.6 (EV-ju3o, PR #1243, GH #1240)
|
|
17
|
+
- **`Minitest.autorun` no longer installs an at-exit handler when invoked by evilution-loaded user helpers** — Minitest-based projects (redis-rb, mail, others) routinely have `test/helper.rb` do `require "minitest/autorun"`, which installs an `at_exit` block calling `Minitest.run(ARGV)`. Evilution loads those helpers during baseline and per-mutation execution, so the handler ran at evilution's own process exit, saw the original `ARGV` (still carrying `--integration`, `--spec`, `--preload`, ...), and Minitest's option parser printed a misleading "invalid option: --integration" usage banner after the mutation summary. `Evilution::Integration::Minitest.stub_autorun!` is now invoked right after `require "minitest"` in both the orchestrator's baseline path (`run_baseline_test_file`) and the per-instance `ensure_framework_loaded`; it redefines `Minitest.autorun` to a no-op (idempotently, keyed on the redefined method's `source_location`) so subsequent `require "minitest/autorun"` calls in user code never register the handler. Cosmetic-only fix: mutation scoring and exit code were already correct (EV-7u9c, PR #1244, GH #1241)
|
|
18
|
+
|
|
5
19
|
## [0.30.0] - 2026-05-15
|
|
6
20
|
|
|
7
21
|
### Added
|
|
@@ -16,11 +16,24 @@ class Evilution::Integration::Minitest < Evilution::Integration::Base
|
|
|
16
16
|
def self.run_baseline_test_file(test_file)
|
|
17
17
|
require "minitest"
|
|
18
18
|
require "stringio"
|
|
19
|
+
stub_autorun!
|
|
19
20
|
::Minitest::Runnable.runnables.clear
|
|
20
21
|
baseline_test_files(test_file).each { |f| load(File.expand_path(f)) }
|
|
21
22
|
run_baseline_minitest
|
|
22
23
|
end
|
|
23
24
|
|
|
25
|
+
# User helpers that `require "minitest/autorun"` install an at_exit handler
|
|
26
|
+
# calling `Minitest.run(ARGV)`. At evilution process exit ARGV still holds
|
|
27
|
+
# evilution flags (--integration, --spec, ...) and Minitest's option parser
|
|
28
|
+
# prints a misleading "invalid option" banner. Stubbing autorun before user
|
|
29
|
+
# code loads prevents the handler from ever being installed.
|
|
30
|
+
def self.stub_autorun!
|
|
31
|
+
location = ::Minitest.singleton_class.instance_method(:autorun).source_location
|
|
32
|
+
return if location && location.first == __FILE__
|
|
33
|
+
|
|
34
|
+
::Minitest.define_singleton_method(:autorun) { nil }
|
|
35
|
+
end
|
|
36
|
+
|
|
24
37
|
def self.baseline_test_files(test_file)
|
|
25
38
|
File.directory?(test_file) ? Dir.glob(File.join(test_file, "**/*_test.rb")) : [test_file]
|
|
26
39
|
end
|
|
@@ -96,6 +109,7 @@ class Evilution::Integration::Minitest < Evilution::Integration::Base
|
|
|
96
109
|
|
|
97
110
|
fire_hook(:setup_integration_pre, integration: :minitest)
|
|
98
111
|
require "minitest"
|
|
112
|
+
self.class.stub_autorun!
|
|
99
113
|
@minitest_loaded = true
|
|
100
114
|
fire_hook(:setup_integration_post, integration: :minitest)
|
|
101
115
|
rescue LoadError => e
|
|
@@ -11,6 +11,10 @@ class Evilution::Integration::MinitestCrashDetector
|
|
|
11
11
|
# Required by Minitest reporter interface
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
def prerecord(_klass, _name)
|
|
15
|
+
# Required by Minitest::AbstractReporter (5.11+/6.x) before each test
|
|
16
|
+
end
|
|
17
|
+
|
|
14
18
|
def report
|
|
15
19
|
# Required by Minitest reporter interface
|
|
16
20
|
end
|
|
@@ -7,14 +7,18 @@ class Evilution::Mutator::Operator::MethodBodyReplacement < Evilution::Mutator::
|
|
|
7
7
|
SUPER_REPLACEMENT = "super"
|
|
8
8
|
|
|
9
9
|
def visit_def_node(node)
|
|
10
|
-
|
|
10
|
+
target = mutation_target(node.body)
|
|
11
|
+
if target
|
|
11
12
|
replacements = ALWAYS_SAFE_REPLACEMENTS.dup
|
|
13
|
+
# Super detection scans the whole body (rescue/else/ensure clauses
|
|
14
|
+
# included) — a super call in any clause means a parent target exists,
|
|
15
|
+
# so a bare-super replacement of the statements is meaningful.
|
|
12
16
|
replacements << SUPER_REPLACEMENT if body_calls_super?(node.body)
|
|
13
17
|
|
|
14
18
|
replacements.each do |replacement|
|
|
15
19
|
add_mutation(
|
|
16
|
-
offset:
|
|
17
|
-
length:
|
|
20
|
+
offset: target.location.start_offset,
|
|
21
|
+
length: target.location.length,
|
|
18
22
|
replacement: replacement,
|
|
19
23
|
node: node
|
|
20
24
|
)
|
|
@@ -26,6 +30,18 @@ class Evilution::Mutator::Operator::MethodBodyReplacement < Evilution::Mutator::
|
|
|
26
30
|
|
|
27
31
|
private
|
|
28
32
|
|
|
33
|
+
# A method-level rescue/ensure (`def foo; stmts; rescue; ...; end`) makes
|
|
34
|
+
# node.body a BeginNode whose location spans the entire `def...end` — the
|
|
35
|
+
# `def` keyword and matching `end` included. Replacing that range obliterates
|
|
36
|
+
# the method framing, leaving the replacement (`nil`/`self`/`super`) dangling
|
|
37
|
+
# at the enclosing scope. The replaceable region is only the leading
|
|
38
|
+
# statements; returns nil when there are none (rescue/ensure-only body).
|
|
39
|
+
def mutation_target(body)
|
|
40
|
+
return body unless body.is_a?(Prism::BeginNode)
|
|
41
|
+
|
|
42
|
+
body.statements
|
|
43
|
+
end
|
|
44
|
+
|
|
29
45
|
# The bare-super replacement raises NoMethodError at runtime when the enclosing
|
|
30
46
|
# class has no parent implementation of the method. We emit it only when the
|
|
31
47
|
# original body already calls super, using that as a heuristic that a super
|
|
@@ -37,6 +37,7 @@ class Evilution::Runner::IsolationResolver
|
|
|
37
37
|
return unless path
|
|
38
38
|
|
|
39
39
|
prepare_load_path_for_preload
|
|
40
|
+
prepare_integration_for_preload
|
|
40
41
|
require File.expand_path(path)
|
|
41
42
|
rescue Evilution::ConfigError
|
|
42
43
|
raise
|
|
@@ -186,6 +187,19 @@ class Evilution::Runner::IsolationResolver
|
|
|
186
187
|
raise Evilution::ConfigError.new("preload file not found: #{path.inspect}", file: path)
|
|
187
188
|
end
|
|
188
189
|
|
|
190
|
+
# User preload files (spec_helper.rb, test_helper.rb) typically require
|
|
191
|
+
# 'minitest/autorun', which installs an at_exit handler that re-parses ARGV
|
|
192
|
+
# at process exit. The stub from Integration::Minitest#ensure_framework_loaded
|
|
193
|
+
# only fires during baseline — too late to prevent the handler from being
|
|
194
|
+
# registered. Stub before user code runs.
|
|
195
|
+
def prepare_integration_for_preload
|
|
196
|
+
return unless config.integration.to_s == "minitest"
|
|
197
|
+
|
|
198
|
+
require "minitest"
|
|
199
|
+
require_relative "../integration/minitest"
|
|
200
|
+
Evilution::Integration::Minitest.stub_autorun!
|
|
201
|
+
end
|
|
202
|
+
|
|
189
203
|
def warn_missing_explicit_preload(path)
|
|
190
204
|
return if config.quiet
|
|
191
205
|
|
data/lib/evilution/version.rb
CHANGED