evilution 0.3.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/.migration-hint-ts +1 -1
- data/.beads/issues.jsonl +15 -10
- data/CHANGELOG.md +39 -0
- data/lib/evilution/ast/parser.rb +7 -3
- data/lib/evilution/cli.rb +43 -8
- data/lib/evilution/config.rb +53 -14
- data/lib/evilution/git/changed_files.rb +54 -0
- data/lib/evilution/integration/rspec.rb +14 -4
- data/lib/evilution/isolation/fork.rb +8 -1
- data/lib/evilution/parallel/pool.rb +63 -0
- data/lib/evilution/reporter/cli.rb +1 -0
- data/lib/evilution/reporter/json.rb +4 -1
- data/lib/evilution/result/mutation_result.rb +3 -2
- data/lib/evilution/result/summary.rb +6 -1
- data/lib/evilution/runner.rb +74 -5
- data/lib/evilution/spec_resolver.rb +40 -0
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +12 -1
- metadata +5 -2
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/.migration-hint-ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
1773589314
|
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":"
|
|
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":"
|
|
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":"
|
|
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
|
+
{"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":"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,6 +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":"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
|
+
{"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":"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"}
|
|
84
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"}]}
|
|
85
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"}]}
|
|
86
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/ast/parser.rb
CHANGED
|
@@ -6,12 +6,16 @@ module Evilution
|
|
|
6
6
|
module AST
|
|
7
7
|
class Parser
|
|
8
8
|
def call(file_path)
|
|
9
|
-
raise ParseError
|
|
9
|
+
raise ParseError.new("file not found: #{file_path}", file: file_path) unless File.exist?(file_path)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
begin
|
|
12
|
+
source = File.read(file_path)
|
|
13
|
+
rescue SystemCallError => e
|
|
14
|
+
raise ParseError.new("cannot read #{file_path}: #{e.message}", file: file_path)
|
|
15
|
+
end
|
|
12
16
|
result = Prism.parse(source)
|
|
13
17
|
|
|
14
|
-
raise ParseError
|
|
18
|
+
raise ParseError.new("failed to parse #{file_path}: #{result.errors.map(&:message).join(", ")}", file: file_path) if result.failure?
|
|
15
19
|
|
|
16
20
|
extract_subjects(result.value, source, file_path)
|
|
17
21
|
end
|
data/lib/evilution/cli.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
3
4
|
require "optparse"
|
|
4
5
|
require_relative "version"
|
|
5
6
|
require_relative "config"
|
|
@@ -12,7 +13,7 @@ module Evilution
|
|
|
12
13
|
@command = :run
|
|
13
14
|
argv = argv.dup
|
|
14
15
|
argv = extract_command(argv)
|
|
15
|
-
argv =
|
|
16
|
+
argv = preprocess_flags(argv)
|
|
16
17
|
raw_args = build_option_parser.parse!(argv)
|
|
17
18
|
@files, @line_ranges = parse_file_args(raw_args)
|
|
18
19
|
end
|
|
@@ -45,17 +46,23 @@ module Evilution
|
|
|
45
46
|
argv
|
|
46
47
|
end
|
|
47
48
|
|
|
48
|
-
def
|
|
49
|
+
def preprocess_flags(argv)
|
|
49
50
|
result = []
|
|
50
51
|
i = 0
|
|
51
52
|
while i < argv.length
|
|
52
53
|
arg = argv[i]
|
|
53
|
-
if
|
|
54
|
-
warn("Warning: --jobs is no longer supported and will be ignored.")
|
|
54
|
+
if arg == "--fail-fast"
|
|
55
55
|
next_arg = argv[i + 1]
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
|
|
57
|
+
if next_arg && next_arg.match?(/\A-?\d+\z/)
|
|
58
|
+
@options[:fail_fast] = next_arg
|
|
59
|
+
i += 2
|
|
60
|
+
else
|
|
61
|
+
result << arg
|
|
62
|
+
i += 1
|
|
63
|
+
end
|
|
64
|
+
elsif arg.start_with?("--fail-fast=")
|
|
65
|
+
@options[:fail_fast] = arg.delete_prefix("--fail-fast=")
|
|
59
66
|
i += 1
|
|
60
67
|
else
|
|
61
68
|
result << arg
|
|
@@ -68,6 +75,7 @@ module Evilution
|
|
|
68
75
|
def build_option_parser
|
|
69
76
|
OptionParser.new do |opts|
|
|
70
77
|
opts.banner = "Usage: evilution [command] [options] [files...]"
|
|
78
|
+
opts.version = VERSION
|
|
71
79
|
add_separators(opts)
|
|
72
80
|
add_options(opts)
|
|
73
81
|
end
|
|
@@ -83,6 +91,7 @@ module Evilution
|
|
|
83
91
|
end
|
|
84
92
|
|
|
85
93
|
def add_options(opts)
|
|
94
|
+
opts.on("-j", "--jobs N", Integer, "Number of parallel workers (default: 1)") { |n| @options[:jobs] = n }
|
|
86
95
|
opts.on("-t", "--timeout N", Integer, "Per-mutation timeout in seconds") { |n| @options[:timeout] = n }
|
|
87
96
|
opts.on("-f", "--format FORMAT", "Output format: text, json") { |f| @options[:format] = f.to_sym }
|
|
88
97
|
opts.on("--diff BASE", "DEPRECATED: Use line-range targeting instead") do |b|
|
|
@@ -97,6 +106,8 @@ module Evilution
|
|
|
97
106
|
warn("Warning: --no-coverage is deprecated, currently has no effect, and will be removed in a future version.")
|
|
98
107
|
@options[:coverage] = false
|
|
99
108
|
end
|
|
109
|
+
opts.on("--fail-fast", "Stop after N surviving mutants " \
|
|
110
|
+
"(default: disabled; if provided without N, uses 1; use --fail-fast=N)") { @options[:fail_fast] ||= 1 }
|
|
100
111
|
opts.on("-v", "--verbose", "Verbose output") { @options[:verbose] = true }
|
|
101
112
|
opts.on("-q", "--quiet", "Suppress output") { @options[:quiet] = true }
|
|
102
113
|
end
|
|
@@ -141,13 +152,37 @@ module Evilution
|
|
|
141
152
|
end
|
|
142
153
|
|
|
143
154
|
def run_mutations
|
|
155
|
+
file_options = Config.file_options
|
|
144
156
|
config = Config.new(**@options, target_files: @files, line_ranges: @line_ranges)
|
|
145
157
|
runner = Runner.new(config: config)
|
|
146
158
|
summary = runner.call
|
|
147
159
|
summary.success?(min_score: config.min_score) ? 0 : 1
|
|
148
160
|
rescue Error => e
|
|
149
|
-
|
|
161
|
+
if json_format?(config, file_options)
|
|
162
|
+
$stdout.puts(JSON.generate(error_payload(e)))
|
|
163
|
+
else
|
|
164
|
+
warn("Error: #{e.message}")
|
|
165
|
+
end
|
|
150
166
|
2
|
|
151
167
|
end
|
|
168
|
+
|
|
169
|
+
def json_format?(config, file_options)
|
|
170
|
+
return config.json? if config
|
|
171
|
+
|
|
172
|
+
format = @options[:format] || (file_options && file_options[:format])
|
|
173
|
+
format && format.to_sym == :json
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def error_payload(error)
|
|
177
|
+
error_type = case error
|
|
178
|
+
when ConfigError then "config_error"
|
|
179
|
+
when ParseError then "parse_error"
|
|
180
|
+
else "runtime_error"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
payload = { type: error_type, message: error.message }
|
|
184
|
+
payload[:file] = error.file if error.file
|
|
185
|
+
{ error: payload }
|
|
186
|
+
end
|
|
152
187
|
end
|
|
153
188
|
end
|
data/lib/evilution/config.rb
CHANGED
|
@@ -16,13 +16,15 @@ module Evilution
|
|
|
16
16
|
coverage: true,
|
|
17
17
|
verbose: false,
|
|
18
18
|
quiet: false,
|
|
19
|
+
jobs: 1,
|
|
20
|
+
fail_fast: nil,
|
|
19
21
|
line_ranges: {},
|
|
20
22
|
spec_files: []
|
|
21
23
|
}.freeze
|
|
22
24
|
|
|
23
25
|
attr_reader :target_files, :timeout, :format, :diff_base,
|
|
24
26
|
:target, :min_score, :integration, :coverage, :verbose, :quiet,
|
|
25
|
-
:line_ranges, :spec_files
|
|
27
|
+
:jobs, :fail_fast, :line_ranges, :spec_files
|
|
26
28
|
|
|
27
29
|
def initialize(**options)
|
|
28
30
|
file_options = options.delete(:skip_config_file) ? {} : load_config_file
|
|
@@ -38,6 +40,8 @@ module Evilution
|
|
|
38
40
|
@coverage = merged[:coverage]
|
|
39
41
|
@verbose = merged[:verbose]
|
|
40
42
|
@quiet = merged[:quiet]
|
|
43
|
+
@jobs = validate_jobs(merged[:jobs])
|
|
44
|
+
@fail_fast = validate_fail_fast(merged[:fail_fast])
|
|
41
45
|
@line_ranges = merged[:line_ranges] || {}
|
|
42
46
|
@spec_files = Array(merged[:spec_files])
|
|
43
47
|
freeze
|
|
@@ -63,6 +67,25 @@ module Evilution
|
|
|
63
67
|
!target.nil?
|
|
64
68
|
end
|
|
65
69
|
|
|
70
|
+
def fail_fast?
|
|
71
|
+
!fail_fast.nil?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.file_options
|
|
75
|
+
CONFIG_FILES.each do |path|
|
|
76
|
+
next unless File.exist?(path)
|
|
77
|
+
|
|
78
|
+
data = YAML.safe_load_file(path, symbolize_names: true)
|
|
79
|
+
return data.is_a?(Hash) ? data : {}
|
|
80
|
+
rescue Psych::SyntaxError, Psych::DisallowedClass => e
|
|
81
|
+
raise ConfigError.new("failed to parse config file #{path}: #{e.message}", file: path)
|
|
82
|
+
rescue SystemCallError => e
|
|
83
|
+
raise ConfigError.new("cannot read config file #{path}: #{e.message}", file: path)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
{}
|
|
87
|
+
end
|
|
88
|
+
|
|
66
89
|
# Generates a default config file template.
|
|
67
90
|
def self.default_template
|
|
68
91
|
<<~YAML
|
|
@@ -81,6 +104,12 @@ module Evilution
|
|
|
81
104
|
# Test integration: rspec (default: rspec)
|
|
82
105
|
# integration: rspec
|
|
83
106
|
|
|
107
|
+
# Number of parallel workers (default: 1)
|
|
108
|
+
# jobs: 1
|
|
109
|
+
|
|
110
|
+
# Stop after N surviving mutants (default: disabled)
|
|
111
|
+
# fail_fast: 1
|
|
112
|
+
|
|
84
113
|
# DEPRECATED: Coverage filtering is deprecated and will be removed
|
|
85
114
|
# coverage: true
|
|
86
115
|
YAML
|
|
@@ -88,12 +117,29 @@ module Evilution
|
|
|
88
117
|
|
|
89
118
|
private
|
|
90
119
|
|
|
91
|
-
def
|
|
92
|
-
if
|
|
93
|
-
warn("Warning: 'jobs' option is no longer supported and will be ignored. " \
|
|
94
|
-
"Remove it from your configuration or invocation.")
|
|
95
|
-
end
|
|
120
|
+
def validate_fail_fast(value)
|
|
121
|
+
return nil if value.nil?
|
|
96
122
|
|
|
123
|
+
value = Integer(value)
|
|
124
|
+
raise ConfigError, "fail_fast must be a positive integer, got #{value}" unless value >= 1
|
|
125
|
+
|
|
126
|
+
value
|
|
127
|
+
rescue ::ArgumentError, ::TypeError
|
|
128
|
+
raise ConfigError, "fail_fast must be a positive integer, got #{value.inspect}"
|
|
129
|
+
end
|
|
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
|
|
141
|
+
|
|
142
|
+
def warn_removed_options(_merged, file_options)
|
|
97
143
|
if file_options.key?(:coverage)
|
|
98
144
|
warn("Warning: 'coverage' in config file is deprecated and ignored. " \
|
|
99
145
|
"This option will be removed in a future version.")
|
|
@@ -106,14 +152,7 @@ module Evilution
|
|
|
106
152
|
end
|
|
107
153
|
|
|
108
154
|
def load_config_file
|
|
109
|
-
|
|
110
|
-
next unless File.exist?(path)
|
|
111
|
-
|
|
112
|
-
data = YAML.safe_load_file(path, symbolize_names: true)
|
|
113
|
-
return data.is_a?(Hash) ? data : {}
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
{}
|
|
155
|
+
self.class.file_options
|
|
117
156
|
end
|
|
118
157
|
end
|
|
119
158
|
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "English"
|
|
4
|
+
|
|
5
|
+
module Evilution
|
|
6
|
+
module Git
|
|
7
|
+
class ChangedFiles
|
|
8
|
+
MAIN_BRANCHES = %w[main master origin/main origin/master].freeze
|
|
9
|
+
SOURCE_PREFIXES = %w[lib/ app/].freeze
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
main_branch = detect_main_branch
|
|
13
|
+
merge_base = run_git("merge-base", "HEAD", main_branch)
|
|
14
|
+
diff_output = run_git("diff", "--name-only", "--diff-filter=ACMR", "#{merge_base}..HEAD")
|
|
15
|
+
|
|
16
|
+
files = diff_output.split("\n").select { |f| ruby_source_file?(f) }
|
|
17
|
+
raise Error, "no changed Ruby files found since merge base with #{main_branch}" if files.empty?
|
|
18
|
+
|
|
19
|
+
files
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def detect_main_branch
|
|
25
|
+
MAIN_BRANCHES.each do |branch|
|
|
26
|
+
return branch if branch_exists?(branch)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
raise Error, "could not detect main branch (tried #{MAIN_BRANCHES.join(", ")})"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def branch_exists?(name)
|
|
33
|
+
run_git("rev-parse", "--verify", name)
|
|
34
|
+
true
|
|
35
|
+
rescue Error => e
|
|
36
|
+
raise if e.message.include?("not a git repository")
|
|
37
|
+
|
|
38
|
+
false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def ruby_source_file?(path)
|
|
42
|
+
path.end_with?(".rb") && SOURCE_PREFIXES.any? { |prefix| path.start_with?(prefix) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def run_git(*args)
|
|
46
|
+
output = `git #{args.join(" ")} 2>&1`.strip
|
|
47
|
+
raise Error, "not a git repository" if output.include?("not a git repository")
|
|
48
|
+
raise Error, "git command failed: git #{args.join(" ")}: #{output}" unless $CHILD_STATUS.success?
|
|
49
|
+
|
|
50
|
+
output
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -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,19 +100,28 @@ module Evilution
|
|
|
99
100
|
|
|
100
101
|
out = StringIO.new
|
|
101
102
|
err = StringIO.new
|
|
103
|
+
command = "rspec"
|
|
102
104
|
args = build_args(mutation)
|
|
105
|
+
command = "rspec #{args.join(" ")}"
|
|
103
106
|
|
|
104
107
|
status = ::RSpec::Core::Runner.run(args, out, err)
|
|
105
108
|
|
|
106
|
-
{ passed: status.zero
|
|
109
|
+
{ passed: status.zero?, test_command: command }
|
|
107
110
|
rescue StandardError => e
|
|
108
|
-
{ passed: false, error: e.message }
|
|
111
|
+
{ passed: false, error: e.message, test_command: command }
|
|
109
112
|
end
|
|
110
113
|
|
|
111
|
-
def build_args(
|
|
112
|
-
files =
|
|
114
|
+
def build_args(mutation)
|
|
115
|
+
files = resolve_test_files(mutation)
|
|
113
116
|
["--format", "progress", "--no-color", "--order", "defined", *files]
|
|
114
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
|
|
115
125
|
end
|
|
116
126
|
end
|
|
117
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
|
|
@@ -91,7 +97,8 @@ module Evilution
|
|
|
91
97
|
Result::MutationResult.new(
|
|
92
98
|
mutation: mutation,
|
|
93
99
|
status: status,
|
|
94
|
-
duration: duration
|
|
100
|
+
duration: duration,
|
|
101
|
+
test_command: result[:test_command]
|
|
95
102
|
)
|
|
96
103
|
end
|
|
97
104
|
end
|
|
@@ -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
|
|
@@ -30,7 +30,7 @@ module Evilution
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def build_summary(summary)
|
|
33
|
-
{
|
|
33
|
+
data = {
|
|
34
34
|
total: summary.total,
|
|
35
35
|
killed: summary.killed,
|
|
36
36
|
survived: summary.survived,
|
|
@@ -39,6 +39,8 @@ module Evilution
|
|
|
39
39
|
score: summary.score.round(4),
|
|
40
40
|
duration: summary.duration.round(4)
|
|
41
41
|
}
|
|
42
|
+
data[:truncated] = true if summary.truncated?
|
|
43
|
+
data
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
def build_mutation_detail(result)
|
|
@@ -52,6 +54,7 @@ module Evilution
|
|
|
52
54
|
diff: mutation.diff
|
|
53
55
|
}
|
|
54
56
|
detail[:suggestion] = @suggestion.suggestion_for(mutation) if result.status == :survived
|
|
57
|
+
detail[:test_command] = result.test_command if result.test_command
|
|
55
58
|
detail
|
|
56
59
|
end
|
|
57
60
|
end
|
|
@@ -5,15 +5,16 @@ module Evilution
|
|
|
5
5
|
class MutationResult
|
|
6
6
|
STATUSES = %i[killed survived timeout error].freeze
|
|
7
7
|
|
|
8
|
-
attr_reader :mutation, :status, :duration, :killing_test
|
|
8
|
+
attr_reader :mutation, :status, :duration, :killing_test, :test_command
|
|
9
9
|
|
|
10
|
-
def initialize(mutation:, status:, duration: 0.0, killing_test: nil)
|
|
10
|
+
def initialize(mutation:, status:, duration: 0.0, killing_test: nil, test_command: nil)
|
|
11
11
|
raise ArgumentError, "invalid status: #{status}" unless STATUSES.include?(status)
|
|
12
12
|
|
|
13
13
|
@mutation = mutation
|
|
14
14
|
@status = status
|
|
15
15
|
@duration = duration
|
|
16
16
|
@killing_test = killing_test
|
|
17
|
+
@test_command = test_command
|
|
17
18
|
freeze
|
|
18
19
|
end
|
|
19
20
|
|
|
@@ -5,12 +5,17 @@ module Evilution
|
|
|
5
5
|
class Summary
|
|
6
6
|
attr_reader :results, :duration
|
|
7
7
|
|
|
8
|
-
def initialize(results:, duration: 0.0)
|
|
8
|
+
def initialize(results:, duration: 0.0, truncated: false)
|
|
9
9
|
@results = results
|
|
10
10
|
@duration = duration
|
|
11
|
+
@truncated = truncated
|
|
11
12
|
freeze
|
|
12
13
|
end
|
|
13
14
|
|
|
15
|
+
def truncated?
|
|
16
|
+
@truncated
|
|
17
|
+
end
|
|
18
|
+
|
|
14
19
|
def total
|
|
15
20
|
results.length
|
|
16
21
|
end
|
data/lib/evilution/runner.rb
CHANGED
|
@@ -10,8 +10,10 @@ require_relative "reporter/cli"
|
|
|
10
10
|
require_relative "reporter/suggestion"
|
|
11
11
|
require_relative "diff/parser"
|
|
12
12
|
require_relative "diff/file_filter"
|
|
13
|
+
require_relative "git/changed_files"
|
|
13
14
|
require_relative "result/mutation_result"
|
|
14
15
|
require_relative "result/summary"
|
|
16
|
+
require_relative "parallel/pool"
|
|
15
17
|
|
|
16
18
|
module Evilution
|
|
17
19
|
class Runner
|
|
@@ -32,10 +34,10 @@ module Evilution
|
|
|
32
34
|
subjects = filter_by_line_ranges(subjects) if config.line_ranges?
|
|
33
35
|
subjects = filter_by_diff(subjects) if config.diff?
|
|
34
36
|
mutations = generate_mutations(subjects)
|
|
35
|
-
results = run_mutations(mutations)
|
|
37
|
+
results, truncated = run_mutations(mutations)
|
|
36
38
|
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
37
39
|
|
|
38
|
-
summary = Result::Summary.new(results: results, duration: duration)
|
|
40
|
+
summary = Result::Summary.new(results: results, duration: duration, truncated: truncated)
|
|
39
41
|
output_report(summary)
|
|
40
42
|
|
|
41
43
|
summary
|
|
@@ -46,7 +48,14 @@ module Evilution
|
|
|
46
48
|
attr_reader :parser, :registry, :isolator
|
|
47
49
|
|
|
48
50
|
def parse_subjects
|
|
49
|
-
|
|
51
|
+
files = resolve_target_files
|
|
52
|
+
files.flat_map { |file| parser.call(file) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def resolve_target_files
|
|
56
|
+
return config.target_files unless config.target_files.empty?
|
|
57
|
+
|
|
58
|
+
Git::ChangedFiles.new.call
|
|
50
59
|
end
|
|
51
60
|
|
|
52
61
|
def filter_by_target(subjects)
|
|
@@ -78,16 +87,70 @@ module Evilution
|
|
|
78
87
|
end
|
|
79
88
|
|
|
80
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)
|
|
81
98
|
integration = build_integration
|
|
99
|
+
results = []
|
|
100
|
+
survived_count = 0
|
|
101
|
+
truncated = false
|
|
82
102
|
|
|
83
|
-
mutations.
|
|
103
|
+
mutations.each_with_index do |mutation, index|
|
|
84
104
|
test_command = ->(m) { integration.call(m) }
|
|
85
|
-
isolator.call(
|
|
105
|
+
result = isolator.call(
|
|
86
106
|
mutation: mutation,
|
|
87
107
|
test_command: test_command,
|
|
88
108
|
timeout: config.timeout
|
|
89
109
|
)
|
|
110
|
+
results << result
|
|
111
|
+
survived_count += 1 if result.survived?
|
|
112
|
+
log_progress(index + 1, mutations.length, result.status)
|
|
113
|
+
|
|
114
|
+
if config.fail_fast? && survived_count >= config.fail_fast && index < mutations.length - 1
|
|
115
|
+
truncated = true
|
|
116
|
+
break
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
[results, truncated]
|
|
121
|
+
end
|
|
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)
|
|
90
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
|
|
91
154
|
end
|
|
92
155
|
|
|
93
156
|
def build_integration
|
|
@@ -108,6 +171,12 @@ module Evilution
|
|
|
108
171
|
$stdout.puts(output) unless config.quiet
|
|
109
172
|
end
|
|
110
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
|
+
|
|
111
180
|
def build_reporter
|
|
112
181
|
case config.format
|
|
113
182
|
when :json
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Evilution
|
|
4
|
+
class SpecResolver
|
|
5
|
+
STRIPPABLE_PREFIXES = %w[lib/ app/].freeze
|
|
6
|
+
|
|
7
|
+
def call(source_path)
|
|
8
|
+
return nil if source_path.nil? || source_path.empty?
|
|
9
|
+
|
|
10
|
+
normalized = normalize_path(source_path)
|
|
11
|
+
candidates = candidate_spec_paths(normalized)
|
|
12
|
+
candidates.find { |path| File.exist?(path) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def resolve_all(source_paths)
|
|
16
|
+
Array(source_paths).filter_map { |path| call(path) }.uniq
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def normalize_path(path)
|
|
22
|
+
path = path.delete_prefix("./")
|
|
23
|
+
path = path.delete_prefix("#{Dir.pwd}/") if path.start_with?("/")
|
|
24
|
+
path
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def candidate_spec_paths(source_path)
|
|
28
|
+
base = source_path.sub(/\.rb\z/, "_spec.rb")
|
|
29
|
+
prefix = STRIPPABLE_PREFIXES.find { |p| source_path.start_with?(p) }
|
|
30
|
+
|
|
31
|
+
if prefix
|
|
32
|
+
stripped = "spec/#{base.delete_prefix(prefix)}"
|
|
33
|
+
kept = "spec/#{base}"
|
|
34
|
+
[stripped, kept]
|
|
35
|
+
else
|
|
36
|
+
["spec/#{base}"]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/evilution/version.rb
CHANGED
data/lib/evilution.rb
CHANGED
|
@@ -27,8 +27,10 @@ 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"
|
|
33
|
+
require_relative "evilution/git/changed_files"
|
|
32
34
|
require_relative "evilution/integration/base"
|
|
33
35
|
require_relative "evilution/integration/rspec"
|
|
34
36
|
require_relative "evilution/result/mutation_result"
|
|
@@ -38,11 +40,20 @@ require_relative "evilution/reporter/cli"
|
|
|
38
40
|
require_relative "evilution/reporter/suggestion"
|
|
39
41
|
require_relative "evilution/coverage/collector"
|
|
40
42
|
require_relative "evilution/coverage/test_map"
|
|
43
|
+
require_relative "evilution/spec_resolver"
|
|
41
44
|
require_relative "evilution/cli"
|
|
42
45
|
require_relative "evilution/runner"
|
|
43
46
|
|
|
44
47
|
module Evilution
|
|
45
|
-
class Error < StandardError
|
|
48
|
+
class Error < StandardError
|
|
49
|
+
attr_reader :file
|
|
50
|
+
|
|
51
|
+
def initialize(message = nil, file: nil)
|
|
52
|
+
super(message)
|
|
53
|
+
@file = file
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
46
57
|
class ConfigError < Error; end
|
|
47
58
|
class ParseError < Error; end
|
|
48
59
|
class IsolationError < Error; end
|
metadata
CHANGED
|
@@ -1,14 +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.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Denis Kiselev
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: diff-lcs
|
|
@@ -66,6 +66,7 @@ files:
|
|
|
66
66
|
- lib/evilution/coverage/test_map.rb
|
|
67
67
|
- lib/evilution/diff/file_filter.rb
|
|
68
68
|
- lib/evilution/diff/parser.rb
|
|
69
|
+
- lib/evilution/git/changed_files.rb
|
|
69
70
|
- lib/evilution/integration/base.rb
|
|
70
71
|
- lib/evilution/integration/rspec.rb
|
|
71
72
|
- lib/evilution/isolation/fork.rb
|
|
@@ -90,12 +91,14 @@ files:
|
|
|
90
91
|
- lib/evilution/mutator/operator/string_literal.rb
|
|
91
92
|
- lib/evilution/mutator/operator/symbol_literal.rb
|
|
92
93
|
- lib/evilution/mutator/registry.rb
|
|
94
|
+
- lib/evilution/parallel/pool.rb
|
|
93
95
|
- lib/evilution/reporter/cli.rb
|
|
94
96
|
- lib/evilution/reporter/json.rb
|
|
95
97
|
- lib/evilution/reporter/suggestion.rb
|
|
96
98
|
- lib/evilution/result/mutation_result.rb
|
|
97
99
|
- lib/evilution/result/summary.rb
|
|
98
100
|
- lib/evilution/runner.rb
|
|
101
|
+
- lib/evilution/spec_resolver.rb
|
|
99
102
|
- lib/evilution/subject.rb
|
|
100
103
|
- lib/evilution/version.rb
|
|
101
104
|
- sig/evilution.rbs
|