evilution 0.3.0 → 0.4.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 +9 -5
- data/lib/evilution/ast/parser.rb +7 -3
- data/lib/evilution/cli.rb +45 -4
- data/lib/evilution/config.rb +37 -9
- data/lib/evilution/git/changed_files.rb +54 -0
- data/lib/evilution/integration/rspec.rb +3 -2
- data/lib/evilution/isolation/fork.rb +2 -1
- 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 +25 -5
- data/lib/evilution/spec_resolver.rb +40 -0
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +11 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 67bff7d0366e8fe092534c14669ec3637ba1d95934a003f595519dca8f16fe68
|
|
4
|
+
data.tar.gz: b4545412acaedb471187e9522cb8858c07b9a83b748a58ffbd8aa2a1f0ec7a29
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 15c2460a72fc5822ddecc3e38c4e60424bc96a24274542ce2492c55e9a847ca02484ec103513791f91c285f3ee18b8fb3017009a146fc517822c9a1749f2cd93
|
|
7
|
+
data.tar.gz: 98f93f7b5bbe2baf2172bee631a44fa8cde99e5913e3484b91c06d780ea2f0d7f462e6a030187d3179dfbe0b0bd9d78afdeff29dd28e38e7d7b9f6efa90763c1
|
data/.beads/.migration-hint-ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
1773589314
|
data/.beads/issues.jsonl
CHANGED
|
@@ -31,10 +31,10 @@
|
|
|
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
33
|
{"id":"EV-21","title":"Epic: Speed & Performance","description":"Reduce mutation testing wall-clock time for fast agent feedback loops. Includes fail-fast, parallel execution, and per-mutation spec targeting.","status":"open","priority":1,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:17:26.608316104+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T06:17:26.608316104+07:00","dependencies":[{"issue_id":"EV-21","depends_on_id":"EV-22","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-21","depends_on_id":"EV-23","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
34
|
-
{"id":"EV-22","title":"Add --fail-fast flag with survivor threshold","description":"Add --fail-fast [N] CLI flag and fail_fast config option to stop mutation testing after N surviving mutants (default N=1 when flag given without value). Agents fix gaps iteratively, so discovering all survivors upfront is wasted work. A threshold gives control over the speed/thoroughness tradeoff: --fail-fast 1 for quick checks, --fail-fast 5 for CI gates, omit for full scans. Runner#call should stop running mutations and return a partial Summary once the threshold is reached. JSON output should include a 'truncated: true' field when fail-fast triggered.","status":"
|
|
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":"in_progress","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-13T08:24:05.534981769+07:00"}
|
|
35
35
|
{"id":"EV-23","title":"Per-mutation spec targeting","description":"Instead of running the full spec suite for every mutation, map each mutated source file to its relevant spec file(s) using convention-based resolution (e.g. lib/foo/bar.rb -> spec/foo/bar_spec.rb) and only run those. This dramatically reduces per-mutation test time. Depends on convention-based spec file resolution being implemented first.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:17:28.98620973+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T06:17:28.98620973+07:00","dependencies":[{"issue_id":"EV-23","depends_on_id":"EV-34","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
36
|
-
{"id":"EV-24","title":"Epic: JSON Output Improvements","description":"Make JSON output fully machine-parseable in all scenarios, including errors. Add diagnostic fields that help agents debug failures.","status":"open","priority":
|
|
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":"
|
|
36
|
+
{"id":"EV-24","title":"Epic: JSON Output Improvements","description":"Make JSON output fully machine-parseable in all scenarios, including errors. Add diagnostic fields that help agents debug failures.","status":"open","priority":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-13T09:57:11.531255221+07:00","dependencies":[{"issue_id":"EV-24","depends_on_id":"EV-25","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-24","depends_on_id":"EV-26","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"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
38
|
{"id":"EV-26","title":"Include test command in mutation result JSON","description":"Add a 'test_command' field to each mutation result in JSON output showing the exact command that was run to test that mutation. Helps agents debug when a mutation errors out or times out.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:17:39.227881462+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T06:17:39.227881462+07:00"}
|
|
39
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"}
|
|
@@ -64,8 +64,8 @@
|
|
|
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
66
|
{"id":"EV-32","title":"Epic: Zero-friction Defaults","description":"Make evilution work well with zero configuration. Auto-detect what to mutate and which specs to run based on git state and file conventions.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T06:18:06.843446356+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T06:18:06.843446356+07:00","dependencies":[{"issue_id":"EV-32","depends_on_id":"EV-33","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-32","depends_on_id":"EV-34","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
67
|
-
{"id":"EV-33","title":"Auto-detect changed files from git merge base","description":"When evilution is run with no file arguments, default to mutating files changed since the merge base of the current branch (git merge-base HEAD main). This makes 'evilution run --format json' just work in a feature branch without specifying files. Should be skippable if explicit files are provided.","status":"
|
|
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":"
|
|
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":"in_progress","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-15T23:20:23.413703926+07:00"}
|
|
68
|
+
{"id":"EV-34","title":"Convention-based spec file resolution","description":"Implement a mapping from source files to their spec files using Ruby/RSpec conventions: lib/foo/bar.rb -> spec/foo/bar_spec.rb, app/models/user.rb -> spec/models/user_spec.rb. This is a foundational piece used by both zero-friction defaults (auto-detect specs) and per-mutation spec targeting (run only relevant specs). Should handle common Rails and gem layouts.","status":"in_progress","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-15T22:50:31.462784114+07:00"}
|
|
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,10 @@
|
|
|
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":"open","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-13T09:56:58.604909176+07:00"}
|
|
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":"open","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-13T09:57:01.023293323+07:00"}
|
|
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":"open","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-13T09:57:05.048211823+07:00"}
|
|
84
88
|
{"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
89
|
{"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
90
|
{"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/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,7 +46,7 @@ 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
|
|
@@ -53,10 +54,24 @@ module Evilution
|
|
|
53
54
|
if %w[--jobs -j].include?(arg)
|
|
54
55
|
warn("Warning: --jobs is no longer supported and will be ignored.")
|
|
55
56
|
next_arg = argv[i + 1]
|
|
56
|
-
|
|
57
|
+
numeric_next = next_arg && next_arg.match?(/\A-?\d+\z/)
|
|
58
|
+
i += numeric_next ? 2 : 1
|
|
57
59
|
elsif arg.start_with?("--jobs=") || arg.match?(/\A-j-?\d+\z/)
|
|
58
60
|
warn("Warning: --jobs is no longer supported and will be ignored.")
|
|
59
61
|
i += 1
|
|
62
|
+
elsif arg == "--fail-fast"
|
|
63
|
+
next_arg = argv[i + 1]
|
|
64
|
+
|
|
65
|
+
if next_arg && next_arg.match?(/\A-?\d+\z/)
|
|
66
|
+
@options[:fail_fast] = next_arg
|
|
67
|
+
i += 2
|
|
68
|
+
else
|
|
69
|
+
result << arg
|
|
70
|
+
i += 1
|
|
71
|
+
end
|
|
72
|
+
elsif arg.start_with?("--fail-fast=")
|
|
73
|
+
@options[:fail_fast] = arg.delete_prefix("--fail-fast=")
|
|
74
|
+
i += 1
|
|
60
75
|
else
|
|
61
76
|
result << arg
|
|
62
77
|
i += 1
|
|
@@ -97,6 +112,8 @@ module Evilution
|
|
|
97
112
|
warn("Warning: --no-coverage is deprecated, currently has no effect, and will be removed in a future version.")
|
|
98
113
|
@options[:coverage] = false
|
|
99
114
|
end
|
|
115
|
+
opts.on("--fail-fast", "Stop after N surviving mutants " \
|
|
116
|
+
"(default: disabled; if provided without N, uses 1; use --fail-fast=N)") { @options[:fail_fast] ||= 1 }
|
|
100
117
|
opts.on("-v", "--verbose", "Verbose output") { @options[:verbose] = true }
|
|
101
118
|
opts.on("-q", "--quiet", "Suppress output") { @options[:quiet] = true }
|
|
102
119
|
end
|
|
@@ -141,13 +158,37 @@ module Evilution
|
|
|
141
158
|
end
|
|
142
159
|
|
|
143
160
|
def run_mutations
|
|
161
|
+
file_options = Config.file_options
|
|
144
162
|
config = Config.new(**@options, target_files: @files, line_ranges: @line_ranges)
|
|
145
163
|
runner = Runner.new(config: config)
|
|
146
164
|
summary = runner.call
|
|
147
165
|
summary.success?(min_score: config.min_score) ? 0 : 1
|
|
148
166
|
rescue Error => e
|
|
149
|
-
|
|
167
|
+
if json_format?(config, file_options)
|
|
168
|
+
$stdout.puts(JSON.generate(error_payload(e)))
|
|
169
|
+
else
|
|
170
|
+
warn("Error: #{e.message}")
|
|
171
|
+
end
|
|
150
172
|
2
|
|
151
173
|
end
|
|
174
|
+
|
|
175
|
+
def json_format?(config, file_options)
|
|
176
|
+
return config.json? if config
|
|
177
|
+
|
|
178
|
+
format = @options[:format] || (file_options && file_options[:format])
|
|
179
|
+
format && format.to_sym == :json
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def error_payload(error)
|
|
183
|
+
error_type = case error
|
|
184
|
+
when ConfigError then "config_error"
|
|
185
|
+
when ParseError then "parse_error"
|
|
186
|
+
else "runtime_error"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
payload = { type: error_type, message: error.message }
|
|
190
|
+
payload[:file] = error.file if error.file
|
|
191
|
+
{ error: payload }
|
|
192
|
+
end
|
|
152
193
|
end
|
|
153
194
|
end
|
data/lib/evilution/config.rb
CHANGED
|
@@ -16,13 +16,14 @@ module Evilution
|
|
|
16
16
|
coverage: true,
|
|
17
17
|
verbose: false,
|
|
18
18
|
quiet: false,
|
|
19
|
+
fail_fast: nil,
|
|
19
20
|
line_ranges: {},
|
|
20
21
|
spec_files: []
|
|
21
22
|
}.freeze
|
|
22
23
|
|
|
23
24
|
attr_reader :target_files, :timeout, :format, :diff_base,
|
|
24
25
|
:target, :min_score, :integration, :coverage, :verbose, :quiet,
|
|
25
|
-
:line_ranges, :spec_files
|
|
26
|
+
:fail_fast, :line_ranges, :spec_files
|
|
26
27
|
|
|
27
28
|
def initialize(**options)
|
|
28
29
|
file_options = options.delete(:skip_config_file) ? {} : load_config_file
|
|
@@ -38,6 +39,7 @@ module Evilution
|
|
|
38
39
|
@coverage = merged[:coverage]
|
|
39
40
|
@verbose = merged[:verbose]
|
|
40
41
|
@quiet = merged[:quiet]
|
|
42
|
+
@fail_fast = validate_fail_fast(merged[:fail_fast])
|
|
41
43
|
@line_ranges = merged[:line_ranges] || {}
|
|
42
44
|
@spec_files = Array(merged[:spec_files])
|
|
43
45
|
freeze
|
|
@@ -63,6 +65,25 @@ module Evilution
|
|
|
63
65
|
!target.nil?
|
|
64
66
|
end
|
|
65
67
|
|
|
68
|
+
def fail_fast?
|
|
69
|
+
!fail_fast.nil?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.file_options
|
|
73
|
+
CONFIG_FILES.each do |path|
|
|
74
|
+
next unless File.exist?(path)
|
|
75
|
+
|
|
76
|
+
data = YAML.safe_load_file(path, symbolize_names: true)
|
|
77
|
+
return data.is_a?(Hash) ? data : {}
|
|
78
|
+
rescue Psych::SyntaxError, Psych::DisallowedClass => e
|
|
79
|
+
raise ConfigError.new("failed to parse config file #{path}: #{e.message}", file: path)
|
|
80
|
+
rescue SystemCallError => e
|
|
81
|
+
raise ConfigError.new("cannot read config file #{path}: #{e.message}", file: path)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
{}
|
|
85
|
+
end
|
|
86
|
+
|
|
66
87
|
# Generates a default config file template.
|
|
67
88
|
def self.default_template
|
|
68
89
|
<<~YAML
|
|
@@ -81,6 +102,9 @@ module Evilution
|
|
|
81
102
|
# Test integration: rspec (default: rspec)
|
|
82
103
|
# integration: rspec
|
|
83
104
|
|
|
105
|
+
# Stop after N surviving mutants (default: disabled)
|
|
106
|
+
# fail_fast: 1
|
|
107
|
+
|
|
84
108
|
# DEPRECATED: Coverage filtering is deprecated and will be removed
|
|
85
109
|
# coverage: true
|
|
86
110
|
YAML
|
|
@@ -88,6 +112,17 @@ module Evilution
|
|
|
88
112
|
|
|
89
113
|
private
|
|
90
114
|
|
|
115
|
+
def validate_fail_fast(value)
|
|
116
|
+
return nil if value.nil?
|
|
117
|
+
|
|
118
|
+
value = Integer(value)
|
|
119
|
+
raise ConfigError, "fail_fast must be a positive integer, got #{value}" unless value >= 1
|
|
120
|
+
|
|
121
|
+
value
|
|
122
|
+
rescue ::ArgumentError, ::TypeError
|
|
123
|
+
raise ConfigError, "fail_fast must be a positive integer, got #{value.inspect}"
|
|
124
|
+
end
|
|
125
|
+
|
|
91
126
|
def warn_removed_options(merged, file_options)
|
|
92
127
|
if merged.key?(:jobs)
|
|
93
128
|
warn("Warning: 'jobs' option is no longer supported and will be ignored. " \
|
|
@@ -106,14 +141,7 @@ module Evilution
|
|
|
106
141
|
end
|
|
107
142
|
|
|
108
143
|
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
|
-
{}
|
|
144
|
+
self.class.file_options
|
|
117
145
|
end
|
|
118
146
|
end
|
|
119
147
|
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
|
|
@@ -100,12 +100,13 @@ module Evilution
|
|
|
100
100
|
out = StringIO.new
|
|
101
101
|
err = StringIO.new
|
|
102
102
|
args = build_args(mutation)
|
|
103
|
+
command = "rspec #{args.join(" ")}"
|
|
103
104
|
|
|
104
105
|
status = ::RSpec::Core::Runner.run(args, out, err)
|
|
105
106
|
|
|
106
|
-
{ passed: status.zero
|
|
107
|
+
{ passed: status.zero?, test_command: command }
|
|
107
108
|
rescue StandardError => e
|
|
108
|
-
{ passed: false, error: e.message }
|
|
109
|
+
{ passed: false, error: e.message, test_command: command }
|
|
109
110
|
end
|
|
110
111
|
|
|
111
112
|
def build_args(_mutation)
|
|
@@ -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,6 +10,7 @@ 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"
|
|
15
16
|
|
|
@@ -32,10 +33,10 @@ module Evilution
|
|
|
32
33
|
subjects = filter_by_line_ranges(subjects) if config.line_ranges?
|
|
33
34
|
subjects = filter_by_diff(subjects) if config.diff?
|
|
34
35
|
mutations = generate_mutations(subjects)
|
|
35
|
-
results = run_mutations(mutations)
|
|
36
|
+
results, truncated = run_mutations(mutations)
|
|
36
37
|
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
37
38
|
|
|
38
|
-
summary = Result::Summary.new(results: results, duration: duration)
|
|
39
|
+
summary = Result::Summary.new(results: results, duration: duration, truncated: truncated)
|
|
39
40
|
output_report(summary)
|
|
40
41
|
|
|
41
42
|
summary
|
|
@@ -46,7 +47,14 @@ module Evilution
|
|
|
46
47
|
attr_reader :parser, :registry, :isolator
|
|
47
48
|
|
|
48
49
|
def parse_subjects
|
|
49
|
-
|
|
50
|
+
files = resolve_target_files
|
|
51
|
+
files.flat_map { |file| parser.call(file) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def resolve_target_files
|
|
55
|
+
return config.target_files unless config.target_files.empty?
|
|
56
|
+
|
|
57
|
+
Git::ChangedFiles.new.call
|
|
50
58
|
end
|
|
51
59
|
|
|
52
60
|
def filter_by_target(subjects)
|
|
@@ -79,15 +87,27 @@ module Evilution
|
|
|
79
87
|
|
|
80
88
|
def run_mutations(mutations)
|
|
81
89
|
integration = build_integration
|
|
90
|
+
results = []
|
|
91
|
+
survived_count = 0
|
|
92
|
+
truncated = false
|
|
82
93
|
|
|
83
|
-
mutations.
|
|
94
|
+
mutations.each_with_index do |mutation, index|
|
|
84
95
|
test_command = ->(m) { integration.call(m) }
|
|
85
|
-
isolator.call(
|
|
96
|
+
result = isolator.call(
|
|
86
97
|
mutation: mutation,
|
|
87
98
|
test_command: test_command,
|
|
88
99
|
timeout: config.timeout
|
|
89
100
|
)
|
|
101
|
+
results << result
|
|
102
|
+
survived_count += 1 if result.survived?
|
|
103
|
+
|
|
104
|
+
if config.fail_fast? && survived_count >= config.fail_fast && index < mutations.length - 1
|
|
105
|
+
truncated = true
|
|
106
|
+
break
|
|
107
|
+
end
|
|
90
108
|
end
|
|
109
|
+
|
|
110
|
+
[results, truncated]
|
|
91
111
|
end
|
|
92
112
|
|
|
93
113
|
def build_integration
|
|
@@ -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
|
@@ -29,6 +29,7 @@ require_relative "evilution/mutator/registry"
|
|
|
29
29
|
require_relative "evilution/isolation/fork"
|
|
30
30
|
require_relative "evilution/diff/parser"
|
|
31
31
|
require_relative "evilution/diff/file_filter"
|
|
32
|
+
require_relative "evilution/git/changed_files"
|
|
32
33
|
require_relative "evilution/integration/base"
|
|
33
34
|
require_relative "evilution/integration/rspec"
|
|
34
35
|
require_relative "evilution/result/mutation_result"
|
|
@@ -38,11 +39,20 @@ require_relative "evilution/reporter/cli"
|
|
|
38
39
|
require_relative "evilution/reporter/suggestion"
|
|
39
40
|
require_relative "evilution/coverage/collector"
|
|
40
41
|
require_relative "evilution/coverage/test_map"
|
|
42
|
+
require_relative "evilution/spec_resolver"
|
|
41
43
|
require_relative "evilution/cli"
|
|
42
44
|
require_relative "evilution/runner"
|
|
43
45
|
|
|
44
46
|
module Evilution
|
|
45
|
-
class Error < StandardError
|
|
47
|
+
class Error < StandardError
|
|
48
|
+
attr_reader :file
|
|
49
|
+
|
|
50
|
+
def initialize(message = nil, file: nil)
|
|
51
|
+
super(message)
|
|
52
|
+
@file = file
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
46
56
|
class ConfigError < Error; end
|
|
47
57
|
class ParseError < Error; end
|
|
48
58
|
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.4.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
|
|
@@ -96,6 +97,7 @@ files:
|
|
|
96
97
|
- lib/evilution/result/mutation_result.rb
|
|
97
98
|
- lib/evilution/result/summary.rb
|
|
98
99
|
- lib/evilution/runner.rb
|
|
100
|
+
- lib/evilution/spec_resolver.rb
|
|
99
101
|
- lib/evilution/subject.rb
|
|
100
102
|
- lib/evilution/version.rb
|
|
101
103
|
- sig/evilution.rbs
|