evilution 0.1.0 → 0.2.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 +10 -3
- data/.claude/prompts/architect.md +0 -1
- data/CHANGELOG.md +23 -0
- data/README.md +42 -12
- data/lib/evilution/cli.rb +98 -23
- data/lib/evilution/config.rb +35 -14
- data/lib/evilution/integration/rspec.rb +2 -69
- data/lib/evilution/runner.rb +22 -41
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +0 -2
- metadata +7 -5
- data/lib/evilution/parallel/pool.rb +0 -98
- data/lib/evilution/parallel/worker.rb +0 -24
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d8dc33cb03f8b2d2d94c2ca64ade2397c1933c1b431c5b804637949451ede95d
|
|
4
|
+
data.tar.gz: 36bf7bc0172dd6a7262f5ef8c716dbe64662c1c7c2c7a05b0ef0109d3479131d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d8ae3315b4c8b118e60f2a84c1d9d7854fb9f666997db5ae5697d6d9ac12838724a6be10627cc2c30bb2c8bd5e4f7babb97086fb27616b378e07e1fcc9c0ca0e
|
|
7
|
+
data.tar.gz: 557abcaebed599383335a06f913c04565513240e984f65304d4fd8f547c38dc0bc58d8c34a7f465d3947df56ba88277516970b87e791f1811d323a6f3b8562fe
|
data/.beads/.migration-hint-ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
1773064889
|
data/.beads/issues.jsonl
CHANGED
|
@@ -5,10 +5,16 @@
|
|
|
5
5
|
{"id":"EV-1.4","title":"Delete services.md prompt","description":"Remove .claude/prompts/services.md — not applicable to a gem project.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:17.649482526+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:41:19.226636126+07:00","closed_at":"2026-03-02T10:41:19.226636126+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-1.4","depends_on_id":"EV-1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
|
|
6
6
|
{"id":"EV-1.5","title":"Update gemspec metadata","description":"Fill in summary, description, homepage, source_code_uri, changelog_uri. Add runtime dependency: diff-lcs (>= 1.5, < 3). Set allowed_push_host to rubygems.org.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:17.756448433+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:41:47.168270415+07:00","closed_at":"2026-03-02T10:41:47.168270415+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-1.5","depends_on_id":"EV-1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
|
|
7
7
|
{"id":"EV-1.6","title":"Update .rubocop.yml for gem conventions","description":"Configure rubocop for gem: enable NewCops, set reasonable line length, exclude spec fixtures.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:17.855784448+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:42:03.456572349+07:00","closed_at":"2026-03-02T10:42:03.456572349+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-1.6","depends_on_id":"EV-1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
|
|
8
|
-
{"id":"EV-10","title":"Publish v0.1.0 gem release","description":"Gemspec is ready. Tag v0.1.0, build the gem, and publish to RubyGems. Steps: verify gemspec metadata, run rake release (or manual gem build + gem push), create GitHub release with CHANGELOG notes.","status":"
|
|
8
|
+
{"id":"EV-10","title":"Publish v0.1.0 gem release","description":"Gemspec is ready. Tag v0.1.0, build the gem, and publish to RubyGems. Steps: verify gemspec metadata, run rake release (or manual gem build + gem push), create GitHub release with CHANGELOG notes.","status":"closed","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T16:21:53.571182801+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-06T11:44:35.748868691+07:00","closed_at":"2026-03-06T11:44:35.748868691+07:00","close_reason":"Published to RubyGems"}
|
|
9
9
|
{"id":"EV-11","title":"Wire coverage-based filtering into Runner","description":"Collector and TestMap exist and pass specs, but Runner never calls them. When config.coverage is true, Runner collects aggregate line-level coverage via Coverage::Collector, builds a Coverage::TestMap, and skips mutations on lines that no test exercises (marking them as survived with zero duration). Note: Ruby Coverage provides aggregate per-file line hit counts, not per-test-file tracking, so we filter out uncovered mutations rather than selecting specific test files.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-04T11:52:42.607616728+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-06T11:02:25.4848637+07:00","closed_at":"2026-03-05T14:53:24.894588088+07:00","close_reason":"Coverage-based test selection wired into Runner"}
|
|
10
10
|
{"id":"EV-12","title":"Resolve suggestion gap","description":"The workflow section instructs agents to read a suggestion field from survived[], but the JSON reporter output does not include suggestion (lib/evilution/reporter/json.rb only emits operator/file/line/status/duration/diff). Either add suggestion to the JSON output/schema or update the workflow steps to match the actual report fields. See GitHub issue #12.","status":"closed","priority":2,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-05T12:33:23.674094791+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-05T13:05:48.785616141+07:00","closed_at":"2026-03-05T13:05:48.785616141+07:00","close_reason":"Wired Suggestion into JSON reporter for survived mutations, updated README schema, added specs"}
|
|
11
11
|
{"id":"EV-13","title":"Enable true per-mutation isolation with temp file copies","description":"Replace direct file writes with temp-dir + $LOAD_PATH isolation so multiple workers can mutate the same file in parallel. Remove per-file grouping from Pool#partition.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-05T13:42:04.711580397+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-05T13:44:34.779951959+07:00","closed_at":"2026-03-05T13:44:34.779951959+07:00","close_reason":"Implemented temp file isolation via $LOAD_PATH and round-robin partition"}
|
|
12
|
+
{"id":"EV-14","title":"Add changelog_uri to gemspec metadata","description":"The published gem on RubyGems is missing the Changelog link. Add changelog_uri to spec.metadata in evilution.gemspec pointing to CHANGELOG.md on master.","status":"closed","priority":2,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-06T11:46:12.648668594+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-06T11:47:41.23691915+07:00","closed_at":"2026-03-06T11:47:41.23691915+07:00","close_reason":"Added changelog_uri to gemspec"}
|
|
13
|
+
{"id":"EV-15","title":"Add line-range targeting (file:line-line syntax)","description":"Parse line-range syntax in CLI, store in Config, filter subjects in Runner. GH #29.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-08T00:36:29.164188342+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-08T00:38:02.739833414+07:00","closed_at":"2026-03-08T00:38:02.739833414+07:00","close_reason":"Implemented line-range targeting in CLI, Config, and Runner with full test coverage"}
|
|
14
|
+
{"id":"EV-16","title":"Remove file-discovery logic from Integration::RSpec (GH #33)","description":"Integration::RSpec has detect_test_files, spec_file_candidates, and fallback_spec_dir that guess which specs to run. With precise targeting, agents can pass spec files directly. Simplify or remove this guessing logic, possibly adding a --spec flag.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-08T19:17:27.268579626+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-09T18:13:46.899195957+07:00","closed_at":"2026-03-09T18:13:46.899195957+07:00","close_reason":"Completed via PR #38 (tracked as EV-17)"}
|
|
15
|
+
{"id":"EV-17","title":"Remove file-discovery logic from Integration::RSpec","description":"Add --spec flag, simplify RSpec integration by removing detect_test_files heuristics. GH #33","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-09T00:41:29.880486333+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-09T00:43:21.229172712+07:00","closed_at":"2026-03-09T00:43:21.229172712+07:00","close_reason":"Removed ~60 lines of file-discovery logic from Integration::RSpec, added --spec CLI flag, spec_files config, and wired through Runner"}
|
|
16
|
+
{"id":"EV-18","title":"Add method-name targeting (Class#method) (GH #30)","description":"Allow specifying a fully-qualified method name: evilution run Foo::Bar#calculate. Resolve name to file and line range, then mutate only that method. Lower priority since it requires name-to-file resolution which can be ambiguous with reopened classes or metaprogramming. Update README, --help, and CHANGELOG.","status":"in_progress","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-09T23:49:31.827723147+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T00:05:22.381962628+07:00"}
|
|
17
|
+
{"id":"EV-19","title":"Add method-name targeting (--target flag)","description":"Users can target a specific method for mutation testing via --target METHOD flag. Matches against Subject.name.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T00:09:57.091870734+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T00:10:02.637447023+07:00","closed_at":"2026-03-10T00:10:02.637447023+07:00","close_reason":"Implemented --target flag in CLI, Config.target? predicate, Runner.filter_by_target, README docs, and specs"}
|
|
12
18
|
{"id":"EV-2","title":"Phase 1: Foundation — End-to-End Single Mutation","description":"Build the core pipeline: parse Ruby with Prism, generate mutations, fork-based isolation, RSpec integration, JSON reporting. Milestone: Runner.new(files: ['lib/user.rb']).call produces JSON output.","status":"closed","priority":2,"issue_type":"epic","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:04:58.737191467+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:02:00.342745637+07:00","closed_at":"2026-03-02T11:02:00.342745637+07:00","close_reason":"Phase 1 Foundation complete: Config, AST::Parser, Subject, SourceSurgeon, Mutation, Mutator::Base+Registry, ComparisonReplacement, Isolation::Fork, Integration::RSpec, Result objects, Reporter::JSON, Runner — all 13 tasks done"}
|
|
13
19
|
{"id":"EV-2.1","title":"Implement Evilution::Config","description":"Immutable configuration value object. Fields: target_files, jobs (default: Etc.nprocessors), timeout (default: 10s), format (:json/:text), diff_base (nil), min_score (0.0), integration (:rspec), config_file path. Merge from defaults + YAML + CLI flags. File: lib/evilution/config.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:50.275297792+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:43:14.688620834+07:00","closed_at":"2026-03-02T10:43:14.688620834+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-2.1","depends_on_id":"EV-2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
|
|
14
20
|
{"id":"EV-2.10","title":"Implement Evilution::Integration::Base and RSpec adapter","description":"Base: abstract adapter with interface #call(test_files) -> {passed: bool, example_count: int, failure_count: int}. RSpec: programmatic runner using RSpec::Core::Runner.run with StringIO for capture. Auto-detects spec/ directory. Files: lib/evilution/integration/base.rb, lib/evilution/integration/rspec.rb + specs.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:51.246990324+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:58:18.324768622+07:00","closed_at":"2026-03-02T10:58:18.324768622+07:00","close_reason":"Integration::Base and Integration::RSpec implemented with 8 passing specs","dependencies":[{"issue_id":"EV-2.10","depends_on_id":"EV-2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
|
|
@@ -23,6 +29,7 @@
|
|
|
23
29
|
{"id":"EV-2.7","title":"Implement ComparisonReplacement operator","description":"First mutation operator. Targets CallNode where name is :>, :<, :>=, :<=, :==, :!=. Replacements: > -> [>=, ==], < -> [<=, ==], >= -> [>, ==], <= -> [<, ==], == -> [!=], != -> [==]. Uses SourceSurgeon to apply text replacement at message_loc offsets. File: lib/evilution/mutator/operator/comparison_replacement.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:50.917501278+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:50:01.327985674+07:00","closed_at":"2026-03-02T10:50:01.327985674+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-2.7","depends_on_id":"EV-2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-2.7","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-2.7","depends_on_id":"EV-2.4","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
24
30
|
{"id":"EV-2.8","title":"Implement Evilution::Result::MutationResult and Summary","description":"MutationResult: value object with fields: mutation, status (:killed/:survived/:timeout/:error), duration, killing_test (optional). Summary: aggregates MutationResult array into total/killed/survived/timed_out/errors/score/duration. Files: lib/evilution/result/mutation_result.rb, lib/evilution/result/summary.rb + specs.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:51.022249568+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:51:08.482621898+07:00","closed_at":"2026-03-02T10:51:08.482621898+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-2.8","depends_on_id":"EV-2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
|
|
25
31
|
{"id":"EV-2.9","title":"Implement Evilution::Isolation::Fork","description":"Fork-based process isolation. Method: run(mutation, test_command, timeout:) -> MutationResult. Flow: create pipe, fork child, child applies mutation via eval, child runs test command, marshal result back via pipe, parent reads with IO.select timeout, SIGKILL on deadline. File: lib/evilution/isolation/fork.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:51.142167275+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:54:08.819310594+07:00","closed_at":"2026-03-02T10:54:08.819310594+07:00","close_reason":"Fork isolation implemented with 6 passing specs","dependencies":[{"issue_id":"EV-2.9","depends_on_id":"EV-2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-2.9","depends_on_id":"EV-2.8","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
32
|
+
{"id":"EV-20","title":"Deprecate coverage filtering","description":"With line-range and method-name targeting, coverage collection adds overhead for little benefit. Deprecate --no-coverage flag, coverage config key, and remove coverage logic from runner.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T00:30:58.837659684+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T00:33:06.453669976+07:00","closed_at":"2026-03-10T00:33:06.453669976+07:00","close_reason":"Deprecated coverage filtering: CLI warns on --no-coverage, config warns on coverage key, runner no longer uses coverage logic"}
|
|
26
33
|
{"id":"EV-3","title":"Phase 2: Mutation Operators & CLI","description":"Implement remaining 17 mutation operators, build CLI with OptionParser, exe/evilution executable, human-readable reporter. Milestone: bundle exec evilution run lib/user.rb --format json","status":"closed","priority":2,"issue_type":"epic","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:00.492971295+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:21:32.168384165+07:00","closed_at":"2026-03-02T11:21:32.168384165+07:00","close_reason":"Phase 2 complete: all 18 operators, CLI, Reporter::CLI, Registry registration, executable","dependencies":[{"issue_id":"EV-3","depends_on_id":"EV-2","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
27
34
|
{"id":"EV-3.1","title":"Implement ArithmeticReplacement operator","description":"Targets CallNode where name is :+, :-, :*, :/, :%, :**. Replacements: + <-> -, * <-> /, % -> /, ** -> *. File: lib/evilution/mutator/operator/arithmetic_replacement.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:13.025649082+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:06:10.922412304+07:00","closed_at":"2026-03-02T11:06:10.922412304+07:00","close_reason":"All 3 operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.1","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.1","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
28
35
|
{"id":"EV-3.10","title":"Implement SymbolLiteral operator","description":"Targets SymbolNode. Mutation: :foo -> :__evilution_mutated__. File: lib/evilution/mutator/operator/symbol_literal.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:13.935877816+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:13:36.541040613+07:00","closed_at":"2026-03-02T11:13:36.541040613+07:00","close_reason":"All 3 operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.10","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.10","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
@@ -30,7 +37,7 @@
|
|
|
30
37
|
{"id":"EV-3.12","title":"Implement ConditionalBranch operator","description":"Targets IfNode with both if and else branches. Mutations: replace entire if/else with if-branch only, replace with else-branch only. File: lib/evilution/mutator/operator/conditional_branch.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:14.138508575+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:13:36.541106251+07:00","closed_at":"2026-03-02T11:13:36.541106251+07:00","close_reason":"All 3 operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.12","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.12","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
31
38
|
{"id":"EV-3.13","title":"Implement StatementDeletion operator","description":"Targets StatementsNode bodies with 2+ statements. For each statement, produce a mutation that removes it. File: lib/evilution/mutator/operator/statement_deletion.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:14.240151154+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:16:08.954716442+07:00","closed_at":"2026-03-02T11:16:08.954716442+07:00","close_reason":"All 3 operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.13","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.13","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
32
39
|
{"id":"EV-3.14","title":"Implement MethodBodyReplacement operator","description":"Targets DefNode with body. Mutations: replace body with nil, replace with raise NotImplementedError. File: lib/evilution/mutator/operator/method_body_replacement.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:14.333934644+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:16:08.954887847+07:00","closed_at":"2026-03-02T11:16:08.954887847+07:00","close_reason":"All 3 operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.14","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.14","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
33
|
-
{"id":"EV-3.15","title":"Implement NegationInsertion operator","description":"Targets CallNode with predicate method names (ending in ?). Mutation: x.empty? -> !x.empty?. File: lib/evilution/mutator/operator/negation_insertion.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:14.438532333+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-
|
|
40
|
+
{"id":"EV-3.15","title":"Implement NegationInsertion operator","description":"Targets CallNode with predicate method names (ending in ?). Mutation: x.empty? -> !x.empty?. File: lib/evilution/mutator/operator/negation_insertion.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:14.438532333+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-06T16:11:55.50396051+07:00","closed_at":"2026-03-02T11:16:08.95490096+07:00","close_reason":"Commands now listed in --help output","dependencies":[{"issue_id":"EV-3.15","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.15","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
34
41
|
{"id":"EV-3.16","title":"Implement ReturnValueRemoval operator","description":"Targets ReturnNode with arguments. Mutations: return x -> return, return x -> return nil. File: lib/evilution/mutator/operator/return_value_removal.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:14.541333631+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:18:14.396683739+07:00","closed_at":"2026-03-02T11:18:14.396683739+07:00","close_reason":"Both operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.16","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.16","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
35
42
|
{"id":"EV-3.17","title":"Implement CollectionReplacement operator","description":"Targets CallNode where name matches collection methods. Replacements: map->each, select<->reject, all?<->any?, first<->last, min<->max, flat_map->map. File: lib/evilution/mutator/operator/collection_replacement.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:14.635444378+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:18:14.396880048+07:00","closed_at":"2026-03-02T11:18:14.396880048+07:00","close_reason":"Both operators implemented with passing specs","dependencies":[{"issue_id":"EV-3.17","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.17","depends_on_id":"EV-2.6","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
36
43
|
{"id":"EV-3.18","title":"Implement Evilution::CLI","description":"OptionParser-based CLI. Commands: run [FILES...], version, init. Flags: --jobs/-j, --timeout/-t, --format/-f (json/text), --diff BASE, --target PATTERN, --min-score FLOAT, --no-coverage, --config FILE, --verbose/-v, --quiet/-q. Parses args into Config, delegates to Runner. File: lib/evilution/cli.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:26.540459363+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:20:50.084035782+07:00","closed_at":"2026-03-02T11:20:50.084035782+07:00","close_reason":"CLI, Reporter::CLI, and Registry registration all implemented","dependencies":[{"issue_id":"EV-3.18","depends_on_id":"EV-3","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.18","depends_on_id":"EV-2.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-3.18","depends_on_id":"EV-2.12","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
@@ -63,6 +70,6 @@
|
|
|
63
70
|
{"id":"EV-5.5","title":"Write README.md","description":"Replace placeholder README with: gem description, installation, quick start, CLI usage, configuration, output formats (JSON schema), operator list, comparison with mutant, contributing guide, license.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:58.454635118+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:54:29.226280116+07:00","closed_at":"2026-03-02T11:54:29.226280116+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-5.5","depends_on_id":"EV-5","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-5.5","depends_on_id":"EV-4.9","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-5.5","depends_on_id":"EV-5.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
64
71
|
{"id":"EV-5.6","title":"Update CHANGELOG.md for v0.1.0","description":"Document all features in the initial release: operator list, RSpec integration, JSON/CLI output, parallel execution, coverage-based selection, diff-based targeting.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:06:58.571499415+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:54:29.22634981+07:00","closed_at":"2026-03-02T11:54:29.22634981+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-5.6","depends_on_id":"EV-5","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-5.6","depends_on_id":"EV-5.5","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
|
|
65
72
|
{"id":"EV-6","title":"Fix pool fork tests hanging in CI and WSL2","description":"The spec/evilution/parallel/pool_spec.rb fork-based tests hang on both WSL2 and GitHub Actions. CI currently excludes them via --exclude-pattern. Root cause is likely double-fork (Pool forks workers, each Worker uses Isolation::Fork which forks again) causing pipe/process management issues. Needs investigation and fix so pool tests run reliably in CI.","status":"closed","priority":1,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T16:21:43.62587758+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-05T12:31:51.831163433+07:00","closed_at":"2026-03-05T12:31:51.831163433+07:00","close_reason":"Closed"}
|
|
66
|
-
{"id":"EV-7","title":"Enable true per-mutation isolation with temp file copies","description":"Currently all mutations for the same file go to one worker (PR #4 fix). This means single-file projects get no parallelism benefit. Implement temp file copy approach: each worker copies the source file to a temp location, mutates the copy, and runs tests against it. This enables safe parallel mutation of the same file across multiple workers.","status":"
|
|
73
|
+
{"id":"EV-7","title":"Enable true per-mutation isolation with temp file copies","description":"Currently all mutations for the same file go to one worker (PR #4 fix). This means single-file projects get no parallelism benefit. Implement temp file copy approach: each worker copies the source file to a temp location, mutates the copy, and runs tests against it. This enables safe parallel mutation of the same file across multiple workers.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T16:21:46.820210376+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-06T11:46:55.740292929+07:00","closed_at":"2026-03-06T11:46:55.740292929+07:00","close_reason":"Already merged in PR #20"}
|
|
67
74
|
{"id":"EV-8","title":"Investigate RSpec state accumulation across mutation runs","description":"RSpec.reset is called between mutations but loaded spec files persist in memory. Long mutation runs may accumulate state from previously loaded specs, potentially causing false positives/negatives. Investigate whether RSpec::Core::Runner.run properly isolates between runs or if additional cleanup is needed.","status":"closed","priority":2,"issue_type":"bug","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T16:21:48.618669476+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-05T11:57:56.965910759+07:00","closed_at":"2026-03-05T11:57:56.965910759+07:00","close_reason":"Investigation complete: fork isolation already prevents state accumulation. Added documentation explaining the guarantee and tests verifying independent consecutive runs."}
|
|
68
75
|
{"id":"EV-9","title":"Add multi-Ruby CI test matrix (3.2, 3.3, 4.0)","description":"CI currently only tests Ruby 4.0.1. Add a matrix strategy testing Ruby 3.2, 3.3, and 4.0 to ensure compatibility across supported Ruby versions. Prism ships with Ruby 3.3+ so 3.2 may need the prism gem as a dependency.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T16:21:51.239774764+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-05T12:31:51.83181612+07:00","closed_at":"2026-03-05T12:31:51.83181612+07:00","close_reason":"Closed"}
|
|
@@ -73,7 +73,6 @@ Evilution
|
|
|
73
73
|
::Coverage::TestMap # Source line → test file mapping
|
|
74
74
|
::Diff::Parser # Git diff output parser
|
|
75
75
|
::Diff::FileFilter # Filter subjects to changed code
|
|
76
|
-
::Parallel::Pool # Thread-based worker pool
|
|
77
76
|
::Result::MutationResult # Single mutation outcome
|
|
78
77
|
::Result::Summary # Aggregated results
|
|
79
78
|
::Reporter::JSON # Structured output for AI agents
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2026-03-10
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Line-range targeting** — scope mutations to exact lines: `lib/foo.rb:15-30`, `lib/foo.rb:15`, `lib/foo.rb:15-`
|
|
8
|
+
- **Method-name targeting** (`--target`) — mutate a single method by fully-qualified name (e.g. `Foo::Bar#calculate`)
|
|
9
|
+
- **Commands section** in `--help` output
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Added `changelog_uri` to gemspec metadata
|
|
14
|
+
- Added GitHub publish workflow
|
|
15
|
+
|
|
16
|
+
### Deprecated
|
|
17
|
+
|
|
18
|
+
- **`--diff` flag** — use line-range targeting instead
|
|
19
|
+
- **Coverage-based filtering flags/config** (`--no-coverage` flag and `coverage` config key) — deprecated and now ignored; coverage-based filtering behavior has been removed from `Runner`
|
|
20
|
+
|
|
21
|
+
### Removed
|
|
22
|
+
|
|
23
|
+
- **Parallel execution** (`--jobs` flag) — simplifies codebase for AI-agent-first design; will be reintroduced later
|
|
24
|
+
- **File-discovery logic** from `Integration::RSpec` — spec files are now passed explicitly or default to `spec/`
|
|
25
|
+
|
|
3
26
|
## [0.1.0] - 2026-03-02
|
|
4
27
|
|
|
5
28
|
### Added
|
data/README.md
CHANGED
|
@@ -37,12 +37,13 @@ evilution [command] [options] [files...]
|
|
|
37
37
|
|
|
38
38
|
| Flag | Type | Default | Description |
|
|
39
39
|
|-------------------------|---------|--------------|---------------------------------------------------|
|
|
40
|
-
| `-j`, `--jobs N` | Integer | CPU cores | Parallel worker count. Use `1` for sequential. |
|
|
41
40
|
| `-t`, `--timeout N` | Integer | 10 | Per-mutation timeout in seconds. |
|
|
42
41
|
| `-f`, `--format FORMAT` | String | `text` | Output format: `text` or `json`. |
|
|
43
|
-
| `--
|
|
42
|
+
| `--target METHOD` | String | _(none)_ | Only mutate the named method (e.g. `Foo::Bar#calculate`). |
|
|
43
|
+
| `--diff BASE` | String | _(none)_ | **DEPRECATED**: Use line-range targeting instead. Git ref. Only mutate methods whose definition line changed since BASE. |
|
|
44
44
|
| `--min-score FLOAT` | Float | 0.0 | Minimum mutation score (0.0–1.0) to pass. |
|
|
45
|
-
| `--
|
|
45
|
+
| `--spec FILES` | Array | _(none)_ | Spec files to run (comma-separated). Defaults to `spec/`. |
|
|
46
|
+
| `--no-coverage` | Boolean | false | **DEPRECATED, NO-OP**: Kept for backward compatibility. Will be removed. |
|
|
46
47
|
| `-v`, `--verbose` | Boolean | false | Verbose output. |
|
|
47
48
|
| `-q`, `--quiet` | Boolean | false | Suppress output. |
|
|
48
49
|
|
|
@@ -61,12 +62,10 @@ Generate default config: `bundle exec evilution init`
|
|
|
61
62
|
Creates `.evilution.yml`:
|
|
62
63
|
|
|
63
64
|
```yaml
|
|
64
|
-
# jobs: 4 # parallel workers
|
|
65
65
|
# timeout: 10 # seconds per mutation
|
|
66
66
|
# format: text # text | json
|
|
67
67
|
# min_score: 0.0 # 0.0–1.0
|
|
68
68
|
# integration: rspec # test framework
|
|
69
|
-
# coverage: true # skip mutations on uncovered lines
|
|
70
69
|
```
|
|
71
70
|
|
|
72
71
|
**Precedence**: CLI flags override `.evilution.yml` values.
|
|
@@ -137,20 +136,51 @@ Each operator name is stable and appears in JSON output under `survived[].operat
|
|
|
137
136
|
### 1. Full project scan
|
|
138
137
|
|
|
139
138
|
```bash
|
|
140
|
-
bundle exec evilution run lib/ --format json --
|
|
139
|
+
bundle exec evilution run lib/ --format json --min-score 0.8
|
|
141
140
|
```
|
|
142
141
|
|
|
143
142
|
Parse JSON output. Exit code 0 = pass, 1 = surviving mutants to address.
|
|
144
143
|
|
|
145
|
-
### 2. PR /
|
|
144
|
+
### 2. PR / changed-lines scan (fast feedback)
|
|
146
145
|
|
|
147
146
|
```bash
|
|
148
|
-
bundle exec evilution run lib/ --format json --
|
|
147
|
+
bundle exec evilution run lib/foo.rb:15-30 lib/bar.rb:5-20 --format json --min-score 0.9
|
|
149
148
|
```
|
|
150
149
|
|
|
151
|
-
|
|
150
|
+
Target the exact lines you changed for fast, focused mutation testing. See line-range syntax below.
|
|
152
151
|
|
|
153
|
-
|
|
152
|
+
> **Note**: `--diff BASE` is deprecated and will be removed in a future version. Prefer line-range targeting for new workflows.
|
|
153
|
+
|
|
154
|
+
### 3. Line-range targeted scan (fastest)
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
bundle exec evilution run lib/foo.rb:15-30 --format json
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Target exact lines you changed. Supports multiple syntaxes:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
evilution run lib/foo.rb:15-30 # lines 15 through 30
|
|
164
|
+
evilution run lib/foo.rb:15 # single line 15
|
|
165
|
+
evilution run lib/foo.rb:15- # from line 15 to end of file
|
|
166
|
+
evilution run lib/foo.rb # whole file (existing behavior)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Methods whose body overlaps the requested range are included. Mix targeted and whole-file arguments freely:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
evilution run lib/foo.rb:15-30 lib/bar.rb --format json
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 4. Method-name targeted scan
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
bundle exec evilution run lib/foo.rb --target Foo::Bar#calculate --format json
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Target a specific method by its fully-qualified name. Useful when you want to focus on a single method without knowing its exact line numbers.
|
|
182
|
+
|
|
183
|
+
### 5. Single-file targeted scan
|
|
154
184
|
|
|
155
185
|
```bash
|
|
156
186
|
bundle exec evilution run lib/specific_file.rb --format json
|
|
@@ -158,7 +188,7 @@ bundle exec evilution run lib/specific_file.rb --format json
|
|
|
158
188
|
|
|
159
189
|
Use when you know which file was modified and want to verify its test coverage.
|
|
160
190
|
|
|
161
|
-
###
|
|
191
|
+
### 6. Fixing surviving mutants
|
|
162
192
|
|
|
163
193
|
For each entry in `survived[]`:
|
|
164
194
|
1. Read `file` at `line` to understand the code context
|
|
@@ -167,7 +197,7 @@ For each entry in `survived[]`:
|
|
|
167
197
|
4. Write a test that would fail if the mutation were applied
|
|
168
198
|
5. Re-run evilution on just that file to verify the mutant is now killed
|
|
169
199
|
|
|
170
|
-
###
|
|
200
|
+
### 7. CI gate
|
|
171
201
|
|
|
172
202
|
```bash
|
|
173
203
|
bundle exec evilution run lib/ --format json --min-score 0.8 --quiet
|
data/lib/evilution/cli.rb
CHANGED
|
@@ -11,7 +11,27 @@ module Evilution
|
|
|
11
11
|
@options = {}
|
|
12
12
|
@command = :run
|
|
13
13
|
argv = argv.dup
|
|
14
|
+
argv = extract_command(argv)
|
|
15
|
+
argv = warn_removed_flags(argv)
|
|
16
|
+
raw_args = build_option_parser.parse!(argv)
|
|
17
|
+
@files, @line_ranges = parse_file_args(raw_args)
|
|
18
|
+
end
|
|
14
19
|
|
|
20
|
+
def call
|
|
21
|
+
case @command
|
|
22
|
+
when :version
|
|
23
|
+
$stdout.puts(VERSION)
|
|
24
|
+
0
|
|
25
|
+
when :init
|
|
26
|
+
run_init
|
|
27
|
+
when :run
|
|
28
|
+
run_mutations
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def extract_command(argv)
|
|
15
35
|
case argv.first
|
|
16
36
|
when "version"
|
|
17
37
|
@command = :version
|
|
@@ -22,37 +42,65 @@ module Evilution
|
|
|
22
42
|
when "run"
|
|
23
43
|
argv.shift
|
|
24
44
|
end
|
|
45
|
+
argv
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def warn_removed_flags(argv)
|
|
49
|
+
result = []
|
|
50
|
+
i = 0
|
|
51
|
+
while i < argv.length
|
|
52
|
+
arg = argv[i]
|
|
53
|
+
if %w[--jobs -j].include?(arg)
|
|
54
|
+
warn("Warning: --jobs is no longer supported and will be ignored.")
|
|
55
|
+
next_arg = argv[i + 1]
|
|
56
|
+
i += next_arg&.match?(/\A-?\d+\z/) ? 2 : 1
|
|
57
|
+
elsif arg.start_with?("--jobs=") || arg.match?(/\A-j-?\d+\z/)
|
|
58
|
+
warn("Warning: --jobs is no longer supported and will be ignored.")
|
|
59
|
+
i += 1
|
|
60
|
+
else
|
|
61
|
+
result << arg
|
|
62
|
+
i += 1
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
result
|
|
66
|
+
end
|
|
25
67
|
|
|
26
|
-
|
|
68
|
+
def build_option_parser
|
|
69
|
+
OptionParser.new do |opts|
|
|
27
70
|
opts.banner = "Usage: evilution [command] [options] [files...]"
|
|
28
|
-
|
|
29
|
-
opts
|
|
30
|
-
opts.on("-t", "--timeout N", Integer, "Per-mutation timeout in seconds") { |n| @options[:timeout] = n }
|
|
31
|
-
opts.on("-f", "--format FORMAT", "Output format: text, json") { |f| @options[:format] = f.to_sym }
|
|
32
|
-
opts.on("--diff BASE", "Only mutate code changed since BASE") { |b| @options[:diff_base] = b }
|
|
33
|
-
opts.on("--min-score FLOAT", Float, "Minimum mutation score to pass") { |s| @options[:min_score] = s }
|
|
34
|
-
opts.on("--no-coverage", "Disable coverage-based filtering of uncovered mutations") { @options[:coverage] = false }
|
|
35
|
-
opts.on("-v", "--verbose", "Verbose output") { @options[:verbose] = true }
|
|
36
|
-
opts.on("-q", "--quiet", "Suppress output") { @options[:quiet] = true }
|
|
71
|
+
add_separators(opts)
|
|
72
|
+
add_options(opts)
|
|
37
73
|
end
|
|
74
|
+
end
|
|
38
75
|
|
|
39
|
-
|
|
76
|
+
def add_separators(opts)
|
|
77
|
+
opts.separator ""
|
|
78
|
+
opts.separator "Line-range targeting: lib/foo.rb:15-30, lib/foo.rb:15, lib/foo.rb:15-"
|
|
79
|
+
opts.separator ""
|
|
80
|
+
opts.separator "Commands: run (default), init, version"
|
|
81
|
+
opts.separator ""
|
|
82
|
+
opts.separator "Options:"
|
|
40
83
|
end
|
|
41
84
|
|
|
42
|
-
def
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
when :run
|
|
50
|
-
run_mutations
|
|
85
|
+
def add_options(opts)
|
|
86
|
+
opts.on("-t", "--timeout N", Integer, "Per-mutation timeout in seconds") { |n| @options[:timeout] = n }
|
|
87
|
+
opts.on("-f", "--format FORMAT", "Output format: text, json") { |f| @options[:format] = f.to_sym }
|
|
88
|
+
opts.on("--diff BASE", "DEPRECATED: Use line-range targeting instead") do |b|
|
|
89
|
+
warn("Warning: --diff is deprecated and will be removed in a future version. " \
|
|
90
|
+
"Use line-range targeting instead: evilution run lib/foo.rb:15-30")
|
|
91
|
+
@options[:diff_base] = b
|
|
51
92
|
end
|
|
93
|
+
opts.on("--min-score FLOAT", Float, "Minimum mutation score to pass") { |s| @options[:min_score] = s }
|
|
94
|
+
opts.on("--spec FILES", Array, "Spec files to run (comma-separated)") { |f| @options[:spec_files] = f }
|
|
95
|
+
opts.on("--target METHOD", "Only mutate the named method (e.g. Foo::Bar#calculate)") { |m| @options[:target] = m }
|
|
96
|
+
opts.on("--no-coverage", "DEPRECATED: Has no effect and will be removed in a future version") do
|
|
97
|
+
warn("Warning: --no-coverage is deprecated, currently has no effect, and will be removed in a future version.")
|
|
98
|
+
@options[:coverage] = false
|
|
99
|
+
end
|
|
100
|
+
opts.on("-v", "--verbose", "Verbose output") { @options[:verbose] = true }
|
|
101
|
+
opts.on("-q", "--quiet", "Suppress output") { @options[:quiet] = true }
|
|
52
102
|
end
|
|
53
103
|
|
|
54
|
-
private
|
|
55
|
-
|
|
56
104
|
def run_init
|
|
57
105
|
path = ".evilution.yml"
|
|
58
106
|
if File.exist?(path)
|
|
@@ -65,8 +113,35 @@ module Evilution
|
|
|
65
113
|
0
|
|
66
114
|
end
|
|
67
115
|
|
|
116
|
+
def parse_file_args(raw_args)
|
|
117
|
+
files = []
|
|
118
|
+
ranges = {}
|
|
119
|
+
|
|
120
|
+
raw_args.each do |arg|
|
|
121
|
+
file, range_str = arg.split(":", 2)
|
|
122
|
+
files << file
|
|
123
|
+
next unless range_str
|
|
124
|
+
|
|
125
|
+
ranges[file] = parse_line_range(range_str)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
[files, ranges]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def parse_line_range(str)
|
|
132
|
+
if str.include?("-")
|
|
133
|
+
start_str, end_str = str.split("-", 2)
|
|
134
|
+
start_line = Integer(start_str)
|
|
135
|
+
end_line = end_str.empty? ? Float::INFINITY : Integer(end_str)
|
|
136
|
+
start_line..end_line
|
|
137
|
+
else
|
|
138
|
+
line = Integer(str)
|
|
139
|
+
line..line
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
68
143
|
def run_mutations
|
|
69
|
-
config = Config.new(**@options, target_files: @files)
|
|
144
|
+
config = Config.new(**@options, target_files: @files, line_ranges: @line_ranges)
|
|
70
145
|
runner = Runner.new(config: config)
|
|
71
146
|
summary = runner.call
|
|
72
147
|
summary.success?(min_score: config.min_score) ? 0 : 1
|
data/lib/evilution/config.rb
CHANGED
|
@@ -7,7 +7,6 @@ module Evilution
|
|
|
7
7
|
CONFIG_FILES = %w[.evilution.yml config/evilution.yml].freeze
|
|
8
8
|
|
|
9
9
|
DEFAULTS = {
|
|
10
|
-
jobs: nil,
|
|
11
10
|
timeout: 10,
|
|
12
11
|
format: :text,
|
|
13
12
|
diff_base: nil,
|
|
@@ -16,17 +15,20 @@ module Evilution
|
|
|
16
15
|
integration: :rspec,
|
|
17
16
|
coverage: true,
|
|
18
17
|
verbose: false,
|
|
19
|
-
quiet: false
|
|
18
|
+
quiet: false,
|
|
19
|
+
line_ranges: {},
|
|
20
|
+
spec_files: []
|
|
20
21
|
}.freeze
|
|
21
22
|
|
|
22
|
-
attr_reader :target_files, :
|
|
23
|
-
:target, :min_score, :integration, :coverage, :verbose, :quiet
|
|
23
|
+
attr_reader :target_files, :timeout, :format, :diff_base,
|
|
24
|
+
:target, :min_score, :integration, :coverage, :verbose, :quiet,
|
|
25
|
+
:line_ranges, :spec_files
|
|
24
26
|
|
|
25
27
|
def initialize(**options)
|
|
26
28
|
file_options = options.delete(:skip_config_file) ? {} : load_config_file
|
|
27
29
|
merged = DEFAULTS.merge(file_options).merge(options)
|
|
30
|
+
warn_removed_options(merged, file_options)
|
|
28
31
|
@target_files = Array(merged[:target_files])
|
|
29
|
-
@jobs = merged[:jobs] || default_jobs
|
|
30
32
|
@timeout = merged[:timeout]
|
|
31
33
|
@format = merged[:format].to_sym
|
|
32
34
|
@diff_base = merged[:diff_base]
|
|
@@ -36,6 +38,8 @@ module Evilution
|
|
|
36
38
|
@coverage = merged[:coverage]
|
|
37
39
|
@verbose = merged[:verbose]
|
|
38
40
|
@quiet = merged[:quiet]
|
|
41
|
+
@line_ranges = merged[:line_ranges] || {}
|
|
42
|
+
@spec_files = Array(merged[:spec_files])
|
|
39
43
|
freeze
|
|
40
44
|
end
|
|
41
45
|
|
|
@@ -51,15 +55,20 @@ module Evilution
|
|
|
51
55
|
!diff_base.nil?
|
|
52
56
|
end
|
|
53
57
|
|
|
58
|
+
def line_ranges?
|
|
59
|
+
!line_ranges.empty?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def target?
|
|
63
|
+
!target.nil?
|
|
64
|
+
end
|
|
65
|
+
|
|
54
66
|
# Generates a default config file template.
|
|
55
67
|
def self.default_template
|
|
56
68
|
<<~YAML
|
|
57
69
|
# Evilution configuration
|
|
58
70
|
# See: https://github.com/marinazzio/evilution
|
|
59
71
|
|
|
60
|
-
# Number of parallel workers (default: number of CPU cores)
|
|
61
|
-
# jobs: 4
|
|
62
|
-
|
|
63
72
|
# Per-mutation timeout in seconds (default: 10)
|
|
64
73
|
# timeout: 10
|
|
65
74
|
|
|
@@ -72,13 +81,30 @@ module Evilution
|
|
|
72
81
|
# Test integration: rspec (default: rspec)
|
|
73
82
|
# integration: rspec
|
|
74
83
|
|
|
75
|
-
#
|
|
84
|
+
# DEPRECATED: Coverage filtering is deprecated and will be removed
|
|
76
85
|
# coverage: true
|
|
77
86
|
YAML
|
|
78
87
|
end
|
|
79
88
|
|
|
80
89
|
private
|
|
81
90
|
|
|
91
|
+
def warn_removed_options(merged, file_options)
|
|
92
|
+
if merged.key?(:jobs)
|
|
93
|
+
warn("Warning: 'jobs' option is no longer supported and will be ignored. " \
|
|
94
|
+
"Remove it from your configuration or invocation.")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
if file_options.key?(:coverage)
|
|
98
|
+
warn("Warning: 'coverage' in config file is deprecated and ignored. " \
|
|
99
|
+
"This option will be removed in a future version.")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
return unless file_options[:diff_base]
|
|
103
|
+
|
|
104
|
+
warn("Warning: 'diff_base' in config file is deprecated and will be removed in a future version. " \
|
|
105
|
+
"Use line-range targeting instead: evilution run lib/foo.rb:15-30")
|
|
106
|
+
end
|
|
107
|
+
|
|
82
108
|
def load_config_file
|
|
83
109
|
CONFIG_FILES.each do |path|
|
|
84
110
|
next unless File.exist?(path)
|
|
@@ -89,10 +115,5 @@ module Evilution
|
|
|
89
115
|
|
|
90
116
|
{}
|
|
91
117
|
end
|
|
92
|
-
|
|
93
|
-
def default_jobs
|
|
94
|
-
require "etc"
|
|
95
|
-
Etc.nprocessors
|
|
96
|
-
end
|
|
97
118
|
end
|
|
98
119
|
end
|
|
@@ -108,77 +108,10 @@ module Evilution
|
|
|
108
108
|
{ passed: false, error: e.message }
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
-
def build_args(
|
|
112
|
-
files = test_files ||
|
|
111
|
+
def build_args(_mutation)
|
|
112
|
+
files = test_files || ["spec"]
|
|
113
113
|
["--format", "progress", "--no-color", "--order", "defined", *files]
|
|
114
114
|
end
|
|
115
|
-
|
|
116
|
-
def detect_test_files(mutation)
|
|
117
|
-
# Convention: lib/foo/bar.rb -> spec/foo/bar_spec.rb
|
|
118
|
-
candidates = spec_file_candidates(mutation.file_path)
|
|
119
|
-
found = candidates.select { |f| File.exist?(f) }
|
|
120
|
-
return found unless found.empty?
|
|
121
|
-
|
|
122
|
-
# Fallback: find spec/ directory relative to the mutation's project root
|
|
123
|
-
fallback = fallback_spec_dir(mutation.file_path)
|
|
124
|
-
return [fallback] if fallback
|
|
125
|
-
|
|
126
|
-
[]
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def fallback_spec_dir(source_path)
|
|
130
|
-
expanded = File.expand_path(source_path)
|
|
131
|
-
|
|
132
|
-
# Derive spec/ from mutation's project, not CWD
|
|
133
|
-
if expanded.include?("/lib/")
|
|
134
|
-
project_root = expanded.split(%r{/lib/}, 2).first
|
|
135
|
-
spec_dir = File.join(project_root, "spec")
|
|
136
|
-
return spec_dir if Dir.exist?(spec_dir)
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# Walk up from the file's directory looking for spec/
|
|
140
|
-
dir = File.dirname(expanded)
|
|
141
|
-
loop do
|
|
142
|
-
spec_dir = File.join(dir, "spec")
|
|
143
|
-
return spec_dir if Dir.exist?(spec_dir)
|
|
144
|
-
|
|
145
|
-
parent = File.dirname(dir)
|
|
146
|
-
break if parent == dir
|
|
147
|
-
|
|
148
|
-
dir = parent
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
nil
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def spec_file_candidates(source_path)
|
|
155
|
-
candidates = []
|
|
156
|
-
|
|
157
|
-
if source_path.start_with?("lib/")
|
|
158
|
-
# lib/foo/bar.rb -> spec/foo/bar_spec.rb
|
|
159
|
-
relative = source_path.sub(%r{^lib/}, "")
|
|
160
|
-
spec_name = relative.sub(/\.rb$/, "_spec.rb")
|
|
161
|
-
candidates << File.join("spec", spec_name)
|
|
162
|
-
candidates << File.join("spec", "unit", spec_name)
|
|
163
|
-
elsif source_path.include?("/lib/")
|
|
164
|
-
# /absolute/path/lib/foo/bar.rb -> /absolute/path/spec/foo/bar_spec.rb
|
|
165
|
-
prefix, relative = source_path.split(%r{/lib/}, 2)
|
|
166
|
-
spec_name = relative.sub(/\.rb$/, "_spec.rb")
|
|
167
|
-
candidates << File.join(prefix, "spec", spec_name)
|
|
168
|
-
candidates << File.join(prefix, "spec", "unit", spec_name)
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# Same directory: foo/bar.rb -> foo/bar_spec.rb
|
|
172
|
-
sibling_spec = source_path.sub(/\.rb$/, "_spec.rb")
|
|
173
|
-
candidates << sibling_spec
|
|
174
|
-
|
|
175
|
-
# Subdirectory spec/ variant: foo/bar.rb -> foo/spec/bar_spec.rb
|
|
176
|
-
dir = File.dirname(source_path)
|
|
177
|
-
base = File.basename(source_path, ".rb")
|
|
178
|
-
candidates << File.join(dir, "spec", "#{base}_spec.rb")
|
|
179
|
-
|
|
180
|
-
candidates.uniq
|
|
181
|
-
end
|
|
182
115
|
end
|
|
183
116
|
end
|
|
184
117
|
end
|
data/lib/evilution/runner.rb
CHANGED
|
@@ -4,13 +4,10 @@ require_relative "config"
|
|
|
4
4
|
require_relative "ast/parser"
|
|
5
5
|
require_relative "mutator/registry"
|
|
6
6
|
require_relative "isolation/fork"
|
|
7
|
-
require_relative "parallel/pool"
|
|
8
7
|
require_relative "integration/rspec"
|
|
9
8
|
require_relative "reporter/json"
|
|
10
9
|
require_relative "reporter/cli"
|
|
11
10
|
require_relative "reporter/suggestion"
|
|
12
|
-
require_relative "coverage/collector"
|
|
13
|
-
require_relative "coverage/test_map"
|
|
14
11
|
require_relative "diff/parser"
|
|
15
12
|
require_relative "diff/file_filter"
|
|
16
13
|
require_relative "result/mutation_result"
|
|
@@ -31,12 +28,11 @@ module Evilution
|
|
|
31
28
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
32
29
|
|
|
33
30
|
subjects = parse_subjects
|
|
31
|
+
subjects = filter_by_target(subjects) if config.target?
|
|
32
|
+
subjects = filter_by_line_ranges(subjects) if config.line_ranges?
|
|
34
33
|
subjects = filter_by_diff(subjects) if config.diff?
|
|
35
34
|
mutations = generate_mutations(subjects)
|
|
36
|
-
test_map = collect_coverage if config.coverage && config.integration == :rspec
|
|
37
|
-
mutations, skipped = filter_by_coverage(mutations, test_map) if test_map
|
|
38
35
|
results = run_mutations(mutations)
|
|
39
|
-
results.concat(skipped) if skipped
|
|
40
36
|
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
41
37
|
|
|
42
38
|
summary = Result::Summary.new(results: results, duration: duration)
|
|
@@ -53,6 +49,24 @@ module Evilution
|
|
|
53
49
|
config.target_files.flat_map { |file| parser.call(file) }
|
|
54
50
|
end
|
|
55
51
|
|
|
52
|
+
def filter_by_target(subjects)
|
|
53
|
+
matched = subjects.select { |s| s.name == config.target }
|
|
54
|
+
raise Error, "no method found matching '#{config.target}'" if matched.empty?
|
|
55
|
+
|
|
56
|
+
matched
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def filter_by_line_ranges(subjects)
|
|
60
|
+
subjects.select do |subject|
|
|
61
|
+
range = config.line_ranges[subject.file_path]
|
|
62
|
+
next true unless range
|
|
63
|
+
|
|
64
|
+
subject_start = subject.line_number
|
|
65
|
+
subject_end = subject_start + subject.source.count("\n")
|
|
66
|
+
subject_start <= range.last && subject_end >= range.first
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
56
70
|
def filter_by_diff(subjects)
|
|
57
71
|
diff_parser = Diff::Parser.new
|
|
58
72
|
changed_ranges = diff_parser.parse(config.diff_base)
|
|
@@ -63,37 +77,9 @@ module Evilution
|
|
|
63
77
|
subjects.flat_map { |subject| registry.mutations_for(subject) }
|
|
64
78
|
end
|
|
65
79
|
|
|
66
|
-
def collect_coverage
|
|
67
|
-
test_files = Dir.glob("spec/**/*_spec.rb")
|
|
68
|
-
return nil if test_files.empty?
|
|
69
|
-
|
|
70
|
-
data = Coverage::Collector.new.call(test_files: test_files)
|
|
71
|
-
Coverage::TestMap.new(data)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def filter_by_coverage(mutations, test_map)
|
|
75
|
-
covered, uncovered = mutations.partition do |m|
|
|
76
|
-
test_map.covered?(File.expand_path(m.file_path), m.line)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
skipped = uncovered.map do |m|
|
|
80
|
-
Result::MutationResult.new(mutation: m, status: :survived, duration: 0.0)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
[covered, skipped]
|
|
84
|
-
end
|
|
85
|
-
|
|
86
80
|
def run_mutations(mutations)
|
|
87
81
|
integration = build_integration
|
|
88
82
|
|
|
89
|
-
if config.jobs > 1 && mutations.size > 1
|
|
90
|
-
run_parallel(mutations, integration)
|
|
91
|
-
else
|
|
92
|
-
run_sequential(mutations, integration)
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def run_sequential(mutations, integration)
|
|
97
83
|
mutations.map do |mutation|
|
|
98
84
|
test_command = ->(m) { integration.call(m) }
|
|
99
85
|
isolator.call(
|
|
@@ -104,16 +90,11 @@ module Evilution
|
|
|
104
90
|
end
|
|
105
91
|
end
|
|
106
92
|
|
|
107
|
-
def run_parallel(mutations, integration)
|
|
108
|
-
pool = Parallel::Pool.new(jobs: config.jobs)
|
|
109
|
-
test_command_builder = ->(_mutation) { ->(m) { integration.call(m) } }
|
|
110
|
-
pool.call(mutations: mutations, test_command_builder: test_command_builder, timeout: config.timeout)
|
|
111
|
-
end
|
|
112
|
-
|
|
113
93
|
def build_integration
|
|
114
94
|
case config.integration
|
|
115
95
|
when :rspec
|
|
116
|
-
|
|
96
|
+
test_files = config.spec_files.empty? ? nil : config.spec_files
|
|
97
|
+
Integration::RSpec.new(test_files: test_files)
|
|
117
98
|
else
|
|
118
99
|
raise Error, "unknown integration: #{config.integration}"
|
|
119
100
|
end
|
data/lib/evilution/version.rb
CHANGED
data/lib/evilution.rb
CHANGED
|
@@ -27,8 +27,6 @@ require_relative "evilution/mutator/operator/return_value_removal"
|
|
|
27
27
|
require_relative "evilution/mutator/operator/collection_replacement"
|
|
28
28
|
require_relative "evilution/mutator/registry"
|
|
29
29
|
require_relative "evilution/isolation/fork"
|
|
30
|
-
require_relative "evilution/parallel/worker"
|
|
31
|
-
require_relative "evilution/parallel/pool"
|
|
32
30
|
require_relative "evilution/diff/parser"
|
|
33
31
|
require_relative "evilution/diff/file_filter"
|
|
34
32
|
require_relative "evilution/integration/base"
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: evilution
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Denis Kiselev
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-03-09 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: diff-lcs
|
|
@@ -89,8 +90,6 @@ files:
|
|
|
89
90
|
- lib/evilution/mutator/operator/string_literal.rb
|
|
90
91
|
- lib/evilution/mutator/operator/symbol_literal.rb
|
|
91
92
|
- lib/evilution/mutator/registry.rb
|
|
92
|
-
- lib/evilution/parallel/pool.rb
|
|
93
|
-
- lib/evilution/parallel/worker.rb
|
|
94
93
|
- lib/evilution/reporter/cli.rb
|
|
95
94
|
- lib/evilution/reporter/json.rb
|
|
96
95
|
- lib/evilution/reporter/suggestion.rb
|
|
@@ -107,9 +106,11 @@ metadata:
|
|
|
107
106
|
allowed_push_host: https://rubygems.org
|
|
108
107
|
bug_tracker_uri: https://github.com/marinazzio/evilution/issues
|
|
109
108
|
documentation_uri: https://github.com/marinazzio/evilution/blob/master/README.md
|
|
109
|
+
changelog_uri: https://github.com/marinazzio/evilution/blob/master/CHANGELOG.md
|
|
110
110
|
homepage_uri: https://github.com/marinazzio/evilution
|
|
111
111
|
rubygems_mfa_required: 'true'
|
|
112
112
|
source_code_uri: https://github.com/marinazzio/evilution
|
|
113
|
+
post_install_message:
|
|
113
114
|
rdoc_options: []
|
|
114
115
|
require_paths:
|
|
115
116
|
- lib
|
|
@@ -124,7 +125,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
124
125
|
- !ruby/object:Gem::Version
|
|
125
126
|
version: '0'
|
|
126
127
|
requirements: []
|
|
127
|
-
rubygems_version:
|
|
128
|
+
rubygems_version: 3.5.22
|
|
129
|
+
signing_key:
|
|
128
130
|
specification_version: 4
|
|
129
131
|
summary: Free, MIT-licensed mutation testing for Ruby
|
|
130
132
|
test_files: []
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Evilution
|
|
4
|
-
module Parallel
|
|
5
|
-
class Pool
|
|
6
|
-
def initialize(jobs:)
|
|
7
|
-
@jobs = jobs
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
# Executes mutations in parallel across N worker processes.
|
|
11
|
-
#
|
|
12
|
-
# @param mutations [Array] Array of mutation objects to run
|
|
13
|
-
# @param test_command_builder [#call] Callable that receives a mutation and returns a test command callable
|
|
14
|
-
# @param timeout [Numeric] Per-mutation timeout in seconds
|
|
15
|
-
# @return [Array<Result::MutationResult>]
|
|
16
|
-
def call(mutations:, test_command_builder:, timeout:)
|
|
17
|
-
return [] if mutations.empty?
|
|
18
|
-
|
|
19
|
-
worker_count = [@jobs, mutations.size].min
|
|
20
|
-
chunks = partition(mutations, worker_count)
|
|
21
|
-
|
|
22
|
-
pipes = worker_count.times.map { IO.pipe }
|
|
23
|
-
|
|
24
|
-
pids = chunks.each_with_index.map do |chunk, index|
|
|
25
|
-
_, write_io = pipes[index]
|
|
26
|
-
|
|
27
|
-
pid = Process.fork do
|
|
28
|
-
# Close all read ends in the child; close sibling write ends too
|
|
29
|
-
pipes.each_with_index do |(r, w), i|
|
|
30
|
-
if i == index
|
|
31
|
-
r.close
|
|
32
|
-
else
|
|
33
|
-
r.close
|
|
34
|
-
w.close
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
results = run_chunk(chunk, test_command_builder, timeout)
|
|
39
|
-
Marshal.dump(results, write_io)
|
|
40
|
-
write_io.close
|
|
41
|
-
exit!(0)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
write_io.close
|
|
45
|
-
pid
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
results = collect_results(pipes.map(&:first), pids)
|
|
49
|
-
|
|
50
|
-
results
|
|
51
|
-
ensure
|
|
52
|
-
# Ensure all pipes are closed even if something goes wrong
|
|
53
|
-
pipes&.each do |read_io, write_io|
|
|
54
|
-
read_io.close unless read_io.closed?
|
|
55
|
-
write_io.close unless write_io.closed?
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
private
|
|
60
|
-
|
|
61
|
-
attr_reader :jobs
|
|
62
|
-
|
|
63
|
-
# Distributes mutations across N chunks using round-robin.
|
|
64
|
-
# File isolation is handled by Integration::RSpec via temp directories
|
|
65
|
-
# and $LOAD_PATH, so same-file mutations can safely run in parallel.
|
|
66
|
-
def partition(mutations, n)
|
|
67
|
-
chunks = Array.new(n) { [] }
|
|
68
|
-
mutations.each_with_index { |m, i| chunks[i % n] << m }
|
|
69
|
-
chunks
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# Runs a chunk of mutations sequentially inside a worker process.
|
|
73
|
-
def run_chunk(mutations, test_command_builder, timeout)
|
|
74
|
-
worker = Worker.new
|
|
75
|
-
worker.call(mutations: mutations, test_command_builder: test_command_builder, timeout: timeout)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Reads results from all worker pipes and waits for workers to finish.
|
|
79
|
-
def collect_results(read_ios, pids)
|
|
80
|
-
results = []
|
|
81
|
-
|
|
82
|
-
read_ios.each_with_index do |read_io, _index|
|
|
83
|
-
data = read_io.read
|
|
84
|
-
read_io.close
|
|
85
|
-
|
|
86
|
-
unless data.empty?
|
|
87
|
-
chunk_results = Marshal.load(data) # rubocop:disable Security/MarshalLoad
|
|
88
|
-
results.concat(chunk_results)
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
pids.each { |pid| Process.wait(pid) rescue nil } # rubocop:disable Style/RescueModifier
|
|
93
|
-
|
|
94
|
-
results
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Evilution
|
|
4
|
-
module Parallel
|
|
5
|
-
class Worker
|
|
6
|
-
def initialize(isolator: Isolation::Fork.new)
|
|
7
|
-
@isolator = isolator
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
# Runs a batch of mutations sequentially using fork isolation.
|
|
11
|
-
#
|
|
12
|
-
# @param mutations [Array<Mutation>] Mutations to execute
|
|
13
|
-
# @param test_command_builder [#call] Receives a mutation, returns a test command callable
|
|
14
|
-
# @param timeout [Numeric] Per-mutation timeout in seconds
|
|
15
|
-
# @return [Array<Result::MutationResult>]
|
|
16
|
-
def call(mutations:, test_command_builder:, timeout:)
|
|
17
|
-
mutations.map do |mutation|
|
|
18
|
-
test_command = test_command_builder.call(mutation)
|
|
19
|
-
@isolator.call(mutation: mutation, test_command: test_command, timeout: timeout)
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|