evilution 0.1.0 → 0.3.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/.migration-hint-ts +1 -1
- data/.beads/issues.jsonl +29 -3
- data/.claude/prompts/architect.md +0 -1
- data/CHANGELOG.md +23 -0
- data/README.md +48 -16
- data/lib/evilution/cli.rb +98 -23
- data/lib/evilution/config.rb +38 -17
- data/lib/evilution/integration/rspec.rb +2 -69
- data/lib/evilution/isolation/fork.rb +31 -2
- data/lib/evilution/runner.rb +22 -41
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +0 -2
- metadata +7 -5
- data/lib/evilution/parallel/pool.rb +0 -98
- data/lib/evilution/parallel/worker.rb +0 -24
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 646e7a441c39108d413a0b33cb28234b090e16c1614d343c40be84581fb45adf
|
|
4
|
+
data.tar.gz: fc65d99be854a51dc0354a77db413d90a8344611dc8d94a5e97ef61e91e764a9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 38954ff26a4f48a050ca7a5d4270307157f7482836718152e03d17642df566110144c2773a5ce62ea8877ed2bdc97e6b2ccc33d49e9521e2074ad7cfc2b46b1e
|
|
7
|
+
data.tar.gz: c4a4bcbf1f59e2f8ff01138bf690132dcb183330009340b76b4d8db1d8807411c04dcb8f3a52820857b9563158a49147903a7274e1395b0f4f76047f239d625e
|
data/.beads/.migration-hint-ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
1773300459
|
data/.beads/issues.jsonl
CHANGED
|
@@ -5,10 +5,16 @@
|
|
|
5
5
|
{"id":"EV-1.4","title":"Delete services.md prompt","description":"Remove .claude/prompts/services.md — not applicable to a gem project.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:17.649482526+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:41:19.226636126+07:00","closed_at":"2026-03-02T10:41:19.226636126+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-1.4","depends_on_id":"EV-1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
|
|
6
6
|
{"id":"EV-1.5","title":"Update gemspec metadata","description":"Fill in summary, description, homepage, source_code_uri, changelog_uri. Add runtime dependency: diff-lcs (>= 1.5, < 3). Set allowed_push_host to rubygems.org.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:17.756448433+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:41:47.168270415+07:00","closed_at":"2026-03-02T10:41:47.168270415+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-1.5","depends_on_id":"EV-1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
|
|
7
7
|
{"id":"EV-1.6","title":"Update .rubocop.yml for gem conventions","description":"Configure rubocop for gem: enable NewCops, set reasonable line length, exclude spec fixtures.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:17.855784448+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:42:03.456572349+07:00","closed_at":"2026-03-02T10:42:03.456572349+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-1.6","depends_on_id":"EV-1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
|
|
8
|
-
{"id":"EV-10","title":"Publish v0.1.0 gem release","description":"Gemspec is ready. Tag v0.1.0, build the gem, and publish to RubyGems. Steps: verify gemspec metadata, run rake release (or manual gem build + gem push), create GitHub release with CHANGELOG notes.","status":"
|
|
8
|
+
{"id":"EV-10","title":"Publish v0.1.0 gem release","description":"Gemspec is ready. Tag v0.1.0, build the gem, and publish to RubyGems. Steps: verify gemspec metadata, run rake release (or manual gem build + gem push), create GitHub release with CHANGELOG notes.","status":"closed","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T16:21:53.571182801+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-06T11:44:35.748868691+07:00","closed_at":"2026-03-06T11:44:35.748868691+07:00","close_reason":"Published to RubyGems"}
|
|
9
9
|
{"id":"EV-11","title":"Wire coverage-based filtering into Runner","description":"Collector and TestMap exist and pass specs, but Runner never calls them. When config.coverage is true, Runner collects aggregate line-level coverage via Coverage::Collector, builds a Coverage::TestMap, and skips mutations on lines that no test exercises (marking them as survived with zero duration). Note: Ruby Coverage provides aggregate per-file line hit counts, not per-test-file tracking, so we filter out uncovered mutations rather than selecting specific test files.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-04T11:52:42.607616728+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-06T11:02:25.4848637+07:00","closed_at":"2026-03-05T14:53:24.894588088+07:00","close_reason":"Coverage-based test selection wired into Runner"}
|
|
10
10
|
{"id":"EV-12","title":"Resolve suggestion gap","description":"The workflow section instructs agents to read a suggestion field from survived[], but the JSON reporter output does not include suggestion (lib/evilution/reporter/json.rb only emits operator/file/line/status/duration/diff). Either add suggestion to the JSON output/schema or update the workflow steps to match the actual report fields. See GitHub issue #12.","status":"closed","priority":2,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-05T12:33:23.674094791+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-05T13:05:48.785616141+07:00","closed_at":"2026-03-05T13:05:48.785616141+07:00","close_reason":"Wired Suggestion into JSON reporter for survived mutations, updated README schema, added specs"}
|
|
11
11
|
{"id":"EV-13","title":"Enable true per-mutation isolation with temp file copies","description":"Replace direct file writes with temp-dir + $LOAD_PATH isolation so multiple workers can mutate the same file in parallel. Remove per-file grouping from Pool#partition.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-05T13:42:04.711580397+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-05T13:44:34.779951959+07:00","closed_at":"2026-03-05T13:44:34.779951959+07:00","close_reason":"Implemented temp file isolation via $LOAD_PATH and round-robin partition"}
|
|
12
|
+
{"id":"EV-14","title":"Add changelog_uri to gemspec metadata","description":"The published gem on RubyGems is missing the Changelog link. Add changelog_uri to spec.metadata in evilution.gemspec pointing to CHANGELOG.md on master.","status":"closed","priority":2,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-06T11:46:12.648668594+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-06T11:47:41.23691915+07:00","closed_at":"2026-03-06T11:47:41.23691915+07:00","close_reason":"Added changelog_uri to gemspec"}
|
|
13
|
+
{"id":"EV-15","title":"Add line-range targeting (file:line-line syntax)","description":"Parse line-range syntax in CLI, store in Config, filter subjects in Runner. GH #29.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-08T00:36:29.164188342+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-08T00:38:02.739833414+07:00","closed_at":"2026-03-08T00:38:02.739833414+07:00","close_reason":"Implemented line-range targeting in CLI, Config, and Runner with full test coverage"}
|
|
14
|
+
{"id":"EV-16","title":"Remove file-discovery logic from Integration::RSpec (GH #33)","description":"Integration::RSpec has detect_test_files, spec_file_candidates, and fallback_spec_dir that guess which specs to run. With precise targeting, agents can pass spec files directly. Simplify or remove this guessing logic, possibly adding a --spec flag.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-08T19:17:27.268579626+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-09T18:13:46.899195957+07:00","closed_at":"2026-03-09T18:13:46.899195957+07:00","close_reason":"Completed via PR #38 (tracked as EV-17)"}
|
|
15
|
+
{"id":"EV-17","title":"Remove file-discovery logic from Integration::RSpec","description":"Add --spec flag, simplify RSpec integration by removing detect_test_files heuristics. GH #33","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-09T00:41:29.880486333+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-09T00:43:21.229172712+07:00","closed_at":"2026-03-09T00:43:21.229172712+07:00","close_reason":"Removed ~60 lines of file-discovery logic from Integration::RSpec, added --spec CLI flag, spec_files config, and wired through Runner"}
|
|
16
|
+
{"id":"EV-18","title":"Add method-name targeting (Class#method) (GH #30)","description":"Allow specifying a fully-qualified method name: evilution run Foo::Bar#calculate. Resolve name to file and line range, then mutate only that method. Lower priority since it requires name-to-file resolution which can be ambiguous with reopened classes or metaprogramming. Update README, --help, and CHANGELOG.","status":"in_progress","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-09T23:49:31.827723147+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T00:05:22.381962628+07:00"}
|
|
17
|
+
{"id":"EV-19","title":"Add method-name targeting (--target flag)","description":"Users can target a specific method for mutation testing via --target METHOD flag. Matches against Subject.name.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T00:09:57.091870734+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T00:10:02.637447023+07:00","closed_at":"2026-03-10T00:10:02.637447023+07:00","close_reason":"Implemented --target flag in CLI, Config.target? predicate, Runner.filter_by_target, README docs, and specs"}
|
|
12
18
|
{"id":"EV-2","title":"Phase 1: Foundation — End-to-End Single Mutation","description":"Build the core pipeline: parse Ruby with Prism, generate mutations, fork-based isolation, RSpec integration, JSON reporting. Milestone: Runner.new(files: ['lib/user.rb']).call produces JSON output.","status":"closed","priority":2,"issue_type":"epic","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:04:58.737191467+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:02:00.342745637+07:00","closed_at":"2026-03-02T11:02:00.342745637+07:00","close_reason":"Phase 1 Foundation complete: Config, AST::Parser, Subject, SourceSurgeon, Mutation, Mutator::Base+Registry, ComparisonReplacement, Isolation::Fork, Integration::RSpec, Result objects, Reporter::JSON, Runner — all 13 tasks done"}
|
|
13
19
|
{"id":"EV-2.1","title":"Implement Evilution::Config","description":"Immutable configuration value object. Fields: target_files, jobs (default: Etc.nprocessors), timeout (default: 10s), format (:json/:text), diff_base (nil), min_score (0.0), integration (:rspec), config_file path. Merge from defaults + YAML + CLI flags. File: lib/evilution/config.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:50.275297792+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:43:14.688620834+07:00","closed_at":"2026-03-02T10:43:14.688620834+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-2.1","depends_on_id":"EV-2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
|
|
14
20
|
{"id":"EV-2.10","title":"Implement Evilution::Integration::Base and RSpec adapter","description":"Base: abstract adapter with interface #call(test_files) -> {passed: bool, example_count: int, failure_count: int}. RSpec: programmatic runner using RSpec::Core::Runner.run with StringIO for capture. Auto-detects spec/ directory. Files: lib/evilution/integration/base.rb, lib/evilution/integration/rspec.rb + specs.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:51.246990324+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:58:18.324768622+07:00","closed_at":"2026-03-02T10:58:18.324768622+07:00","close_reason":"Integration::Base and Integration::RSpec implemented with 8 passing specs","dependencies":[{"issue_id":"EV-2.10","depends_on_id":"EV-2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
|
|
@@ -23,6 +29,16 @@
|
|
|
23
29
|
{"id":"EV-2.7","title":"Implement ComparisonReplacement operator","description":"First mutation operator. Targets CallNode where name is :>, :<, :>=, :<=, :==, :!=. Replacements: > -> [>=, ==], < -> [<=, ==], >= -> [>, ==], <= -> [<, ==], == -> [!=], != -> [==]. Uses SourceSurgeon to apply text replacement at message_loc offsets. File: lib/evilution/mutator/operator/comparison_replacement.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:50.917501278+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:50:01.327985674+07:00","closed_at":"2026-03-02T10:50:01.327985674+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-2.7","depends_on_id":"EV-2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-2.7","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-2.7","depends_on_id":"EV-2.4","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
24
30
|
{"id":"EV-2.8","title":"Implement Evilution::Result::MutationResult and Summary","description":"MutationResult: value object with fields: mutation, status (:killed/:survived/:timeout/:error), duration, killing_test (optional). Summary: aggregates MutationResult array into total/killed/survived/timed_out/errors/score/duration. Files: lib/evilution/result/mutation_result.rb, lib/evilution/result/summary.rb + specs.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:51.022249568+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:51:08.482621898+07:00","closed_at":"2026-03-02T10:51:08.482621898+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-2.8","depends_on_id":"EV-2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
|
|
25
31
|
{"id":"EV-2.9","title":"Implement Evilution::Isolation::Fork","description":"Fork-based process isolation. Method: run(mutation, test_command, timeout:) -> MutationResult. Flow: create pipe, fork child, child applies mutation via eval, child runs test command, marshal result back via pipe, parent reads with IO.select timeout, SIGKILL on deadline. File: lib/evilution/isolation/fork.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:51.142167275+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:54:08.819310594+07:00","closed_at":"2026-03-02T10:54:08.819310594+07:00","close_reason":"Fork isolation implemented with 6 passing specs","dependencies":[{"issue_id":"EV-2.9","depends_on_id":"EV-2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-2.9","depends_on_id":"EV-2.8","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
32
|
+
{"id":"EV-20","title":"Deprecate coverage filtering","description":"With line-range and method-name targeting, coverage collection adds overhead for little benefit. Deprecate --no-coverage flag, coverage config key, and remove coverage logic from runner.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T00:30:58.837659684+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T00:33:06.453669976+07:00","closed_at":"2026-03-10T00:33:06.453669976+07:00","close_reason":"Deprecated coverage filtering: CLI warns on --no-coverage, config warns on coverage key, runner no longer uses coverage logic"}
|
|
33
|
+
{"id":"EV-21","title":"Epic: Speed & Performance","description":"Reduce mutation testing wall-clock time for fast agent feedback loops. Includes fail-fast, parallel execution, and per-mutation spec targeting.","status":"open","priority":1,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:17:26.608316104+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T06:17:26.608316104+07:00","dependencies":[{"issue_id":"EV-21","depends_on_id":"EV-22","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-21","depends_on_id":"EV-23","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
34
|
+
{"id":"EV-22","title":"Add --fail-fast flag with survivor threshold","description":"Add --fail-fast [N] CLI flag and fail_fast config option to stop mutation testing after N surviving mutants (default N=1 when flag given without value). Agents fix gaps iteratively, so discovering all survivors upfront is wasted work. A threshold gives control over the speed/thoroughness tradeoff: --fail-fast 1 for quick checks, --fail-fast 5 for CI gates, omit for full scans. Runner#call should stop running mutations and return a partial Summary once the threshold is reached. JSON output should include a 'truncated: true' field when fail-fast triggered.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:17:28.018733235+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T10:29:42.228568154+07:00"}
|
|
35
|
+
{"id":"EV-23","title":"Per-mutation spec targeting","description":"Instead of running the full spec suite for every mutation, map each mutated source file to its relevant spec file(s) using convention-based resolution (e.g. lib/foo/bar.rb -> spec/foo/bar_spec.rb) and only run those. This dramatically reduces per-mutation test time. Depends on convention-based spec file resolution being implemented first.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:17:28.98620973+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T06:17:28.98620973+07:00","dependencies":[{"issue_id":"EV-23","depends_on_id":"EV-34","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
36
|
+
{"id":"EV-24","title":"Epic: JSON Output Improvements","description":"Make JSON output fully machine-parseable in all scenarios, including errors. Add diagnostic fields that help agents debug failures.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:17:37.450686472+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T06:17:37.450686472+07:00","dependencies":[{"issue_id":"EV-24","depends_on_id":"EV-25","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-24","depends_on_id":"EV-26","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
37
|
+
{"id":"EV-25","title":"Structured error responses in JSON mode","description":"When --format json is used and exit code is 2 (error), output a JSON object with error details instead of unstructured stderr text. Schema: { \"error\": { \"type\": \"config_error|parse_error|runtime_error\", \"message\": \"...\", \"file\": \"...\" } }. Agents currently have to regex-parse stderr which is fragile.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:17:38.283715502+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T06:17:38.283715502+07:00"}
|
|
38
|
+
{"id":"EV-26","title":"Include test command in mutation result JSON","description":"Add a 'test_command' field to each mutation result in JSON output showing the exact command that was run to test that mutation. Helps agents debug when a mutation errors out or times out.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:17:39.227881462+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T06:17:39.227881462+07:00"}
|
|
39
|
+
{"id":"EV-27","title":"Epic: Workflow Integration","description":"Make evilution easier to invoke from AI agent toolchains — MCP server for direct tool calls, stdin mode for piping.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:17:43.883767452+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T06:17:43.883767452+07:00","dependencies":[{"issue_id":"EV-27","depends_on_id":"EV-28","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-27","depends_on_id":"EV-29","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
40
|
+
{"id":"EV-28","title":"MCP server for direct tool invocation","description":"Implement a Model Context Protocol (MCP) server that exposes evilution as a tool. Agents could call evilution directly instead of shelling out and parsing output. The server should expose a 'mutate' tool that accepts target files, options, and returns structured results.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:17:45.29866593+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T06:17:45.29866593+07:00"}
|
|
41
|
+
{"id":"EV-29","title":"Add --stdin flag to accept file list from stdin","description":"Add a --stdin flag that reads target file paths (one per line) from stdin. Enables workflows like: git diff --name-only | evilution run --stdin --format json. Each line can include line-range syntax (e.g. lib/foo.rb:15-30).","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:17:46.306306092+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T06:17:46.306306092+07:00"}
|
|
26
42
|
{"id":"EV-3","title":"Phase 2: Mutation Operators & CLI","description":"Implement remaining 17 mutation operators, build CLI with OptionParser, exe/evilution executable, human-readable reporter. Milestone: bundle exec evilution run lib/user.rb --format json","status":"closed","priority":2,"issue_type":"epic","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:00.492971295+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:21:32.168384165+07:00","closed_at":"2026-03-02T11:21:32.168384165+07:00","close_reason":"Phase 2 complete: all 18 operators, CLI, Reporter::CLI, Registry registration, executable","dependencies":[{"issue_id":"EV-3","depends_on_id":"EV-2","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
27
43
|
{"id":"EV-3.1","title":"Implement ArithmeticReplacement operator","description":"Targets CallNode where name is :+, :-, :*, :/, :%, :**. Replacements: + <-> -, * <-> /, % -> /, ** -> *. File: lib/evilution/mutator/operator/arithmetic_replacement.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:13.025649082+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:06:10.922412304+07:00","closed_at":"2026-03-02T11:06:10.922412304+07:00","close_reason":"All 3 operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.1","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.1","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
28
44
|
{"id":"EV-3.10","title":"Implement SymbolLiteral operator","description":"Targets SymbolNode. Mutation: :foo -> :__evilution_mutated__. File: lib/evilution/mutator/operator/symbol_literal.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:13.935877816+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:13:36.541040613+07:00","closed_at":"2026-03-02T11:13:36.541040613+07:00","close_reason":"All 3 operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.10","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.10","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
@@ -30,7 +46,7 @@
|
|
|
30
46
|
{"id":"EV-3.12","title":"Implement ConditionalBranch operator","description":"Targets IfNode with both if and else branches. Mutations: replace entire if/else with if-branch only, replace with else-branch only. File: lib/evilution/mutator/operator/conditional_branch.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:14.138508575+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:13:36.541106251+07:00","closed_at":"2026-03-02T11:13:36.541106251+07:00","close_reason":"All 3 operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.12","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.12","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
31
47
|
{"id":"EV-3.13","title":"Implement StatementDeletion operator","description":"Targets StatementsNode bodies with 2+ statements. For each statement, produce a mutation that removes it. File: lib/evilution/mutator/operator/statement_deletion.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:14.240151154+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:16:08.954716442+07:00","closed_at":"2026-03-02T11:16:08.954716442+07:00","close_reason":"All 3 operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.13","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.13","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
32
48
|
{"id":"EV-3.14","title":"Implement MethodBodyReplacement operator","description":"Targets DefNode with body. Mutations: replace body with nil, replace with raise NotImplementedError. File: lib/evilution/mutator/operator/method_body_replacement.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:14.333934644+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:16:08.954887847+07:00","closed_at":"2026-03-02T11:16:08.954887847+07:00","close_reason":"All 3 operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.14","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.14","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
33
|
-
{"id":"EV-3.15","title":"Implement NegationInsertion operator","description":"Targets CallNode with predicate method names (ending in ?). Mutation: x.empty? -> !x.empty?. File: lib/evilution/mutator/operator/negation_insertion.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:14.438532333+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-
|
|
49
|
+
{"id":"EV-3.15","title":"Implement NegationInsertion operator","description":"Targets CallNode with predicate method names (ending in ?). Mutation: x.empty? -> !x.empty?. File: lib/evilution/mutator/operator/negation_insertion.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:14.438532333+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-06T16:11:55.50396051+07:00","closed_at":"2026-03-02T11:16:08.95490096+07:00","close_reason":"Commands now listed in --help output","dependencies":[{"issue_id":"EV-3.15","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.15","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
34
50
|
{"id":"EV-3.16","title":"Implement ReturnValueRemoval operator","description":"Targets ReturnNode with arguments. Mutations: return x -> return, return x -> return nil. File: lib/evilution/mutator/operator/return_value_removal.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:14.541333631+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:18:14.396683739+07:00","closed_at":"2026-03-02T11:18:14.396683739+07:00","close_reason":"Both operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.16","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.16","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
35
51
|
{"id":"EV-3.17","title":"Implement CollectionReplacement operator","description":"Targets CallNode where name matches collection methods. Replacements: map->each, select<->reject, all?<->any?, first<->last, min<->max, flat_map->map. File: lib/evilution/mutator/operator/collection_replacement.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:14.635444378+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:18:14.396880048+07:00","closed_at":"2026-03-02T11:18:14.396880048+07:00","close_reason":"Both operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.17","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.17","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
36
52
|
{"id":"EV-3.18","title":"Implement Evilution::CLI","description":"OptionParser-based CLI. Commands: run [FILES...], version, init. Flags: --jobs/-j, --timeout/-t, --format/-f (json/text), --diff BASE, --target PATTERN, --min-score FLOAT, --no-coverage, --config FILE, --verbose/-v, --quiet/-q. Parses args into Config, delegates to Runner. File: lib/evilution/cli.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:26.540459363+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:20:50.084035782+07:00","closed_at":"2026-03-02T11:20:50.084035782+07:00","close_reason":"CLI, Reporter::CLI, and Registry registration all implemented","dependencies":[{"issue_id":"EV-3.18","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.18","depends_on_id":"EV-2.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.18","depends_on_id":"EV-2.12","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
@@ -45,6 +61,16 @@
|
|
|
45
61
|
{"id":"EV-3.7","title":"Implement StringLiteral operator","description":"Targets StringNode. Mutations: non-empty -> empty string, empty -> 'mutation'. File: lib/evilution/mutator/operator/string_literal.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:13.633726423+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:10:56.39748015+07:00","closed_at":"2026-03-02T11:10:56.39748015+07:00","close_reason":"All 3 operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.7","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.7","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
46
62
|
{"id":"EV-3.8","title":"Implement ArrayLiteral operator","description":"Targets ArrayNode with elements. Mutation: [a, b, c] -> []. File: lib/evilution/mutator/operator/array_literal.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:13.734617709+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:10:56.397565925+07:00","closed_at":"2026-03-02T11:10:56.397565925+07:00","close_reason":"All 3 operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.8","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.8","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
47
63
|
{"id":"EV-3.9","title":"Implement HashLiteral operator","description":"Targets HashNode with pairs. Mutation: {k: v} -> {}. File: lib/evilution/mutator/operator/hash_literal.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:13.840779748+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:10:56.397570989+07:00","closed_at":"2026-03-02T11:10:56.397570989+07:00","close_reason":"All 3 operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.9","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.9","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
64
|
+
{"id":"EV-30","title":"Epic: Smarter Suggestions","description":"Improve the suggestion field in mutation results to provide concrete, actionable test code that agents can use directly.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:18:03.590579846+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T06:18:03.590579846+07:00","dependencies":[{"issue_id":"EV-30","depends_on_id":"EV-31","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
65
|
+
{"id":"EV-31","title":"Generate concrete RSpec test code in suggestions","description":"Replace prose suggestions like 'add a test that checks the boundary' with actual RSpec code snippets that would kill the mutant. Use the mutation's operator, file context, and method signature to generate a concrete test example. The suggestion should be a valid RSpec 'it' block that an agent can drop into a spec file.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:18:05.964542284+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T06:18:05.964542284+07:00"}
|
|
66
|
+
{"id":"EV-32","title":"Epic: Zero-friction Defaults","description":"Make evilution work well with zero configuration. Auto-detect what to mutate and which specs to run based on git state and file conventions.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:18:06.843446356+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T06:18:06.843446356+07:00","dependencies":[{"issue_id":"EV-32","depends_on_id":"EV-33","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-32","depends_on_id":"EV-34","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
67
|
+
{"id":"EV-33","title":"Auto-detect changed files from git merge base","description":"When evilution is run with no file arguments, default to mutating files changed since the merge base of the current branch (git merge-base HEAD main). This makes 'evilution run --format json' just work in a feature branch without specifying files. Should be skippable if explicit files are provided.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:18:08.235421922+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T06:18:08.235421922+07:00"}
|
|
68
|
+
{"id":"EV-34","title":"Convention-based spec file resolution","description":"Implement a mapping from source files to their spec files using Ruby/RSpec conventions: lib/foo/bar.rb -> spec/foo/bar_spec.rb, app/models/user.rb -> spec/models/user_spec.rb. This is a foundational piece used by both zero-friction defaults (auto-detect specs) and per-mutation spec targeting (run only relevant specs). Should handle common Rails and gem layouts.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:18:09.380269033+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T06:18:09.380269033+07:00"}
|
|
69
|
+
{"id":"EV-35","title":"File left in mutated state on timeout","description":"Epic: File left in mutated state on timeout. Root cause: SIGKILL (fork.rb:45) is untrappable — child's ensure block (rspec.rb:24-26) never runs, so restore_original never executes. Affects Strategy B (direct file write) with data corruption and Strategy A (temp dir) with resource leaks. Decomposed into EV-37, EV-38, EV-39.","status":"closed","priority":1,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-12T14:27:59.54425065+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-13T00:34:44.130203912+07:00","closed_at":"2026-03-13T00:34:44.130203912+07:00","close_reason":"All sub-issues (EV-37, EV-38, EV-39) completed","dependencies":[{"issue_id":"EV-35","depends_on_id":"EV-37","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-35","depends_on_id":"EV-38","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-35","depends_on_id":"EV-39","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
70
|
+
{"id":"EV-36","title":"Raise default timeout from 10s to 30s","description":"Default 10s timeout is too low when running the full test suite per mutation (no smart test selection yet). Agent testing showed 30s was needed. Raising the default prevents confusing timeout failures for new users.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-12T14:27:59.761882257+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-12T14:45:27.102490973+07:00","closed_at":"2026-03-12T14:45:27.102490973+07:00","close_reason":"Closed"}
|
|
71
|
+
{"id":"EV-37","title":"Parent-side backup & restore for direct-write mutations","description":"Root cause: SIGKILL is untrappable, so when a mutation times out the child's ensure block (rspec.rb:24-26) never runs. If Strategy B (direct file write, rspec.rb:50-58) was used, the original file is left in a mutated state on disk. Fix: before forking, the parent must save a backup of the original file content. After the child exits (normally or via kill), the parent verifies the file is restored and restores it if not. This is the primary fix for EV-35.","status":"closed","priority":0,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-12T14:32:49.590919391+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-12T23:04:21.990924732+07:00","closed_at":"2026-03-12T23:04:21.990924732+07:00","close_reason":"Implemented parent-side restore in Fork#call ensure block with read-before-write guard"}
|
|
72
|
+
{"id":"EV-38","title":"Use SIGTERM with grace period before SIGKILL on timeout","description":"Defense-in-depth for EV-35. Currently fork.rb:45 sends SIGKILL immediately on timeout. SIGKILL is untrappable so the child gets no chance to run ensure blocks. Fix: send SIGTERM first (trappable), wait a short grace period (1-2s), then SIGKILL if still alive. This lets the child's ensure block restore files naturally in most timeout cases.","status":"closed","priority":0,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-12T14:32:52.463953548+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-13T00:34:44.01894518+07:00","closed_at":"2026-03-13T00:34:44.01894518+07:00","close_reason":"Implemented SIGTERM with grace period before SIGKILL fallback"}
|
|
73
|
+
{"id":"EV-39","title":"Clean up leaked temp directories after child timeout (Strategy A)","description":"When Strategy A (temp dir, rspec.rb:44-49) is used and the child is killed on timeout, the temp directory is never cleaned up because the child's ensure block doesn't run. The parent should track and clean up any temp dirs created for mutation runs. Minor resource leak, not data corruption.","status":"closed","priority":0,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-12T14:32:54.466514555+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-13T00:34:31.932452641+07:00","closed_at":"2026-03-13T00:34:31.932452641+07:00","close_reason":"Implemented per-run sandbox TMPDIR for child processes, cleaned up by parent in ensure block"}
|
|
48
74
|
{"id":"EV-4","title":"Phase 3: Performance Features","description":"Parallel execution pool, coverage-based mutation filtering, git diff-based targeting. Milestone: evilution run --diff HEAD~1 --jobs 4","status":"closed","priority":2,"issue_type":"epic","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:02.252877807+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-06T11:02:29.997677574+07:00","closed_at":"2026-03-02T11:54:32.782829837+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-4","depends_on_id":"EV-3","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
49
75
|
{"id":"EV-4.1","title":"Implement Evilution::Parallel::Pool","description":"Thread-based worker pool distributing fork workers. Initialize with job_count. Method: #run(mutations, test_command, timeout:) -> [MutationResult]. Uses Queue to distribute work to N threads, each thread calls Isolation::Fork. File: lib/evilution/parallel/pool.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:44.984077473+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:46:46.640997276+07:00","closed_at":"2026-03-02T11:46:46.640997276+07:00","close_reason":"Pool implementation and tests already exist and pass","dependencies":[{"issue_id":"EV-4.1","depends_on_id":"EV-4","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-4.1","depends_on_id":"EV-2.9","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
50
76
|
{"id":"EV-4.2","title":"Implement Evilution::Parallel::Worker","description":"Individual worker abstraction managing fork lifecycle. Wraps Isolation::Fork with queue consumption loop. File: lib/evilution/parallel/worker.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:45.08570827+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:54:21.51846223+07:00","closed_at":"2026-03-02T11:54:21.51846223+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-4.2","depends_on_id":"EV-4","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-4.2","depends_on_id":"EV-4.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
@@ -63,6 +89,6 @@
|
|
|
63
89
|
{"id":"EV-5.5","title":"Write README.md","description":"Replace placeholder README with: gem description, installation, quick start, CLI usage, configuration, output formats (JSON schema), operator list, comparison with mutant, contributing guide, license.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:58.454635118+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:54:29.226280116+07:00","closed_at":"2026-03-02T11:54:29.226280116+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-5.5","depends_on_id":"EV-5","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-5.5","depends_on_id":"EV-4.9","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-5.5","depends_on_id":"EV-5.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
64
90
|
{"id":"EV-5.6","title":"Update CHANGELOG.md for v0.1.0","description":"Document all features in the initial release: operator list, RSpec integration, JSON/CLI output, parallel execution, coverage-based selection, diff-based targeting.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:58.571499415+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:54:29.22634981+07:00","closed_at":"2026-03-02T11:54:29.22634981+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-5.6","depends_on_id":"EV-5","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-5.6","depends_on_id":"EV-5.5","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
65
91
|
{"id":"EV-6","title":"Fix pool fork tests hanging in CI and WSL2","description":"The spec/evilution/parallel/pool_spec.rb fork-based tests hang on both WSL2 and GitHub Actions. CI currently excludes them via --exclude-pattern. Root cause is likely double-fork (Pool forks workers, each Worker uses Isolation::Fork which forks again) causing pipe/process management issues. Needs investigation and fix so pool tests run reliably in CI.","status":"closed","priority":1,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T16:21:43.62587758+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-05T12:31:51.831163433+07:00","closed_at":"2026-03-05T12:31:51.831163433+07:00","close_reason":"Closed"}
|
|
66
|
-
{"id":"EV-7","title":"Enable true per-mutation isolation with temp file copies","description":"Currently all mutations for the same file go to one worker (PR #4 fix). This means single-file projects get no parallelism benefit. Implement temp file copy approach: each worker copies the source file to a temp location, mutates the copy, and runs tests against it. This enables safe parallel mutation of the same file across multiple workers.","status":"
|
|
92
|
+
{"id":"EV-7","title":"Enable true per-mutation isolation with temp file copies","description":"Currently all mutations for the same file go to one worker (PR #4 fix). This means single-file projects get no parallelism benefit. Implement temp file copy approach: each worker copies the source file to a temp location, mutates the copy, and runs tests against it. This enables safe parallel mutation of the same file across multiple workers.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T16:21:46.820210376+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-06T11:46:55.740292929+07:00","closed_at":"2026-03-06T11:46:55.740292929+07:00","close_reason":"Already merged in PR #20"}
|
|
67
93
|
{"id":"EV-8","title":"Investigate RSpec state accumulation across mutation runs","description":"RSpec.reset is called between mutations but loaded spec files persist in memory. Long mutation runs may accumulate state from previously loaded specs, potentially causing false positives/negatives. Investigate whether RSpec::Core::Runner.run properly isolates between runs or if additional cleanup is needed.","status":"closed","priority":2,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T16:21:48.618669476+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-05T11:57:56.965910759+07:00","closed_at":"2026-03-05T11:57:56.965910759+07:00","close_reason":"Investigation complete: fork isolation already prevents state accumulation. Added documentation explaining the guarantee and tests verifying independent consecutive runs."}
|
|
68
94
|
{"id":"EV-9","title":"Add multi-Ruby CI test matrix (3.2, 3.3, 4.0)","description":"CI currently only tests Ruby 4.0.1. Add a matrix strategy testing Ruby 3.2, 3.3, and 4.0 to ensure compatibility across supported Ruby versions. Prism ships with Ruby 3.3+ so 3.2 may need the prism gem as a dependency.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T16:21:51.239774764+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-05T12:31:51.83181612+07:00","closed_at":"2026-03-05T12:31:51.83181612+07:00","close_reason":"Closed"}
|
|
@@ -73,7 +73,6 @@ Evilution
|
|
|
73
73
|
::Coverage::TestMap # Source line → test file mapping
|
|
74
74
|
::Diff::Parser # Git diff output parser
|
|
75
75
|
::Diff::FileFilter # Filter subjects to changed code
|
|
76
|
-
::Parallel::Pool # Thread-based worker pool
|
|
77
76
|
::Result::MutationResult # Single mutation outcome
|
|
78
77
|
::Result::Summary # Aggregated results
|
|
79
78
|
::Reporter::JSON # Structured output for AI agents
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2026-03-10
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Line-range targeting** — scope mutations to exact lines: `lib/foo.rb:15-30`, `lib/foo.rb:15`, `lib/foo.rb:15-`
|
|
8
|
+
- **Method-name targeting** (`--target`) — mutate a single method by fully-qualified name (e.g. `Foo::Bar#calculate`)
|
|
9
|
+
- **Commands section** in `--help` output
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Added `changelog_uri` to gemspec metadata
|
|
14
|
+
- Added GitHub publish workflow
|
|
15
|
+
|
|
16
|
+
### Deprecated
|
|
17
|
+
|
|
18
|
+
- **`--diff` flag** — use line-range targeting instead
|
|
19
|
+
- **Coverage-based filtering flags/config** (`--no-coverage` flag and `coverage` config key) — deprecated and now ignored; coverage-based filtering behavior has been removed from `Runner`
|
|
20
|
+
|
|
21
|
+
### Removed
|
|
22
|
+
|
|
23
|
+
- **Parallel execution** (`--jobs` flag) — simplifies codebase for AI-agent-first design; will be reintroduced later
|
|
24
|
+
- **File-discovery logic** from `Integration::RSpec` — spec files are now passed explicitly or default to `spec/`
|
|
25
|
+
|
|
3
26
|
## [0.1.0] - 2026-03-02
|
|
4
27
|
|
|
5
28
|
### Added
|
data/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
[](https://badge.fury.io/rb/evilution)
|
|
2
|
+
|
|
1
3
|
# Evilution — Mutation Testing for Ruby
|
|
2
4
|
|
|
3
5
|
> **Purpose**: Validate test suite quality by injecting small code changes (mutations) and checking whether tests detect them. Surviving mutations indicate gaps in test coverage.
|
|
4
6
|
|
|
5
|
-
**License**: MIT (free, no commercial restrictions)
|
|
6
|
-
**Language**: Ruby >= 3.3
|
|
7
|
-
**Parser**: Prism (Ruby's official AST parser, ships with Ruby 3.3+)
|
|
8
|
-
**Test framework**: RSpec (currently the only supported integration)
|
|
7
|
+
* **License**: MIT (free, no commercial restrictions)
|
|
8
|
+
* **Language**: Ruby >= 3.3
|
|
9
|
+
* **Parser**: Prism (Ruby's official AST parser, ships with Ruby 3.3+)
|
|
10
|
+
* **Test framework**: RSpec (currently the only supported integration)
|
|
9
11
|
|
|
10
12
|
## Installation
|
|
11
13
|
|
|
@@ -37,12 +39,13 @@ evilution [command] [options] [files...]
|
|
|
37
39
|
|
|
38
40
|
| Flag | Type | Default | Description |
|
|
39
41
|
|-------------------------|---------|--------------|---------------------------------------------------|
|
|
40
|
-
| `-j`, `--jobs N` | Integer | CPU cores | Parallel worker count. Use `1` for sequential. |
|
|
41
42
|
| `-t`, `--timeout N` | Integer | 10 | Per-mutation timeout in seconds. |
|
|
42
43
|
| `-f`, `--format FORMAT` | String | `text` | Output format: `text` or `json`. |
|
|
43
|
-
| `--
|
|
44
|
+
| `--target METHOD` | String | _(none)_ | Only mutate the named method (e.g. `Foo::Bar#calculate`). |
|
|
45
|
+
| `--diff BASE` | String | _(none)_ | **DEPRECATED**: Use line-range targeting instead. Git ref. Only mutate methods whose definition line changed since BASE. |
|
|
44
46
|
| `--min-score FLOAT` | Float | 0.0 | Minimum mutation score (0.0–1.0) to pass. |
|
|
45
|
-
| `--
|
|
47
|
+
| `--spec FILES` | Array | _(none)_ | Spec files to run (comma-separated). Defaults to `spec/`. |
|
|
48
|
+
| `--no-coverage` | Boolean | false | **DEPRECATED, NO-OP**: Kept for backward compatibility. Will be removed. |
|
|
46
49
|
| `-v`, `--verbose` | Boolean | false | Verbose output. |
|
|
47
50
|
| `-q`, `--quiet` | Boolean | false | Suppress output. |
|
|
48
51
|
|
|
@@ -61,12 +64,10 @@ Generate default config: `bundle exec evilution init`
|
|
|
61
64
|
Creates `.evilution.yml`:
|
|
62
65
|
|
|
63
66
|
```yaml
|
|
64
|
-
# jobs: 4 # parallel workers
|
|
65
67
|
# timeout: 10 # seconds per mutation
|
|
66
68
|
# format: text # text | json
|
|
67
69
|
# min_score: 0.0 # 0.0–1.0
|
|
68
70
|
# integration: rspec # test framework
|
|
69
|
-
# coverage: true # skip mutations on uncovered lines
|
|
70
71
|
```
|
|
71
72
|
|
|
72
73
|
**Precedence**: CLI flags override `.evilution.yml` values.
|
|
@@ -137,20 +138,51 @@ Each operator name is stable and appears in JSON output under `survived[].operat
|
|
|
137
138
|
### 1. Full project scan
|
|
138
139
|
|
|
139
140
|
```bash
|
|
140
|
-
bundle exec evilution run lib/ --format json --
|
|
141
|
+
bundle exec evilution run lib/ --format json --min-score 0.8
|
|
141
142
|
```
|
|
142
143
|
|
|
143
144
|
Parse JSON output. Exit code 0 = pass, 1 = surviving mutants to address.
|
|
144
145
|
|
|
145
|
-
### 2. PR /
|
|
146
|
+
### 2. PR / changed-lines scan (fast feedback)
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
bundle exec evilution run lib/foo.rb:15-30 lib/bar.rb:5-20 --format json --min-score 0.9
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Target the exact lines you changed for fast, focused mutation testing. See line-range syntax below.
|
|
153
|
+
|
|
154
|
+
> **Note**: `--diff BASE` is deprecated and will be removed in a future version. Prefer line-range targeting for new workflows.
|
|
155
|
+
|
|
156
|
+
### 3. Line-range targeted scan (fastest)
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
bundle exec evilution run lib/foo.rb:15-30 --format json
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Target exact lines you changed. Supports multiple syntaxes:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
evilution run lib/foo.rb:15-30 # lines 15 through 30
|
|
166
|
+
evilution run lib/foo.rb:15 # single line 15
|
|
167
|
+
evilution run lib/foo.rb:15- # from line 15 to end of file
|
|
168
|
+
evilution run lib/foo.rb # whole file (existing behavior)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Methods whose body overlaps the requested range are included. Mix targeted and whole-file arguments freely:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
evilution run lib/foo.rb:15-30 lib/bar.rb --format json
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 4. Method-name targeted scan
|
|
146
178
|
|
|
147
179
|
```bash
|
|
148
|
-
bundle exec evilution run lib/ --
|
|
180
|
+
bundle exec evilution run lib/foo.rb --target Foo::Bar#calculate --format json
|
|
149
181
|
```
|
|
150
182
|
|
|
151
|
-
|
|
183
|
+
Target a specific method by its fully-qualified name. Useful when you want to focus on a single method without knowing its exact line numbers.
|
|
152
184
|
|
|
153
|
-
###
|
|
185
|
+
### 5. Single-file targeted scan
|
|
154
186
|
|
|
155
187
|
```bash
|
|
156
188
|
bundle exec evilution run lib/specific_file.rb --format json
|
|
@@ -158,7 +190,7 @@ bundle exec evilution run lib/specific_file.rb --format json
|
|
|
158
190
|
|
|
159
191
|
Use when you know which file was modified and want to verify its test coverage.
|
|
160
192
|
|
|
161
|
-
###
|
|
193
|
+
### 6. Fixing surviving mutants
|
|
162
194
|
|
|
163
195
|
For each entry in `survived[]`:
|
|
164
196
|
1. Read `file` at `line` to understand the code context
|
|
@@ -167,7 +199,7 @@ For each entry in `survived[]`:
|
|
|
167
199
|
4. Write a test that would fail if the mutation were applied
|
|
168
200
|
5. Re-run evilution on just that file to verify the mutant is now killed
|
|
169
201
|
|
|
170
|
-
###
|
|
202
|
+
### 7. CI gate
|
|
171
203
|
|
|
172
204
|
```bash
|
|
173
205
|
bundle exec evilution run lib/ --format json --min-score 0.8 --quiet
|
data/lib/evilution/cli.rb
CHANGED
|
@@ -11,7 +11,27 @@ module Evilution
|
|
|
11
11
|
@options = {}
|
|
12
12
|
@command = :run
|
|
13
13
|
argv = argv.dup
|
|
14
|
+
argv = extract_command(argv)
|
|
15
|
+
argv = warn_removed_flags(argv)
|
|
16
|
+
raw_args = build_option_parser.parse!(argv)
|
|
17
|
+
@files, @line_ranges = parse_file_args(raw_args)
|
|
18
|
+
end
|
|
14
19
|
|
|
20
|
+
def call
|
|
21
|
+
case @command
|
|
22
|
+
when :version
|
|
23
|
+
$stdout.puts(VERSION)
|
|
24
|
+
0
|
|
25
|
+
when :init
|
|
26
|
+
run_init
|
|
27
|
+
when :run
|
|
28
|
+
run_mutations
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def extract_command(argv)
|
|
15
35
|
case argv.first
|
|
16
36
|
when "version"
|
|
17
37
|
@command = :version
|
|
@@ -22,37 +42,65 @@ module Evilution
|
|
|
22
42
|
when "run"
|
|
23
43
|
argv.shift
|
|
24
44
|
end
|
|
45
|
+
argv
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def warn_removed_flags(argv)
|
|
49
|
+
result = []
|
|
50
|
+
i = 0
|
|
51
|
+
while i < argv.length
|
|
52
|
+
arg = argv[i]
|
|
53
|
+
if %w[--jobs -j].include?(arg)
|
|
54
|
+
warn("Warning: --jobs is no longer supported and will be ignored.")
|
|
55
|
+
next_arg = argv[i + 1]
|
|
56
|
+
i += next_arg&.match?(/\A-?\d+\z/) ? 2 : 1
|
|
57
|
+
elsif arg.start_with?("--jobs=") || arg.match?(/\A-j-?\d+\z/)
|
|
58
|
+
warn("Warning: --jobs is no longer supported and will be ignored.")
|
|
59
|
+
i += 1
|
|
60
|
+
else
|
|
61
|
+
result << arg
|
|
62
|
+
i += 1
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
result
|
|
66
|
+
end
|
|
25
67
|
|
|
26
|
-
|
|
68
|
+
def build_option_parser
|
|
69
|
+
OptionParser.new do |opts|
|
|
27
70
|
opts.banner = "Usage: evilution [command] [options] [files...]"
|
|
28
|
-
|
|
29
|
-
opts
|
|
30
|
-
opts.on("-t", "--timeout N", Integer, "Per-mutation timeout in seconds") { |n| @options[:timeout] = n }
|
|
31
|
-
opts.on("-f", "--format FORMAT", "Output format: text, json") { |f| @options[:format] = f.to_sym }
|
|
32
|
-
opts.on("--diff BASE", "Only mutate code changed since BASE") { |b| @options[:diff_base] = b }
|
|
33
|
-
opts.on("--min-score FLOAT", Float, "Minimum mutation score to pass") { |s| @options[:min_score] = s }
|
|
34
|
-
opts.on("--no-coverage", "Disable coverage-based filtering of uncovered mutations") { @options[:coverage] = false }
|
|
35
|
-
opts.on("-v", "--verbose", "Verbose output") { @options[:verbose] = true }
|
|
36
|
-
opts.on("-q", "--quiet", "Suppress output") { @options[:quiet] = true }
|
|
71
|
+
add_separators(opts)
|
|
72
|
+
add_options(opts)
|
|
37
73
|
end
|
|
74
|
+
end
|
|
38
75
|
|
|
39
|
-
|
|
76
|
+
def add_separators(opts)
|
|
77
|
+
opts.separator ""
|
|
78
|
+
opts.separator "Line-range targeting: lib/foo.rb:15-30, lib/foo.rb:15, lib/foo.rb:15-"
|
|
79
|
+
opts.separator ""
|
|
80
|
+
opts.separator "Commands: run (default), init, version"
|
|
81
|
+
opts.separator ""
|
|
82
|
+
opts.separator "Options:"
|
|
40
83
|
end
|
|
41
84
|
|
|
42
|
-
def
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
when :run
|
|
50
|
-
run_mutations
|
|
85
|
+
def add_options(opts)
|
|
86
|
+
opts.on("-t", "--timeout N", Integer, "Per-mutation timeout in seconds") { |n| @options[:timeout] = n }
|
|
87
|
+
opts.on("-f", "--format FORMAT", "Output format: text, json") { |f| @options[:format] = f.to_sym }
|
|
88
|
+
opts.on("--diff BASE", "DEPRECATED: Use line-range targeting instead") do |b|
|
|
89
|
+
warn("Warning: --diff is deprecated and will be removed in a future version. " \
|
|
90
|
+
"Use line-range targeting instead: evilution run lib/foo.rb:15-30")
|
|
91
|
+
@options[:diff_base] = b
|
|
51
92
|
end
|
|
93
|
+
opts.on("--min-score FLOAT", Float, "Minimum mutation score to pass") { |s| @options[:min_score] = s }
|
|
94
|
+
opts.on("--spec FILES", Array, "Spec files to run (comma-separated)") { |f| @options[:spec_files] = f }
|
|
95
|
+
opts.on("--target METHOD", "Only mutate the named method (e.g. Foo::Bar#calculate)") { |m| @options[:target] = m }
|
|
96
|
+
opts.on("--no-coverage", "DEPRECATED: Has no effect and will be removed in a future version") do
|
|
97
|
+
warn("Warning: --no-coverage is deprecated, currently has no effect, and will be removed in a future version.")
|
|
98
|
+
@options[:coverage] = false
|
|
99
|
+
end
|
|
100
|
+
opts.on("-v", "--verbose", "Verbose output") { @options[:verbose] = true }
|
|
101
|
+
opts.on("-q", "--quiet", "Suppress output") { @options[:quiet] = true }
|
|
52
102
|
end
|
|
53
103
|
|
|
54
|
-
private
|
|
55
|
-
|
|
56
104
|
def run_init
|
|
57
105
|
path = ".evilution.yml"
|
|
58
106
|
if File.exist?(path)
|
|
@@ -65,8 +113,35 @@ module Evilution
|
|
|
65
113
|
0
|
|
66
114
|
end
|
|
67
115
|
|
|
116
|
+
def parse_file_args(raw_args)
|
|
117
|
+
files = []
|
|
118
|
+
ranges = {}
|
|
119
|
+
|
|
120
|
+
raw_args.each do |arg|
|
|
121
|
+
file, range_str = arg.split(":", 2)
|
|
122
|
+
files << file
|
|
123
|
+
next unless range_str
|
|
124
|
+
|
|
125
|
+
ranges[file] = parse_line_range(range_str)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
[files, ranges]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def parse_line_range(str)
|
|
132
|
+
if str.include?("-")
|
|
133
|
+
start_str, end_str = str.split("-", 2)
|
|
134
|
+
start_line = Integer(start_str)
|
|
135
|
+
end_line = end_str.empty? ? Float::INFINITY : Integer(end_str)
|
|
136
|
+
start_line..end_line
|
|
137
|
+
else
|
|
138
|
+
line = Integer(str)
|
|
139
|
+
line..line
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
68
143
|
def run_mutations
|
|
69
|
-
config = Config.new(**@options, target_files: @files)
|
|
144
|
+
config = Config.new(**@options, target_files: @files, line_ranges: @line_ranges)
|
|
70
145
|
runner = Runner.new(config: config)
|
|
71
146
|
summary = runner.call
|
|
72
147
|
summary.success?(min_score: config.min_score) ? 0 : 1
|
data/lib/evilution/config.rb
CHANGED
|
@@ -7,8 +7,7 @@ module Evilution
|
|
|
7
7
|
CONFIG_FILES = %w[.evilution.yml config/evilution.yml].freeze
|
|
8
8
|
|
|
9
9
|
DEFAULTS = {
|
|
10
|
-
|
|
11
|
-
timeout: 10,
|
|
10
|
+
timeout: 30,
|
|
12
11
|
format: :text,
|
|
13
12
|
diff_base: nil,
|
|
14
13
|
target: nil,
|
|
@@ -16,17 +15,20 @@ module Evilution
|
|
|
16
15
|
integration: :rspec,
|
|
17
16
|
coverage: true,
|
|
18
17
|
verbose: false,
|
|
19
|
-
quiet: false
|
|
18
|
+
quiet: false,
|
|
19
|
+
line_ranges: {},
|
|
20
|
+
spec_files: []
|
|
20
21
|
}.freeze
|
|
21
22
|
|
|
22
|
-
attr_reader :target_files, :
|
|
23
|
-
:target, :min_score, :integration, :coverage, :verbose, :quiet
|
|
23
|
+
attr_reader :target_files, :timeout, :format, :diff_base,
|
|
24
|
+
:target, :min_score, :integration, :coverage, :verbose, :quiet,
|
|
25
|
+
:line_ranges, :spec_files
|
|
24
26
|
|
|
25
27
|
def initialize(**options)
|
|
26
28
|
file_options = options.delete(:skip_config_file) ? {} : load_config_file
|
|
27
29
|
merged = DEFAULTS.merge(file_options).merge(options)
|
|
30
|
+
warn_removed_options(merged, file_options)
|
|
28
31
|
@target_files = Array(merged[:target_files])
|
|
29
|
-
@jobs = merged[:jobs] || default_jobs
|
|
30
32
|
@timeout = merged[:timeout]
|
|
31
33
|
@format = merged[:format].to_sym
|
|
32
34
|
@diff_base = merged[:diff_base]
|
|
@@ -36,6 +38,8 @@ module Evilution
|
|
|
36
38
|
@coverage = merged[:coverage]
|
|
37
39
|
@verbose = merged[:verbose]
|
|
38
40
|
@quiet = merged[:quiet]
|
|
41
|
+
@line_ranges = merged[:line_ranges] || {}
|
|
42
|
+
@spec_files = Array(merged[:spec_files])
|
|
39
43
|
freeze
|
|
40
44
|
end
|
|
41
45
|
|
|
@@ -51,17 +55,22 @@ module Evilution
|
|
|
51
55
|
!diff_base.nil?
|
|
52
56
|
end
|
|
53
57
|
|
|
58
|
+
def line_ranges?
|
|
59
|
+
!line_ranges.empty?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def target?
|
|
63
|
+
!target.nil?
|
|
64
|
+
end
|
|
65
|
+
|
|
54
66
|
# Generates a default config file template.
|
|
55
67
|
def self.default_template
|
|
56
68
|
<<~YAML
|
|
57
69
|
# Evilution configuration
|
|
58
70
|
# See: https://github.com/marinazzio/evilution
|
|
59
71
|
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
|
|
63
|
-
# Per-mutation timeout in seconds (default: 10)
|
|
64
|
-
# timeout: 10
|
|
72
|
+
# Per-mutation timeout in seconds (default: 30)
|
|
73
|
+
# timeout: 30
|
|
65
74
|
|
|
66
75
|
# Output format: text or json (default: text)
|
|
67
76
|
# format: text
|
|
@@ -72,13 +81,30 @@ module Evilution
|
|
|
72
81
|
# Test integration: rspec (default: rspec)
|
|
73
82
|
# integration: rspec
|
|
74
83
|
|
|
75
|
-
#
|
|
84
|
+
# DEPRECATED: Coverage filtering is deprecated and will be removed
|
|
76
85
|
# coverage: true
|
|
77
86
|
YAML
|
|
78
87
|
end
|
|
79
88
|
|
|
80
89
|
private
|
|
81
90
|
|
|
91
|
+
def warn_removed_options(merged, file_options)
|
|
92
|
+
if merged.key?(:jobs)
|
|
93
|
+
warn("Warning: 'jobs' option is no longer supported and will be ignored. " \
|
|
94
|
+
"Remove it from your configuration or invocation.")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
if file_options.key?(:coverage)
|
|
98
|
+
warn("Warning: 'coverage' in config file is deprecated and ignored. " \
|
|
99
|
+
"This option will be removed in a future version.")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
return unless file_options[:diff_base]
|
|
103
|
+
|
|
104
|
+
warn("Warning: 'diff_base' in config file is deprecated and will be removed in a future version. " \
|
|
105
|
+
"Use line-range targeting instead: evilution run lib/foo.rb:15-30")
|
|
106
|
+
end
|
|
107
|
+
|
|
82
108
|
def load_config_file
|
|
83
109
|
CONFIG_FILES.each do |path|
|
|
84
110
|
next unless File.exist?(path)
|
|
@@ -89,10 +115,5 @@ module Evilution
|
|
|
89
115
|
|
|
90
116
|
{}
|
|
91
117
|
end
|
|
92
|
-
|
|
93
|
-
def default_jobs
|
|
94
|
-
require "etc"
|
|
95
|
-
Etc.nprocessors
|
|
96
|
-
end
|
|
97
118
|
end
|
|
98
119
|
end
|
|
@@ -108,77 +108,10 @@ module Evilution
|
|
|
108
108
|
{ passed: false, error: e.message }
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
-
def build_args(
|
|
112
|
-
files = test_files ||
|
|
111
|
+
def build_args(_mutation)
|
|
112
|
+
files = test_files || ["spec"]
|
|
113
113
|
["--format", "progress", "--no-color", "--order", "defined", *files]
|
|
114
114
|
end
|
|
115
|
-
|
|
116
|
-
def detect_test_files(mutation)
|
|
117
|
-
# Convention: lib/foo/bar.rb -> spec/foo/bar_spec.rb
|
|
118
|
-
candidates = spec_file_candidates(mutation.file_path)
|
|
119
|
-
found = candidates.select { |f| File.exist?(f) }
|
|
120
|
-
return found unless found.empty?
|
|
121
|
-
|
|
122
|
-
# Fallback: find spec/ directory relative to the mutation's project root
|
|
123
|
-
fallback = fallback_spec_dir(mutation.file_path)
|
|
124
|
-
return [fallback] if fallback
|
|
125
|
-
|
|
126
|
-
[]
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def fallback_spec_dir(source_path)
|
|
130
|
-
expanded = File.expand_path(source_path)
|
|
131
|
-
|
|
132
|
-
# Derive spec/ from mutation's project, not CWD
|
|
133
|
-
if expanded.include?("/lib/")
|
|
134
|
-
project_root = expanded.split(%r{/lib/}, 2).first
|
|
135
|
-
spec_dir = File.join(project_root, "spec")
|
|
136
|
-
return spec_dir if Dir.exist?(spec_dir)
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# Walk up from the file's directory looking for spec/
|
|
140
|
-
dir = File.dirname(expanded)
|
|
141
|
-
loop do
|
|
142
|
-
spec_dir = File.join(dir, "spec")
|
|
143
|
-
return spec_dir if Dir.exist?(spec_dir)
|
|
144
|
-
|
|
145
|
-
parent = File.dirname(dir)
|
|
146
|
-
break if parent == dir
|
|
147
|
-
|
|
148
|
-
dir = parent
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
nil
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def spec_file_candidates(source_path)
|
|
155
|
-
candidates = []
|
|
156
|
-
|
|
157
|
-
if source_path.start_with?("lib/")
|
|
158
|
-
# lib/foo/bar.rb -> spec/foo/bar_spec.rb
|
|
159
|
-
relative = source_path.sub(%r{^lib/}, "")
|
|
160
|
-
spec_name = relative.sub(/\.rb$/, "_spec.rb")
|
|
161
|
-
candidates << File.join("spec", spec_name)
|
|
162
|
-
candidates << File.join("spec", "unit", spec_name)
|
|
163
|
-
elsif source_path.include?("/lib/")
|
|
164
|
-
# /absolute/path/lib/foo/bar.rb -> /absolute/path/spec/foo/bar_spec.rb
|
|
165
|
-
prefix, relative = source_path.split(%r{/lib/}, 2)
|
|
166
|
-
spec_name = relative.sub(/\.rb$/, "_spec.rb")
|
|
167
|
-
candidates << File.join(prefix, "spec", spec_name)
|
|
168
|
-
candidates << File.join(prefix, "spec", "unit", spec_name)
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# Same directory: foo/bar.rb -> foo/bar_spec.rb
|
|
172
|
-
sibling_spec = source_path.sub(/\.rb$/, "_spec.rb")
|
|
173
|
-
candidates << sibling_spec
|
|
174
|
-
|
|
175
|
-
# Subdirectory spec/ variant: foo/bar.rb -> foo/spec/bar_spec.rb
|
|
176
|
-
dir = File.dirname(source_path)
|
|
177
|
-
base = File.basename(source_path, ".rb")
|
|
178
|
-
candidates << File.join(dir, "spec", "#{base}_spec.rb")
|
|
179
|
-
|
|
180
|
-
candidates.uniq
|
|
181
|
-
end
|
|
182
115
|
end
|
|
183
116
|
end
|
|
184
117
|
end
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
3
6
|
module Evilution
|
|
4
7
|
module Isolation
|
|
5
8
|
class Fork
|
|
9
|
+
GRACE_PERIOD = 2
|
|
10
|
+
|
|
6
11
|
def call(mutation:, test_command:, timeout:)
|
|
12
|
+
sandbox_dir = Dir.mktmpdir("evilution-run")
|
|
7
13
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
8
14
|
read_io, write_io = IO.pipe
|
|
9
15
|
|
|
10
16
|
pid = ::Process.fork do
|
|
17
|
+
ENV["TMPDIR"] = sandbox_dir
|
|
11
18
|
read_io.close
|
|
12
19
|
result = execute_in_child(mutation, test_command)
|
|
13
20
|
Marshal.dump(result, write_io)
|
|
@@ -23,10 +30,20 @@ module Evilution
|
|
|
23
30
|
ensure
|
|
24
31
|
read_io&.close
|
|
25
32
|
write_io&.close
|
|
33
|
+
restore_original_source(mutation)
|
|
34
|
+
FileUtils.rm_rf(sandbox_dir) if sandbox_dir
|
|
26
35
|
end
|
|
27
36
|
|
|
28
37
|
private
|
|
29
38
|
|
|
39
|
+
def restore_original_source(mutation)
|
|
40
|
+
return if File.read(mutation.file_path) == mutation.original_source
|
|
41
|
+
|
|
42
|
+
File.write(mutation.file_path, mutation.original_source)
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
warn("Warning: failed to restore #{mutation.file_path}: #{e.message}")
|
|
45
|
+
end
|
|
46
|
+
|
|
30
47
|
def execute_in_child(mutation, test_command)
|
|
31
48
|
test_command.call(mutation)
|
|
32
49
|
rescue StandardError => e
|
|
@@ -42,12 +59,24 @@ module Evilution
|
|
|
42
59
|
::Process.wait(pid) rescue nil # rubocop:disable Style/RescueModifier
|
|
43
60
|
{ timeout: false, passed: false, error: "empty result from child" }
|
|
44
61
|
else
|
|
45
|
-
|
|
46
|
-
::Process.wait(pid) rescue nil # rubocop:disable Style/RescueModifier
|
|
62
|
+
terminate_child(pid)
|
|
47
63
|
{ timeout: true }
|
|
48
64
|
end
|
|
49
65
|
end
|
|
50
66
|
|
|
67
|
+
def terminate_child(pid)
|
|
68
|
+
::Process.kill("TERM", pid) rescue nil # rubocop:disable Style/RescueModifier
|
|
69
|
+
_, status = ::Process.waitpid2(pid, ::Process::WNOHANG)
|
|
70
|
+
return if status
|
|
71
|
+
|
|
72
|
+
sleep(GRACE_PERIOD)
|
|
73
|
+
_, status = ::Process.waitpid2(pid, ::Process::WNOHANG)
|
|
74
|
+
return if status
|
|
75
|
+
|
|
76
|
+
::Process.kill("KILL", pid) rescue nil # rubocop:disable Style/RescueModifier
|
|
77
|
+
::Process.wait(pid) rescue nil # rubocop:disable Style/RescueModifier
|
|
78
|
+
end
|
|
79
|
+
|
|
51
80
|
def build_mutation_result(mutation, result, duration)
|
|
52
81
|
status = if result[:timeout]
|
|
53
82
|
:timeout
|
data/lib/evilution/runner.rb
CHANGED
|
@@ -4,13 +4,10 @@ require_relative "config"
|
|
|
4
4
|
require_relative "ast/parser"
|
|
5
5
|
require_relative "mutator/registry"
|
|
6
6
|
require_relative "isolation/fork"
|
|
7
|
-
require_relative "parallel/pool"
|
|
8
7
|
require_relative "integration/rspec"
|
|
9
8
|
require_relative "reporter/json"
|
|
10
9
|
require_relative "reporter/cli"
|
|
11
10
|
require_relative "reporter/suggestion"
|
|
12
|
-
require_relative "coverage/collector"
|
|
13
|
-
require_relative "coverage/test_map"
|
|
14
11
|
require_relative "diff/parser"
|
|
15
12
|
require_relative "diff/file_filter"
|
|
16
13
|
require_relative "result/mutation_result"
|
|
@@ -31,12 +28,11 @@ module Evilution
|
|
|
31
28
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
32
29
|
|
|
33
30
|
subjects = parse_subjects
|
|
31
|
+
subjects = filter_by_target(subjects) if config.target?
|
|
32
|
+
subjects = filter_by_line_ranges(subjects) if config.line_ranges?
|
|
34
33
|
subjects = filter_by_diff(subjects) if config.diff?
|
|
35
34
|
mutations = generate_mutations(subjects)
|
|
36
|
-
test_map = collect_coverage if config.coverage && config.integration == :rspec
|
|
37
|
-
mutations, skipped = filter_by_coverage(mutations, test_map) if test_map
|
|
38
35
|
results = run_mutations(mutations)
|
|
39
|
-
results.concat(skipped) if skipped
|
|
40
36
|
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
41
37
|
|
|
42
38
|
summary = Result::Summary.new(results: results, duration: duration)
|
|
@@ -53,6 +49,24 @@ module Evilution
|
|
|
53
49
|
config.target_files.flat_map { |file| parser.call(file) }
|
|
54
50
|
end
|
|
55
51
|
|
|
52
|
+
def filter_by_target(subjects)
|
|
53
|
+
matched = subjects.select { |s| s.name == config.target }
|
|
54
|
+
raise Error, "no method found matching '#{config.target}'" if matched.empty?
|
|
55
|
+
|
|
56
|
+
matched
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def filter_by_line_ranges(subjects)
|
|
60
|
+
subjects.select do |subject|
|
|
61
|
+
range = config.line_ranges[subject.file_path]
|
|
62
|
+
next true unless range
|
|
63
|
+
|
|
64
|
+
subject_start = subject.line_number
|
|
65
|
+
subject_end = subject_start + subject.source.count("\n")
|
|
66
|
+
subject_start <= range.last && subject_end >= range.first
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
56
70
|
def filter_by_diff(subjects)
|
|
57
71
|
diff_parser = Diff::Parser.new
|
|
58
72
|
changed_ranges = diff_parser.parse(config.diff_base)
|
|
@@ -63,37 +77,9 @@ module Evilution
|
|
|
63
77
|
subjects.flat_map { |subject| registry.mutations_for(subject) }
|
|
64
78
|
end
|
|
65
79
|
|
|
66
|
-
def collect_coverage
|
|
67
|
-
test_files = Dir.glob("spec/**/*_spec.rb")
|
|
68
|
-
return nil if test_files.empty?
|
|
69
|
-
|
|
70
|
-
data = Coverage::Collector.new.call(test_files: test_files)
|
|
71
|
-
Coverage::TestMap.new(data)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def filter_by_coverage(mutations, test_map)
|
|
75
|
-
covered, uncovered = mutations.partition do |m|
|
|
76
|
-
test_map.covered?(File.expand_path(m.file_path), m.line)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
skipped = uncovered.map do |m|
|
|
80
|
-
Result::MutationResult.new(mutation: m, status: :survived, duration: 0.0)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
[covered, skipped]
|
|
84
|
-
end
|
|
85
|
-
|
|
86
80
|
def run_mutations(mutations)
|
|
87
81
|
integration = build_integration
|
|
88
82
|
|
|
89
|
-
if config.jobs > 1 && mutations.size > 1
|
|
90
|
-
run_parallel(mutations, integration)
|
|
91
|
-
else
|
|
92
|
-
run_sequential(mutations, integration)
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def run_sequential(mutations, integration)
|
|
97
83
|
mutations.map do |mutation|
|
|
98
84
|
test_command = ->(m) { integration.call(m) }
|
|
99
85
|
isolator.call(
|
|
@@ -104,16 +90,11 @@ module Evilution
|
|
|
104
90
|
end
|
|
105
91
|
end
|
|
106
92
|
|
|
107
|
-
def run_parallel(mutations, integration)
|
|
108
|
-
pool = Parallel::Pool.new(jobs: config.jobs)
|
|
109
|
-
test_command_builder = ->(_mutation) { ->(m) { integration.call(m) } }
|
|
110
|
-
pool.call(mutations: mutations, test_command_builder: test_command_builder, timeout: config.timeout)
|
|
111
|
-
end
|
|
112
|
-
|
|
113
93
|
def build_integration
|
|
114
94
|
case config.integration
|
|
115
95
|
when :rspec
|
|
116
|
-
|
|
96
|
+
test_files = config.spec_files.empty? ? nil : config.spec_files
|
|
97
|
+
Integration::RSpec.new(test_files: test_files)
|
|
117
98
|
else
|
|
118
99
|
raise Error, "unknown integration: #{config.integration}"
|
|
119
100
|
end
|
data/lib/evilution/version.rb
CHANGED
data/lib/evilution.rb
CHANGED
|
@@ -27,8 +27,6 @@ require_relative "evilution/mutator/operator/return_value_removal"
|
|
|
27
27
|
require_relative "evilution/mutator/operator/collection_replacement"
|
|
28
28
|
require_relative "evilution/mutator/registry"
|
|
29
29
|
require_relative "evilution/isolation/fork"
|
|
30
|
-
require_relative "evilution/parallel/worker"
|
|
31
|
-
require_relative "evilution/parallel/pool"
|
|
32
30
|
require_relative "evilution/diff/parser"
|
|
33
31
|
require_relative "evilution/diff/file_filter"
|
|
34
32
|
require_relative "evilution/integration/base"
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: evilution
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Denis Kiselev
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-03-12 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: diff-lcs
|
|
@@ -89,8 +90,6 @@ files:
|
|
|
89
90
|
- lib/evilution/mutator/operator/string_literal.rb
|
|
90
91
|
- lib/evilution/mutator/operator/symbol_literal.rb
|
|
91
92
|
- lib/evilution/mutator/registry.rb
|
|
92
|
-
- lib/evilution/parallel/pool.rb
|
|
93
|
-
- lib/evilution/parallel/worker.rb
|
|
94
93
|
- lib/evilution/reporter/cli.rb
|
|
95
94
|
- lib/evilution/reporter/json.rb
|
|
96
95
|
- lib/evilution/reporter/suggestion.rb
|
|
@@ -107,9 +106,11 @@ metadata:
|
|
|
107
106
|
allowed_push_host: https://rubygems.org
|
|
108
107
|
bug_tracker_uri: https://github.com/marinazzio/evilution/issues
|
|
109
108
|
documentation_uri: https://github.com/marinazzio/evilution/blob/master/README.md
|
|
109
|
+
changelog_uri: https://github.com/marinazzio/evilution/blob/master/CHANGELOG.md
|
|
110
110
|
homepage_uri: https://github.com/marinazzio/evilution
|
|
111
111
|
rubygems_mfa_required: 'true'
|
|
112
112
|
source_code_uri: https://github.com/marinazzio/evilution
|
|
113
|
+
post_install_message:
|
|
113
114
|
rdoc_options: []
|
|
114
115
|
require_paths:
|
|
115
116
|
- lib
|
|
@@ -124,7 +125,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
124
125
|
- !ruby/object:Gem::Version
|
|
125
126
|
version: '0'
|
|
126
127
|
requirements: []
|
|
127
|
-
rubygems_version:
|
|
128
|
+
rubygems_version: 3.5.22
|
|
129
|
+
signing_key:
|
|
128
130
|
specification_version: 4
|
|
129
131
|
summary: Free, MIT-licensed mutation testing for Ruby
|
|
130
132
|
test_files: []
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Evilution
|
|
4
|
-
module Parallel
|
|
5
|
-
class Pool
|
|
6
|
-
def initialize(jobs:)
|
|
7
|
-
@jobs = jobs
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
# Executes mutations in parallel across N worker processes.
|
|
11
|
-
#
|
|
12
|
-
# @param mutations [Array] Array of mutation objects to run
|
|
13
|
-
# @param test_command_builder [#call] Callable that receives a mutation and returns a test command callable
|
|
14
|
-
# @param timeout [Numeric] Per-mutation timeout in seconds
|
|
15
|
-
# @return [Array<Result::MutationResult>]
|
|
16
|
-
def call(mutations:, test_command_builder:, timeout:)
|
|
17
|
-
return [] if mutations.empty?
|
|
18
|
-
|
|
19
|
-
worker_count = [@jobs, mutations.size].min
|
|
20
|
-
chunks = partition(mutations, worker_count)
|
|
21
|
-
|
|
22
|
-
pipes = worker_count.times.map { IO.pipe }
|
|
23
|
-
|
|
24
|
-
pids = chunks.each_with_index.map do |chunk, index|
|
|
25
|
-
_, write_io = pipes[index]
|
|
26
|
-
|
|
27
|
-
pid = Process.fork do
|
|
28
|
-
# Close all read ends in the child; close sibling write ends too
|
|
29
|
-
pipes.each_with_index do |(r, w), i|
|
|
30
|
-
if i == index
|
|
31
|
-
r.close
|
|
32
|
-
else
|
|
33
|
-
r.close
|
|
34
|
-
w.close
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
results = run_chunk(chunk, test_command_builder, timeout)
|
|
39
|
-
Marshal.dump(results, write_io)
|
|
40
|
-
write_io.close
|
|
41
|
-
exit!(0)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
write_io.close
|
|
45
|
-
pid
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
results = collect_results(pipes.map(&:first), pids)
|
|
49
|
-
|
|
50
|
-
results
|
|
51
|
-
ensure
|
|
52
|
-
# Ensure all pipes are closed even if something goes wrong
|
|
53
|
-
pipes&.each do |read_io, write_io|
|
|
54
|
-
read_io.close unless read_io.closed?
|
|
55
|
-
write_io.close unless write_io.closed?
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
private
|
|
60
|
-
|
|
61
|
-
attr_reader :jobs
|
|
62
|
-
|
|
63
|
-
# Distributes mutations across N chunks using round-robin.
|
|
64
|
-
# File isolation is handled by Integration::RSpec via temp directories
|
|
65
|
-
# and $LOAD_PATH, so same-file mutations can safely run in parallel.
|
|
66
|
-
def partition(mutations, n)
|
|
67
|
-
chunks = Array.new(n) { [] }
|
|
68
|
-
mutations.each_with_index { |m, i| chunks[i % n] << m }
|
|
69
|
-
chunks
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# Runs a chunk of mutations sequentially inside a worker process.
|
|
73
|
-
def run_chunk(mutations, test_command_builder, timeout)
|
|
74
|
-
worker = Worker.new
|
|
75
|
-
worker.call(mutations: mutations, test_command_builder: test_command_builder, timeout: timeout)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Reads results from all worker pipes and waits for workers to finish.
|
|
79
|
-
def collect_results(read_ios, pids)
|
|
80
|
-
results = []
|
|
81
|
-
|
|
82
|
-
read_ios.each_with_index do |read_io, _index|
|
|
83
|
-
data = read_io.read
|
|
84
|
-
read_io.close
|
|
85
|
-
|
|
86
|
-
unless data.empty?
|
|
87
|
-
chunk_results = Marshal.load(data) # rubocop:disable Security/MarshalLoad
|
|
88
|
-
results.concat(chunk_results)
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
pids.each { |pid| Process.wait(pid) rescue nil } # rubocop:disable Style/RescueModifier
|
|
93
|
-
|
|
94
|
-
results
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Evilution
|
|
4
|
-
module Parallel
|
|
5
|
-
class Worker
|
|
6
|
-
def initialize(isolator: Isolation::Fork.new)
|
|
7
|
-
@isolator = isolator
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
# Runs a batch of mutations sequentially using fork isolation.
|
|
11
|
-
#
|
|
12
|
-
# @param mutations [Array<Mutation>] Mutations to execute
|
|
13
|
-
# @param test_command_builder [#call] Receives a mutation, returns a test command callable
|
|
14
|
-
# @param timeout [Numeric] Per-mutation timeout in seconds
|
|
15
|
-
# @return [Array<Result::MutationResult>]
|
|
16
|
-
def call(mutations:, test_command_builder:, timeout:)
|
|
17
|
-
mutations.map do |mutation|
|
|
18
|
-
test_command = test_command_builder.call(mutation)
|
|
19
|
-
@isolator.call(mutation: mutation, test_command: test_command, timeout: timeout)
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|