evilution 0.4.0 → 0.5.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/issues.jsonl +13 -12
- data/CHANGELOG.md +39 -0
- data/lib/evilution/cli.rb +3 -9
- data/lib/evilution/config.rb +17 -6
- data/lib/evilution/integration/rspec.rb +11 -2
- data/lib/evilution/isolation/fork.rb +6 -0
- data/lib/evilution/parallel/pool.rb +63 -0
- data/lib/evilution/runner.rb +49 -0
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 02de08018abc9539f569f074024674e60264f525d5693de8931bfc4e61dbe0fe
|
|
4
|
+
data.tar.gz: 3aab82bebf9a9f0d0732260e7852f72854b6aa774f548eff764127833af27445
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a3051426f0e7469408fc04850369a7df0022119b30a2cb56ea85a61e61caf239ec6e8b33bb59870c960c85492e2b32dca8a95015cb77778393038dfc4478d5bc
|
|
7
|
+
data.tar.gz: 252e15324dc314484fa3c136d25239856a39916da3af47fb5f5a399a5048804bdecbab54ac11f8c650a31033d485dc75f1c23132b2dddb6e8bdae59c31f5a6ec
|
data/.beads/issues.jsonl
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
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
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
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":"
|
|
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":"closed","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-16T10:11:01.311634237+07:00","closed_at":"2026-03-16T10:11:01.311634237+07:00","close_reason":"Already merged and released in v0.4.0"}
|
|
17
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"}
|
|
18
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"}
|
|
19
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"}]}
|
|
@@ -30,12 +30,12 @@
|
|
|
30
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"}]}
|
|
31
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
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":"
|
|
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":"
|
|
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":"
|
|
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":"
|
|
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":"closed","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-16T14:49:23.063583958+07:00","closed_at":"2026-03-16T14:49:23.063583958+07:00","close_reason":"All children complete: fail-fast, per-mutation spec targeting","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":"closed","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-16T10:11:01.311500554+07:00","closed_at":"2026-03-16T10:11:01.311500554+07:00","close_reason":"Already merged and released in v0.4.0"}
|
|
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":"closed","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-16T14:49:13.616876819+07:00","closed_at":"2026-03-16T14:49:13.616876819+07:00","close_reason":"Fixed and merged","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":"closed","priority":1,"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-16T11:15:24.900944562+07:00","closed_at":"2026-03-16T11:15:24.900944562+07:00","close_reason":"All children complete: structured errors, test_command in JSON, noise suppression","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"},{"issue_id":"EV-24","depends_on_id":"EV-40","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
37
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":"closed","priority":1,"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-15T22:41:54.370789377+07:00","closed_at":"2026-03-15T22:41:54.370789377+07:00","close_reason":"Merged PR #74 — structured JSON error output in CLI"}
|
|
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":"
|
|
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":"closed","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-16T10:11:01.311631114+07:00","closed_at":"2026-03-16T10:11:01.311631114+07:00","close_reason":"Already merged and released in v0.4.0"}
|
|
39
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
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
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"}
|
|
@@ -63,9 +63,9 @@
|
|
|
63
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
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
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":"
|
|
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":"
|
|
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":"
|
|
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":"closed","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-16T10:11:01.311639679+07:00","closed_at":"2026-03-16T10:11:01.311639679+07:00","close_reason":"Already merged and released in v0.4.0","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":"closed","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-16T10:11:01.311622041+07:00","closed_at":"2026-03-16T10:11:01.311622041+07:00","close_reason":"Already merged and released in v0.4.0"}
|
|
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":"closed","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-16T10:11:01.311627178+07:00","closed_at":"2026-03-16T10:11:01.311627178+07:00","close_reason":"Already merged and released in v0.4.0"}
|
|
69
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
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
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"}
|
|
@@ -81,10 +81,11 @@
|
|
|
81
81
|
{"id":"EV-4.7","title":"Integrate parallel execution into Runner","description":"Update Runner to use Parallel::Pool when jobs > 1. Replace sequential mutation execution with pool.run. Verify results are identical. File: lib/evilution/runner.rb (edit).","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:45.637953763+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:54:25.596270941+07:00","closed_at":"2026-03-02T11:54:25.596270941+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-4.7","depends_on_id":"EV-4","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-4.7","depends_on_id":"EV-4.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-4.7","depends_on_id":"EV-2.12","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
82
82
|
{"id":"EV-4.8","title":"Integrate coverage-based filtering into Runner","description":"Update Runner to optionally collect coverage on first test suite run, build TestMap, and skip mutations on uncovered lines. Controlled by --no-coverage flag. File: lib/evilution/runner.rb (edit).","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:45.745583233+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-06T11:02:35.638752306+07:00","closed_at":"2026-03-02T11:54:25.596274893+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-4.8","depends_on_id":"EV-4","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-4.8","depends_on_id":"EV-4.4","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-4.8","depends_on_id":"EV-2.12","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
83
83
|
{"id":"EV-4.9","title":"Integrate diff-based targeting into Runner","description":"Update Runner to optionally use Diff::Parser + FileFilter when --diff flag is set. Filter subjects before mutation generation. File: lib/evilution/runner.rb (edit).","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:45.851982561+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:54:25.596280246+07:00","closed_at":"2026-03-02T11:54:25.596280246+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-4.9","depends_on_id":"EV-4","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-4.9","depends_on_id":"EV-4.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-4.9","depends_on_id":"EV-2.12","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
84
|
-
{"id":"EV-40","title":"Separate rspec noise from evilution output (stdout/stderr)","description":"RSpec warnings flood stdout and corrupt JSON output. When using --format json, the JSON gets buried in hundreds of lines of rspec warnings. Fix: redirect rspec subprocess output to stderr (or /dev/null), keep evilution results on stdout. Critical for piping JSON output.","status":"
|
|
85
|
-
{"id":"EV-41","title":"Progress indicator during mutation runs","description":"Zero output during multi-minute runs makes it look stuck. Add a simple 'mutation 3/19 killed...' progress line to stderr so users know work is happening. Only show in text mode or when stderr is a TTY.","status":"
|
|
84
|
+
{"id":"EV-40","title":"Separate rspec noise from evilution output (stdout/stderr)","description":"RSpec warnings flood stdout and corrupt JSON output. When using --format json, the JSON gets buried in hundreds of lines of rspec warnings. Fix: redirect rspec subprocess output to stderr (or /dev/null), keep evilution results on stdout. Critical for piping JSON output.","status":"closed","priority":1,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-13T09:56:58.604909176+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-16T11:15:15.02691418+07:00","closed_at":"2026-03-16T11:15:15.02691418+07:00","close_reason":"Fixed and merged via PR #86"}
|
|
85
|
+
{"id":"EV-41","title":"Progress indicator during mutation runs","description":"Zero output during multi-minute runs makes it look stuck. Add a simple 'mutation 3/19 killed...' progress line to stderr so users know work is happening. Only show in text mode or when stderr is a TTY.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-13T09:57:01.023293323+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-16T11:38:40.674664201+07:00","closed_at":"2026-03-16T11:38:40.674664201+07:00","close_reason":"Fixed and merged via PR #87"}
|
|
86
86
|
{"id":"EV-42","title":"Expand mutation operators (method call removal, receiver replacement, etc.)","description":"Currently 18 operators generating ~19 mutations vs mutant's 78 for the same method. Missing operators: method call removal, self receiver replacement, additional boolean logic mutations, nil substitutions. Higher operator count catches more subtle bugs and produces a more meaningful mutation score.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-13T09:57:03.039944039+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-13T09:57:03.039944039+07:00"}
|
|
87
|
-
{"id":"EV-43","title":"Fix --version flag (returns 'version unknown')","description":"Running 'evilution --version' returns 'version unknown' instead of the actual version. The 'evilution version' subcommand works, but --version as a flag does not. Users expect --version to work.","status":"
|
|
87
|
+
{"id":"EV-43","title":"Fix --version flag (returns 'version unknown')","description":"Running 'evilution --version' returns 'version unknown' instead of the actual version. The 'evilution version' subcommand works, but --version as a flag does not. Users expect --version to work.","status":"closed","priority":1,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-13T09:57:05.048211823+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-16T10:31:01.045381336+07:00","closed_at":"2026-03-16T10:31:01.045381336+07:00","close_reason":"Fixed and merged"}
|
|
88
|
+
{"id":"EV-44","title":"Re-introduce parallel execution","description":"Bring back parallel mutation execution. Consider whether two-level fork model is still right, temp-file isolation needs, and auto-detecting when parallelism is worthwhile. GH #35.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-16T10:15:42.050318104+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-16T16:26:30.341909415+07:00","closed_at":"2026-03-16T16:26:30.341909415+07:00","close_reason":"PR #90 merged — process-based parallel pool with --jobs flag"}
|
|
88
89
|
{"id":"EV-5","title":"Phase 4: Polish","description":"Suggestion generator, .evilution.yml config file loading, evilution init subcommand, error handling, README","status":"closed","priority":2,"issue_type":"epic","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:03.497091872+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:54:32.782883746+07:00","closed_at":"2026-03-02T11:54:32.782883746+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-5","depends_on_id":"EV-4","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
89
90
|
{"id":"EV-5.1","title":"Implement Evilution::Reporter::Suggestion","description":"Generates actionable fix suggestions per surviving mutant. Each operator type has a suggestion template. E.g., ComparisonReplacement >= -> > suggests 'Add a test for the boundary case where value equals exactly [threshold]'. File: lib/evilution/reporter/suggestion.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:58.038411009+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:54:21.518470747+07:00","closed_at":"2026-03-02T11:54:21.518470747+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-5.1","depends_on_id":"EV-5","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-5.1","depends_on_id":"EV-2.8","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-5.1","depends_on_id":"EV-3.21","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
90
91
|
{"id":"EV-5.2","title":"Implement .evilution.yml config file loading","description":"Load config from .evilution.yml / config/evilution.yml if present. YAML keys map to Config fields. CLI flags override file values. File: update lib/evilution/config.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:58.142538793+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:54:21.518473527+07:00","closed_at":"2026-03-02T11:54:21.518473527+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-5.2","depends_on_id":"EV-5","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-5.2","depends_on_id":"EV-2.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0] - 2026-03-16
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Parallel execution** (`--jobs N` / `-j N`) — re-introduces parallel mutation execution using a process-based pool; each mutation runs in its own fork-isolated child process; fail-fast is checked between batches
|
|
8
|
+
- **Per-mutation spec targeting** — automatically resolves the matching spec file for each mutated source file using convention-based resolution; falls back to the full suite if no match; `--spec` flag overrides auto-detection
|
|
9
|
+
- **Progress indicator** — prints `mutation 3/19 killed` progress to stderr during text-mode runs so long-running sessions no longer appear stuck; only shown when stderr is a TTY, suppressed in quiet and JSON modes
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **`--version` flag** — now correctly outputs the gem version instead of "version unknown"
|
|
14
|
+
- **RSpec noise suppression** — child process stdout/stderr is redirected to `/dev/null` so RSpec warnings no longer corrupt JSON output or flood the terminal
|
|
15
|
+
|
|
16
|
+
## [0.4.0] - 2026-03-16
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`--fail-fast` flag** — stop after N surviving mutants (`--fail-fast`, `--fail-fast=3`, `--fail-fast 5`); defaults to 1 when given without a value
|
|
21
|
+
- **Structured JSON error responses** — errors in `--format json` mode now output structured JSON with `type`, `message`, and optional `file` fields
|
|
22
|
+
- **Convention-based spec file resolution** — automatically maps source files to their spec counterparts (`lib/` → `spec/`, `app/` → `spec/`)
|
|
23
|
+
- **`test_command` in mutation result JSON** — each mutation result now includes the RSpec command used, for easier debugging
|
|
24
|
+
- **Auto-detect changed files from git merge base** — when no explicit files are given, Evilution automatically finds changed `.rb` files under `lib/` and `app/` since the merge base with `main`/`master` (including `origin/` remotes for CI)
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Error classes (`ConfigError`, `ParseError`) now support a `file:` keyword for richer error context
|
|
29
|
+
|
|
30
|
+
## [0.3.0] - 2026-03-13
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **Sandbox-based temp directory cleanup** — leaked temp directories from timed-out children are now reliably cleaned up
|
|
35
|
+
- **Graceful timeout handling** — sends SIGTERM with a grace period before SIGKILL on child timeout
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- Default per-mutation timeout increased from 10s to 30s
|
|
40
|
+
- Parent process now restores the original source file after each mutation (defense-in-depth)
|
|
41
|
+
|
|
3
42
|
## [0.2.0] - 2026-03-10
|
|
4
43
|
|
|
5
44
|
### Added
|
data/lib/evilution/cli.rb
CHANGED
|
@@ -51,15 +51,7 @@ module Evilution
|
|
|
51
51
|
i = 0
|
|
52
52
|
while i < argv.length
|
|
53
53
|
arg = argv[i]
|
|
54
|
-
if
|
|
55
|
-
warn("Warning: --jobs is no longer supported and will be ignored.")
|
|
56
|
-
next_arg = argv[i + 1]
|
|
57
|
-
numeric_next = next_arg && next_arg.match?(/\A-?\d+\z/)
|
|
58
|
-
i += numeric_next ? 2 : 1
|
|
59
|
-
elsif arg.start_with?("--jobs=") || arg.match?(/\A-j-?\d+\z/)
|
|
60
|
-
warn("Warning: --jobs is no longer supported and will be ignored.")
|
|
61
|
-
i += 1
|
|
62
|
-
elsif arg == "--fail-fast"
|
|
54
|
+
if arg == "--fail-fast"
|
|
63
55
|
next_arg = argv[i + 1]
|
|
64
56
|
|
|
65
57
|
if next_arg && next_arg.match?(/\A-?\d+\z/)
|
|
@@ -83,6 +75,7 @@ module Evilution
|
|
|
83
75
|
def build_option_parser
|
|
84
76
|
OptionParser.new do |opts|
|
|
85
77
|
opts.banner = "Usage: evilution [command] [options] [files...]"
|
|
78
|
+
opts.version = VERSION
|
|
86
79
|
add_separators(opts)
|
|
87
80
|
add_options(opts)
|
|
88
81
|
end
|
|
@@ -98,6 +91,7 @@ module Evilution
|
|
|
98
91
|
end
|
|
99
92
|
|
|
100
93
|
def add_options(opts)
|
|
94
|
+
opts.on("-j", "--jobs N", Integer, "Number of parallel workers (default: 1)") { |n| @options[:jobs] = n }
|
|
101
95
|
opts.on("-t", "--timeout N", Integer, "Per-mutation timeout in seconds") { |n| @options[:timeout] = n }
|
|
102
96
|
opts.on("-f", "--format FORMAT", "Output format: text, json") { |f| @options[:format] = f.to_sym }
|
|
103
97
|
opts.on("--diff BASE", "DEPRECATED: Use line-range targeting instead") do |b|
|
data/lib/evilution/config.rb
CHANGED
|
@@ -16,6 +16,7 @@ module Evilution
|
|
|
16
16
|
coverage: true,
|
|
17
17
|
verbose: false,
|
|
18
18
|
quiet: false,
|
|
19
|
+
jobs: 1,
|
|
19
20
|
fail_fast: nil,
|
|
20
21
|
line_ranges: {},
|
|
21
22
|
spec_files: []
|
|
@@ -23,7 +24,7 @@ module Evilution
|
|
|
23
24
|
|
|
24
25
|
attr_reader :target_files, :timeout, :format, :diff_base,
|
|
25
26
|
:target, :min_score, :integration, :coverage, :verbose, :quiet,
|
|
26
|
-
:fail_fast, :line_ranges, :spec_files
|
|
27
|
+
:jobs, :fail_fast, :line_ranges, :spec_files
|
|
27
28
|
|
|
28
29
|
def initialize(**options)
|
|
29
30
|
file_options = options.delete(:skip_config_file) ? {} : load_config_file
|
|
@@ -39,6 +40,7 @@ module Evilution
|
|
|
39
40
|
@coverage = merged[:coverage]
|
|
40
41
|
@verbose = merged[:verbose]
|
|
41
42
|
@quiet = merged[:quiet]
|
|
43
|
+
@jobs = validate_jobs(merged[:jobs])
|
|
42
44
|
@fail_fast = validate_fail_fast(merged[:fail_fast])
|
|
43
45
|
@line_ranges = merged[:line_ranges] || {}
|
|
44
46
|
@spec_files = Array(merged[:spec_files])
|
|
@@ -102,6 +104,9 @@ module Evilution
|
|
|
102
104
|
# Test integration: rspec (default: rspec)
|
|
103
105
|
# integration: rspec
|
|
104
106
|
|
|
107
|
+
# Number of parallel workers (default: 1)
|
|
108
|
+
# jobs: 1
|
|
109
|
+
|
|
105
110
|
# Stop after N surviving mutants (default: disabled)
|
|
106
111
|
# fail_fast: 1
|
|
107
112
|
|
|
@@ -123,12 +128,18 @@ module Evilution
|
|
|
123
128
|
raise ConfigError, "fail_fast must be a positive integer, got #{value.inspect}"
|
|
124
129
|
end
|
|
125
130
|
|
|
126
|
-
def
|
|
127
|
-
if
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
def validate_jobs(value)
|
|
132
|
+
raise ConfigError, "jobs must be a positive integer, got #{value.inspect}" if value.is_a?(Float)
|
|
133
|
+
|
|
134
|
+
value = Integer(value)
|
|
135
|
+
raise ConfigError, "jobs must be a positive integer, got #{value}" unless value >= 1
|
|
136
|
+
|
|
137
|
+
value
|
|
138
|
+
rescue ::ArgumentError, ::TypeError
|
|
139
|
+
raise ConfigError, "jobs must be a positive integer, got #{value.inspect}"
|
|
140
|
+
end
|
|
131
141
|
|
|
142
|
+
def warn_removed_options(_merged, file_options)
|
|
132
143
|
if file_options.key?(:coverage)
|
|
133
144
|
warn("Warning: 'coverage' in config file is deprecated and ignored. " \
|
|
134
145
|
"This option will be removed in a future version.")
|
|
@@ -4,6 +4,7 @@ require "fileutils"
|
|
|
4
4
|
require "stringio"
|
|
5
5
|
require "tmpdir"
|
|
6
6
|
require_relative "base"
|
|
7
|
+
require_relative "../spec_resolver"
|
|
7
8
|
|
|
8
9
|
module Evilution
|
|
9
10
|
module Integration
|
|
@@ -99,6 +100,7 @@ module Evilution
|
|
|
99
100
|
|
|
100
101
|
out = StringIO.new
|
|
101
102
|
err = StringIO.new
|
|
103
|
+
command = "rspec"
|
|
102
104
|
args = build_args(mutation)
|
|
103
105
|
command = "rspec #{args.join(" ")}"
|
|
104
106
|
|
|
@@ -109,10 +111,17 @@ module Evilution
|
|
|
109
111
|
{ passed: false, error: e.message, test_command: command }
|
|
110
112
|
end
|
|
111
113
|
|
|
112
|
-
def build_args(
|
|
113
|
-
files =
|
|
114
|
+
def build_args(mutation)
|
|
115
|
+
files = resolve_test_files(mutation)
|
|
114
116
|
["--format", "progress", "--no-color", "--order", "defined", *files]
|
|
115
117
|
end
|
|
118
|
+
|
|
119
|
+
def resolve_test_files(mutation)
|
|
120
|
+
return test_files if test_files
|
|
121
|
+
|
|
122
|
+
resolved = SpecResolver.new.call(mutation.file_path)
|
|
123
|
+
resolved ? [resolved] : ["spec"]
|
|
124
|
+
end
|
|
116
125
|
end
|
|
117
126
|
end
|
|
118
127
|
end
|
|
@@ -16,6 +16,7 @@ module Evilution
|
|
|
16
16
|
pid = ::Process.fork do
|
|
17
17
|
ENV["TMPDIR"] = sandbox_dir
|
|
18
18
|
read_io.close
|
|
19
|
+
suppress_child_output
|
|
19
20
|
result = execute_in_child(mutation, test_command)
|
|
20
21
|
Marshal.dump(result, write_io)
|
|
21
22
|
write_io.close
|
|
@@ -44,6 +45,11 @@ module Evilution
|
|
|
44
45
|
warn("Warning: failed to restore #{mutation.file_path}: #{e.message}")
|
|
45
46
|
end
|
|
46
47
|
|
|
48
|
+
def suppress_child_output
|
|
49
|
+
$stdout.reopen(File::NULL, "w")
|
|
50
|
+
$stderr.reopen(File::NULL, "w")
|
|
51
|
+
end
|
|
52
|
+
|
|
47
53
|
def execute_in_child(mutation, test_command)
|
|
48
54
|
test_command.call(mutation)
|
|
49
55
|
rescue StandardError => e
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Evilution
|
|
4
|
+
module Parallel
|
|
5
|
+
class Pool
|
|
6
|
+
def initialize(size:)
|
|
7
|
+
raise ArgumentError, "pool size must be a positive integer, got #{size.inspect}" unless size.is_a?(Integer) && size >= 1
|
|
8
|
+
|
|
9
|
+
@size = size
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def map(items, &block)
|
|
13
|
+
results = []
|
|
14
|
+
|
|
15
|
+
items.each_slice(@size) do |batch|
|
|
16
|
+
results.concat(run_batch(batch, &block))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
results
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def run_batch(items, &block)
|
|
25
|
+
entries = items.map do |item|
|
|
26
|
+
read_io, write_io = IO.pipe
|
|
27
|
+
pid = fork_worker(item, read_io, write_io, &block)
|
|
28
|
+
write_io.close
|
|
29
|
+
{ pid: pid, read_io: read_io }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
collect_results(entries)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def fork_worker(item, read_io, write_io, &block)
|
|
36
|
+
Process.fork do
|
|
37
|
+
read_io.close
|
|
38
|
+
result = block.call(item)
|
|
39
|
+
Marshal.dump(result, write_io)
|
|
40
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
41
|
+
Marshal.dump(e, write_io)
|
|
42
|
+
ensure
|
|
43
|
+
write_io.close
|
|
44
|
+
exit!
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def collect_results(entries)
|
|
49
|
+
entries.map do |entry|
|
|
50
|
+
data = entry[:read_io].read
|
|
51
|
+
entry[:read_io].close
|
|
52
|
+
Process.wait(entry[:pid])
|
|
53
|
+
raise Evilution::Error, "worker process failed with no result" if data.empty?
|
|
54
|
+
|
|
55
|
+
result = Marshal.load(data) # rubocop:disable Security/MarshalLoad
|
|
56
|
+
raise result if result.is_a?(Exception)
|
|
57
|
+
|
|
58
|
+
result
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/evilution/runner.rb
CHANGED
|
@@ -13,6 +13,7 @@ require_relative "diff/file_filter"
|
|
|
13
13
|
require_relative "git/changed_files"
|
|
14
14
|
require_relative "result/mutation_result"
|
|
15
15
|
require_relative "result/summary"
|
|
16
|
+
require_relative "parallel/pool"
|
|
16
17
|
|
|
17
18
|
module Evilution
|
|
18
19
|
class Runner
|
|
@@ -86,6 +87,14 @@ module Evilution
|
|
|
86
87
|
end
|
|
87
88
|
|
|
88
89
|
def run_mutations(mutations)
|
|
90
|
+
if config.jobs > 1
|
|
91
|
+
run_mutations_parallel(mutations)
|
|
92
|
+
else
|
|
93
|
+
run_mutations_sequential(mutations)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def run_mutations_sequential(mutations)
|
|
89
98
|
integration = build_integration
|
|
90
99
|
results = []
|
|
91
100
|
survived_count = 0
|
|
@@ -100,6 +109,7 @@ module Evilution
|
|
|
100
109
|
)
|
|
101
110
|
results << result
|
|
102
111
|
survived_count += 1 if result.survived?
|
|
112
|
+
log_progress(index + 1, mutations.length, result.status)
|
|
103
113
|
|
|
104
114
|
if config.fail_fast? && survived_count >= config.fail_fast && index < mutations.length - 1
|
|
105
115
|
truncated = true
|
|
@@ -110,6 +120,39 @@ module Evilution
|
|
|
110
120
|
[results, truncated]
|
|
111
121
|
end
|
|
112
122
|
|
|
123
|
+
def run_mutations_parallel(mutations)
|
|
124
|
+
integration = build_integration
|
|
125
|
+
pool = Parallel::Pool.new(size: config.jobs)
|
|
126
|
+
results = []
|
|
127
|
+
survived_count = 0
|
|
128
|
+
truncated = false
|
|
129
|
+
completed = 0
|
|
130
|
+
|
|
131
|
+
mutations.each_slice(config.jobs) do |batch|
|
|
132
|
+
break if truncated
|
|
133
|
+
|
|
134
|
+
batch_results = pool.map(batch) do |mutation|
|
|
135
|
+
test_command = ->(m) { integration.call(m) }
|
|
136
|
+
isolator.call(mutation: mutation, test_command: test_command, timeout: config.timeout)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
batch_results.each do |result|
|
|
140
|
+
results << result
|
|
141
|
+
survived_count += 1 if result.survived?
|
|
142
|
+
completed += 1
|
|
143
|
+
log_progress(completed, mutations.length, result.status)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
truncated = true if should_truncate?(survived_count, completed, mutations.length)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
[results, truncated]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def should_truncate?(survived_count, completed, total)
|
|
153
|
+
config.fail_fast? && survived_count >= config.fail_fast && completed < total
|
|
154
|
+
end
|
|
155
|
+
|
|
113
156
|
def build_integration
|
|
114
157
|
case config.integration
|
|
115
158
|
when :rspec
|
|
@@ -128,6 +171,12 @@ module Evilution
|
|
|
128
171
|
$stdout.puts(output) unless config.quiet
|
|
129
172
|
end
|
|
130
173
|
|
|
174
|
+
def log_progress(current, total, status)
|
|
175
|
+
return if config.quiet || !config.text? || !$stderr.tty?
|
|
176
|
+
|
|
177
|
+
$stderr.write("mutation #{current}/#{total} #{status}\n")
|
|
178
|
+
end
|
|
179
|
+
|
|
131
180
|
def build_reporter
|
|
132
181
|
case config.format
|
|
133
182
|
when :json
|
data/lib/evilution/version.rb
CHANGED
data/lib/evilution.rb
CHANGED
|
@@ -27,6 +27,7 @@ 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/pool"
|
|
30
31
|
require_relative "evilution/diff/parser"
|
|
31
32
|
require_relative "evilution/diff/file_filter"
|
|
32
33
|
require_relative "evilution/git/changed_files"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: evilution
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Denis Kiselev
|
|
@@ -91,6 +91,7 @@ files:
|
|
|
91
91
|
- lib/evilution/mutator/operator/string_literal.rb
|
|
92
92
|
- lib/evilution/mutator/operator/symbol_literal.rb
|
|
93
93
|
- lib/evilution/mutator/registry.rb
|
|
94
|
+
- lib/evilution/parallel/pool.rb
|
|
94
95
|
- lib/evilution/reporter/cli.rb
|
|
95
96
|
- lib/evilution/reporter/json.rb
|
|
96
97
|
- lib/evilution/reporter/suggestion.rb
|