evilution 0.27.0 → 0.28.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/interactions.jsonl +13 -0
- data/.rubocop_todo.yml +0 -1
- data/CHANGELOG.md +32 -0
- data/README.md +19 -0
- data/lib/evilution/baseline.rb +5 -4
- data/lib/evilution/cli/parser/options_builder.rb +7 -0
- data/lib/evilution/compare/diff_extractor/evilution.rb +22 -0
- data/lib/evilution/compare/diff_extractor/mutant.rb +30 -0
- data/lib/evilution/compare/diff_extractor.rb +6 -0
- data/lib/evilution/compare/fingerprint.rb +15 -72
- data/lib/evilution/compare/line_normalizer.rb +72 -0
- data/lib/evilution/compare/normalizer.rb +17 -4
- data/lib/evilution/config/validators/profile.rb +11 -0
- data/lib/evilution/config.rb +40 -23
- data/lib/evilution/integration/crash_detector.rb +2 -2
- data/lib/evilution/integration/loading/source_evaluator.rb +6 -2
- data/lib/evilution/integration/minitest_crash_detector.rb +2 -2
- data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +11 -3
- data/lib/evilution/isolation/fork.rb +16 -11
- data/lib/evilution/isolation/in_process.rb +10 -6
- data/lib/evilution/mcp/info_tool.rb +0 -2
- data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +5 -1
- data/lib/evilution/mcp/session_tool.rb +0 -2
- data/lib/evilution/mutation.rb +47 -27
- data/lib/evilution/mutator/base.rb +8 -8
- data/lib/evilution/mutator/operator/predicate_to_nil.rb +20 -0
- data/lib/evilution/mutator/registry.rb +20 -0
- data/lib/evilution/parallel/work_queue/channel/frame.rb +5 -1
- data/lib/evilution/parallel/work_queue/worker/loop.rb +1 -1
- data/lib/evilution/process_cleanup.rb +19 -0
- data/lib/evilution/reporter/html/baseline_keys.rb +1 -1
- data/lib/evilution/reporter/html/diff_formatter.rb +1 -1
- data/lib/evilution/reporter/html/escape.rb +1 -1
- data/lib/evilution/reporter/html/section.rb +1 -1
- data/lib/evilution/reporter/html/sections.rb +4 -2
- data/lib/evilution/reporter/html/stylesheet.rb +1 -1
- data/lib/evilution/reporter/html.rb +8 -3
- data/lib/evilution/reporter/suggestion/registry.rb +1 -5
- data/lib/evilution/reporter/suggestion/templates/generic.rb +1 -1
- data/lib/evilution/reporter/suggestion/templates/minitest.rb +349 -643
- data/lib/evilution/reporter/suggestion/templates/rspec.rb +351 -598
- data/lib/evilution/reporter/suggestion/templates.rb +6 -0
- data/lib/evilution/result/error_info.rb +20 -0
- data/lib/evilution/result/memory_stats.rb +20 -0
- data/lib/evilution/result/mutation_result.rb +30 -14
- data/lib/evilution/runner/baseline_runner.rb +1 -2
- data/lib/evilution/runner/diagnostics.rb +1 -2
- data/lib/evilution/runner/isolation_resolver.rb +1 -2
- data/lib/evilution/runner/mutation_executor/mutation_runner.rb +1 -3
- data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +1 -2
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +3 -10
- data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +3 -10
- data/lib/evilution/runner/mutation_executor/neutralizer.rb +11 -0
- data/lib/evilution/runner/mutation_executor/result_cache.rb +1 -3
- data/lib/evilution/runner/mutation_executor/result_notifier.rb +1 -3
- data/lib/evilution/runner/mutation_executor/result_packer.rb +11 -9
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +1 -3
- data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +1 -3
- data/lib/evilution/runner/mutation_executor/strategy.rb +11 -0
- data/lib/evilution/runner/mutation_executor.rb +12 -20
- data/lib/evilution/runner/mutation_planner.rb +1 -2
- data/lib/evilution/runner/report_publisher.rb +1 -2
- data/lib/evilution/runner/subject_pipeline.rb +1 -2
- data/lib/evilution/runner.rb +33 -31
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +1 -0
- data/script/memory_check +3 -1
- metadata +14 -3
- data/lib/evilution/reporter/html/namespace.rb +0 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dda90ba09660e134411bad5df79ba56075ed9661f8e94aa3b366478cc3191755
|
|
4
|
+
data.tar.gz: c1c5606e0a2c082dd1d7ec24738f0558b7cc94bfc3632cd4c2fdd200d5ad5e11
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 30d4d7dcdcb9d88615fa5d37205bd02a307f32b6150a7ee7fa55b8397b677d40dd26e5777043fb6dfeeddf2559c8e79ae23276574ede65627ccf95f9effcd31e
|
|
7
|
+
data.tar.gz: a657c55edcffd46c6883c4146cd97471db4c51940def54027a49f01ff89a29e7fd6fd4c28e758d9d7207623d664c320ccc22a9a01430abd5d26574afb1142932
|
data/.beads/interactions.jsonl
CHANGED
|
@@ -245,3 +245,16 @@
|
|
|
245
245
|
{"id":"int-9d182026","kind":"field_change","created_at":"2026-04-25T17:54:26.23542935Z","actor":"Denis Kiselev","issue_id":"EV-67yh","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #897"}}
|
|
246
246
|
{"id":"int-7ede49f5","kind":"field_change","created_at":"2026-04-25T18:16:18.358350457Z","actor":"Denis Kiselev","issue_id":"EV-zvhp","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #898"}}
|
|
247
247
|
{"id":"int-c28f7ee0","kind":"field_change","created_at":"2026-04-26T02:28:38.321564801Z","actor":"Denis Kiselev","issue_id":"EV-vev8","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #899"}}
|
|
248
|
+
{"id":"int-111f631a","kind":"field_change","created_at":"2026-04-26T07:57:40.59622849Z","actor":"Denis Kiselev","issue_id":"EV-p5vh","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Released in v0.27.0 (PR #901 merged, release PR #902 shipped)"}}
|
|
249
|
+
{"id":"int-5a159da9","kind":"field_change","created_at":"2026-04-29T11:51:40.86913186Z","actor":"Denis Kiselev","issue_id":"EV-vm61","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
250
|
+
{"id":"int-1787900f","kind":"field_change","created_at":"2026-04-30T02:40:52.288627178Z","actor":"Denis Kiselev","issue_id":"EV-v1i6","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"merged via PR #911"}}
|
|
251
|
+
{"id":"int-08958dc0","kind":"field_change","created_at":"2026-04-30T03:15:14.369272687Z","actor":"Denis Kiselev","issue_id":"EV-x3s0","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"merged via PR #912"}}
|
|
252
|
+
{"id":"int-e013b002","kind":"field_change","created_at":"2026-04-30T05:46:44.70653253Z","actor":"Denis Kiselev","issue_id":"EV-eww3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"merged via PR #913"}}
|
|
253
|
+
{"id":"int-7b15a1b0","kind":"field_change","created_at":"2026-04-30T09:30:21.057039785Z","actor":"Denis Kiselev","issue_id":"EV-7rov","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"merged via PR #914"}}
|
|
254
|
+
{"id":"int-71c9f98d","kind":"field_change","created_at":"2026-04-30T12:30:36.528587334Z","actor":"Denis Kiselev","issue_id":"EV-318q","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"merged"}}
|
|
255
|
+
{"id":"int-42eadbdc","kind":"field_change","created_at":"2026-04-30T16:09:00.539400752Z","actor":"Denis Kiselev","issue_id":"EV-voay","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"merged"}}
|
|
256
|
+
{"id":"int-cb4c0ebf","kind":"field_change","created_at":"2026-05-01T02:38:05.091612839Z","actor":"Denis Kiselev","issue_id":"EV-t918","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"merged via PR #918"}}
|
|
257
|
+
{"id":"int-ffa8f0b2","kind":"field_change","created_at":"2026-05-01T17:47:23.967998678Z","actor":"Denis Kiselev","issue_id":"EV-m3ta","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"merged"}}
|
|
258
|
+
{"id":"int-b9cb7738","kind":"field_change","created_at":"2026-05-01T18:06:39.17748775Z","actor":"Denis Kiselev","issue_id":"EV-gffv","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"merged"}}
|
|
259
|
+
{"id":"int-983c2fe6","kind":"field_change","created_at":"2026-05-02T02:47:27.113692488Z","actor":"Denis Kiselev","issue_id":"EV-3t8l","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Audit confirmed feature already fully implemented in current master: --fail-fast CLI flag, .evilution.yml key, FailFast validator, ResultNotifier trips at threshold, sequential+parallel strategies short-circuit, summary.truncated? indicator, reporter notices (CLI/HTML/JSON), full spec coverage across notifier/sequential/parallel/runner/parser. CI signal available via summary.truncated? in reports."}}
|
|
260
|
+
{"id":"int-1790a8b7","kind":"field_change","created_at":"2026-05-02T17:53:56.73309749Z","actor":"Denis Kiselev","issue_id":"EV-2gpj","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Audit confirmed refactor already done: ProcessCleanup.safe_kill / safe_wait helpers extracted to lib/evilution/process_cleanup.rb (lines 8-18), used by baseline.rb (lines 85,93,94) and parallel/work_queue/worker.rb. No Style/RescueModifier disables remain anywhere in lib/. bundle exec rubocop lib/evilution/baseline.rb clean. Existing specs pass."}}
|
data/.rubocop_todo.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.28.0] - 2026-05-03
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Operator profiles: `default` (current 72-operator set) and `strict` (adds aggressive truthiness mutators)** — pre-merge audits can opt into a more sensitive operator mix. The `strict` profile registers `PredicateToNil`, which replaces every `x.predicate?` call with `nil` to surface tests that only assert truthiness rather than exact return values. Wired through CLI (`--profile=strict`, `--strict` shortcut), `.evilution.yml` (`profile: strict`), and a new `Evilution::Mutator::Registry.for_profile(:default | :strict)` factory. `default` is unchanged, so existing CI runs are not affected (#920, PR #926)
|
|
8
|
+
- **Multi-file batch invocation documented** — `evilution path/a.rb path/b.rb path/c.rb` runs every file in a single Runner invocation so the framework (Rails, Sorbet, etc.) and the `preload` chain load **once** in the parent process. With `--isolation=fork` (default for Rails projects under `auto`), every per-mutation fork branches off the warmed parent — materially faster than `for f in ...; do bundle exec evilution run "$f"; done`. README now has a "5a. Multi-file batch scan" workflow section and an end-to-end runner spec covers two positional file paths; session save/load preserves per-file paths in `survived[].file` (#922, PR #927)
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`Compare::Normalizer` mis-classified Mutant payload lines whose mutated source started with `--` or `++` as unified-diff headers** — pre-existing bug in `extract_from_mutant_diff` that would, for example, drop a removed line `--flag` (emitted as `---flag` in the diff). The new `DiffExtractor::Mutant` requires a trailing space after `---`/`+++` to match a header, preserving real payload. Equivalent Evilution/Mutant mutations on such lines now hash identically and `compare` no longer reports false additions/removals (#917, PR #934)
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **Internal `Evilution::Compare::Fingerprint` SOLID refactor** — module-function form replaced with a class taking injectable `(extractor:, normalizer:)` collaborators and a `#call(diff:, file_path:, line:)` interface. Diff parsing extracted into `Evilution::Compare::DiffExtractor::{Evilution,Mutant}` strategy classes (one per format, common duck-typed interface), enabling open/closed extension for future tools without touching the orchestrator. `Compare::Normalizer` constructs both fingerprints once and reuses them across records (#917, PR #934)
|
|
17
|
+
- **`Evilution::Mutation` migrated to value-object composition** — sources, slice, parse status now Data.define-backed value objects (#822, PR #907)
|
|
18
|
+
- **`Evilution::Result::MutationResult` encapsulates memory and error state in dedicated Data.define value objects** — `MemoryStats` and `ErrorInfo` instead of flat positional fields (#823, PR #907)
|
|
19
|
+
- **`Reporter::Suggestion` registry/templates and the RSpec/Minitest template builders unified** — single `build` entrypoint per format (#824, PR #908; #849, PR #905; #850, PR #904)
|
|
20
|
+
- **`Reporter::HTML` namespace inlined into `report.rb`** — separate `namespace.rb` removed, autoload pattern adopted for sub-templates (#826, PR #909)
|
|
21
|
+
- **`Compare::LineNormalizer` extracted into its own class** — whitespace collapse separated from fingerprint orchestration (#829, PR #832)
|
|
22
|
+
- **`Evilution::Config` attribute assignment migrated to a transformation map** — single source of truth for type coercion across simple attributes (#830)
|
|
23
|
+
- **`Runner` `require` chain consolidated** — sub-component loading now centralized; circular-require pitfalls in `MutationExecutor` resolved with `Module#autoload` for child strategy/neutralizer files (#831)
|
|
24
|
+
- **Process cleanup helpers extracted into `Evilution::ProcessCleanup`** — `safe_kill(sig, pid)` and `safe_wait(pid)` shared by `Baseline`, `Isolation::Fork`, and `WorkQueue::Worker`, replacing scattered inline `rescue` modifiers swallowing `Errno::ESRCH`/`ECHILD` (#838)
|
|
25
|
+
- **`ProgressStreamer` and `Loop` error handling tightened** — generic `StandardError` rescues replaced with specific `Errno::EPIPE`/`Errno::EBADF`/etc.; once-only warning suppression added so a flood of failures cannot drown stderr (#827, #840)
|
|
26
|
+
- **Crash detector predicate methods renamed** — `has_*?` → `*?` per Ruby/RSpec conventions (`have_X` matcher calls `has_X?`; the renamed methods are still picked up by `be_X` matchers used in specs) (#839)
|
|
27
|
+
- **Rubocop hygiene sweep across 6 sites** — `Style/RescueModifier`, `Lint/UnusedMethodArgument`, `Lint/SuppressedException` (3 instances), `Security/Eval`, and `Security/MarshalLoad` (3 instances) inline disable comments removed in favor of either narrowed code, explanatory rescue-body comments, or main-`.rubocop.yml` per-file Excludes documented with the underlying trust boundary (#832, #833, #834, #835, #836, #837)
|
|
28
|
+
|
|
29
|
+
### Documentation
|
|
30
|
+
|
|
31
|
+
- **README "Operator Profiles" subsection** — explains the `default` vs `strict` profiles, how to opt in (CLI, config, shortcut), and what `strict` adds today (#920, PR #926)
|
|
32
|
+
- **README "5a. Multi-file batch scan" workflow** — documents Rails-loads-once amortisation and qualifies the speed claim by isolation mode (`fork` vs `in_process`) (#922, PR #927)
|
|
33
|
+
- **`.evilution.yml` template gained a `profile:` block** — generated by `evilution init` (#920, PR #926)
|
|
34
|
+
|
|
3
35
|
## [0.27.0] - 2026-04-26
|
|
4
36
|
|
|
5
37
|
### Added
|
data/README.md
CHANGED
|
@@ -118,6 +118,17 @@ The shorter alias `evil` ships alongside `evilution` and accepts identical argum
|
|
|
118
118
|
| `--fallback-full-suite` | Boolean | false | When no matching spec/test resolves for a mutation, run the whole test suite instead of marking it `:unresolved` and skipping. |
|
|
119
119
|
| `--baseline-session PATH` | String | _(none)_ | Saved session file for HTML report comparison. |
|
|
120
120
|
| `-e CODE`, `--eval CODE` | String | _(none)_ | Inline Ruby code for `util mutation` command. |
|
|
121
|
+
| `--profile NAME` | String | `default` | Operator profile: `default` or `strict`. `strict` adds aggressive truthiness mutators (e.g. replaces `x.predicate?` with `nil`) intended for pre-merge audits. |
|
|
122
|
+
| `--strict` | Boolean | false | Shortcut for `--profile=strict`. |
|
|
123
|
+
|
|
124
|
+
### Operator Profiles
|
|
125
|
+
|
|
126
|
+
Two profiles ship out of the box:
|
|
127
|
+
|
|
128
|
+
- **`default`** — the 72 stable operators registered in `Mutator::Registry.default`. Suitable for everyday CI runs; balances coverage signal against survivor noise.
|
|
129
|
+
- **`strict`** — adds extra truthiness mutators on top of `default`. Currently `PredicateToNil` (replaces every `x.predicate?` call with `nil` to surface tests that only assert truthiness rather than exact return values). Use for pre-merge audits where you want maximum sensitivity at the cost of more survivors.
|
|
130
|
+
|
|
131
|
+
Set via `--profile=strict`, the `--strict` shortcut, or `profile: strict` in `.evilution.yml`.
|
|
121
132
|
|
|
122
133
|
### Exit Codes
|
|
123
134
|
|
|
@@ -484,6 +495,14 @@ bundle exec evilution run lib/specific_file.rb --format json
|
|
|
484
495
|
|
|
485
496
|
Use when you know which file was modified and want to verify its test coverage.
|
|
486
497
|
|
|
498
|
+
### 5a. Multi-file batch scan
|
|
499
|
+
|
|
500
|
+
```bash
|
|
501
|
+
bundle exec evilution run lib/models/user.rb lib/models/account.rb lib/models/order.rb
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
Pass multiple file paths on a single invocation to amortise startup cost. The framework (Rails, Sorbet, etc.) and the `preload` chain (`spec/rails_helper.rb` → `spec/spec_helper.rb` → `test/test_helper.rb`) load **once** in the parent process. When `--isolation=fork` is selected (the default `--isolation=auto` resolves to `fork` on Rails projects), every subsequent mutation across all files forks from that warmed parent — materially faster than scripting a `for f in ...; do bundle exec evilution run "$f"; done` loop, which pays the bootstrap per file. With `--isolation=in_process` (default for non-Rails projects under `auto`), there is no per-mutation fork, but the parent-process boot still runs once instead of N times. Per-file paths and line numbers are preserved in the report (`survived[].file`, HTML grouping by source file).
|
|
505
|
+
|
|
487
506
|
### 6. Fixing surviving mutants
|
|
488
507
|
|
|
489
508
|
For each entry in `survived[]`:
|
data/lib/evilution/baseline.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "spec_resolver"
|
|
4
|
+
require_relative "process_cleanup"
|
|
4
5
|
|
|
5
6
|
class Evilution::Baseline
|
|
6
7
|
Result = Struct.new(:failed_spec_files, :duration) do
|
|
@@ -72,7 +73,7 @@ class Evilution::Baseline
|
|
|
72
73
|
Process.wait(pid)
|
|
73
74
|
return false if data.empty?
|
|
74
75
|
|
|
75
|
-
result = Marshal.load(data)
|
|
76
|
+
result = Marshal.load(data)
|
|
76
77
|
result[:passed]
|
|
77
78
|
else
|
|
78
79
|
terminate_child(pid)
|
|
@@ -81,7 +82,7 @@ class Evilution::Baseline
|
|
|
81
82
|
end
|
|
82
83
|
|
|
83
84
|
def terminate_child(pid)
|
|
84
|
-
|
|
85
|
+
Evilution::ProcessCleanup.safe_kill("TERM", pid)
|
|
85
86
|
_, status = Process.waitpid2(pid, Process::WNOHANG)
|
|
86
87
|
return if status
|
|
87
88
|
|
|
@@ -89,8 +90,8 @@ class Evilution::Baseline
|
|
|
89
90
|
_, status = Process.waitpid2(pid, Process::WNOHANG)
|
|
90
91
|
return if status
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
Evilution::ProcessCleanup.safe_kill("KILL", pid)
|
|
94
|
+
Evilution::ProcessCleanup.safe_wait(pid)
|
|
94
95
|
end
|
|
95
96
|
|
|
96
97
|
private
|
|
@@ -21,6 +21,7 @@ class Evilution::CLI::Parser::OptionsBuilder
|
|
|
21
21
|
add_core_options(opts)
|
|
22
22
|
add_filter_options(opts)
|
|
23
23
|
add_flag_options(opts)
|
|
24
|
+
add_profile_options(opts)
|
|
24
25
|
add_extra_flag_options(opts)
|
|
25
26
|
add_session_options(opts)
|
|
26
27
|
add_compare_options(opts)
|
|
@@ -95,6 +96,12 @@ class Evilution::CLI::Parser::OptionsBuilder
|
|
|
95
96
|
end
|
|
96
97
|
end
|
|
97
98
|
|
|
99
|
+
def add_profile_options(opts)
|
|
100
|
+
opts.on("--profile NAME", "Operator profile: default, strict (default: default). " \
|
|
101
|
+
"strict adds aggressive truthiness mutators for pre-merge audits.") { |p| @options[:profile] = p }
|
|
102
|
+
opts.on("--strict", "Shortcut for --profile=strict") { @options[:profile] = "strict" }
|
|
103
|
+
end
|
|
104
|
+
|
|
98
105
|
def add_extra_flag_options(opts)
|
|
99
106
|
opts.on("--skip-heredoc-literals", "Skip all string literal mutations inside heredocs") { @options[:skip_heredoc_literals] = true }
|
|
100
107
|
opts.on("--related-specs-heuristic", "Append related request/integration/feature/system specs for includes() mutations") do
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../diff_extractor"
|
|
4
|
+
|
|
5
|
+
# Extracts {minus:, plus:} payload arrays from Evilution-format diffs.
|
|
6
|
+
# Evilution diffs use "- " / "+ " line prefixes (note the trailing space) and
|
|
7
|
+
# do not carry unified-diff headers or hunk markers.
|
|
8
|
+
class Evilution::Compare::DiffExtractor::Evilution
|
|
9
|
+
def call(diff)
|
|
10
|
+
minus = []
|
|
11
|
+
plus = []
|
|
12
|
+
diff.to_s.each_line do |line|
|
|
13
|
+
line = line.chomp
|
|
14
|
+
if line.start_with?("- ")
|
|
15
|
+
minus << line[2..]
|
|
16
|
+
elsif line.start_with?("+ ")
|
|
17
|
+
plus << line[2..]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
{ minus: minus, plus: plus }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../diff_extractor"
|
|
4
|
+
|
|
5
|
+
# Extracts {minus:, plus:} payload arrays from Mutant unified-diff format.
|
|
6
|
+
# Skips the "--- <name>", "+++ <name>", and "@@ ... @@" header lines and
|
|
7
|
+
# returns each remaining payload line with its single leading "-" or "+"
|
|
8
|
+
# marker stripped.
|
|
9
|
+
#
|
|
10
|
+
# Header detection requires a trailing space after "---"/"+++" so that a
|
|
11
|
+
# payload line whose mutated source starts with "--" (emitted as "---var")
|
|
12
|
+
# or "++" (emitted as "+++var") is preserved rather than misclassified as
|
|
13
|
+
# a header.
|
|
14
|
+
class Evilution::Compare::DiffExtractor::Mutant
|
|
15
|
+
def call(diff)
|
|
16
|
+
minus = []
|
|
17
|
+
plus = []
|
|
18
|
+
diff.to_s.each_line do |line|
|
|
19
|
+
line = line.chomp
|
|
20
|
+
next if line.start_with?("--- ", "+++ ", "@@")
|
|
21
|
+
|
|
22
|
+
if line.start_with?("-")
|
|
23
|
+
minus << line[1..]
|
|
24
|
+
elsif line.start_with?("+")
|
|
25
|
+
plus << line[1..]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
{ minus: minus, plus: plus }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -3,80 +3,23 @@
|
|
|
3
3
|
require "digest"
|
|
4
4
|
require_relative "../compare"
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
plus << line[2..]
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
{ minus: minus, plus: plus }
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def extract_from_mutant_diff(diff)
|
|
24
|
-
minus = []
|
|
25
|
-
plus = []
|
|
26
|
-
diff.to_s.each_line do |line|
|
|
27
|
-
line = line.chomp
|
|
28
|
-
next if line.start_with?("---", "+++", "@@")
|
|
29
|
-
|
|
30
|
-
if line.start_with?("-")
|
|
31
|
-
minus << line[1..]
|
|
32
|
-
elsif line.start_with?("+")
|
|
33
|
-
plus << line[1..]
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
{ minus: minus, plus: plus }
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# v1 limitation: only " and ' literals are preserved. Regex literals (/.../),
|
|
40
|
-
# heredocs, %w[], %q{} forms are treated as ordinary code — whitespace runs
|
|
41
|
-
# inside them collapse. A mutation touching whitespace inside a regex may
|
|
42
|
-
# false-match across tools.
|
|
43
|
-
# rubocop:disable Metrics/PerceivedComplexity, Style/MultipleComparison
|
|
44
|
-
def normalize_line(line)
|
|
45
|
-
out = +""
|
|
46
|
-
i = 0
|
|
47
|
-
in_literal = nil
|
|
48
|
-
last_was_space = false
|
|
49
|
-
chars = line.chars
|
|
50
|
-
while i < chars.length
|
|
51
|
-
ch = chars[i]
|
|
52
|
-
if in_literal
|
|
53
|
-
out << ch
|
|
54
|
-
if ch == "\\" && i + 1 < chars.length
|
|
55
|
-
out << chars[i + 1]
|
|
56
|
-
i += 2
|
|
57
|
-
next
|
|
58
|
-
end
|
|
59
|
-
in_literal = nil if ch == in_literal
|
|
60
|
-
elsif ch == '"' || ch == "'"
|
|
61
|
-
in_literal = ch
|
|
62
|
-
out << ch
|
|
63
|
-
last_was_space = false
|
|
64
|
-
elsif ch == " " || ch == "\t"
|
|
65
|
-
out << " " unless last_was_space || out.empty?
|
|
66
|
-
last_was_space = true
|
|
67
|
-
else
|
|
68
|
-
out << ch
|
|
69
|
-
last_was_space = false
|
|
70
|
-
end
|
|
71
|
-
i += 1
|
|
72
|
-
end
|
|
73
|
-
out.rstrip
|
|
6
|
+
# Composes a stable SHA256 fingerprint from a mutation diff for cross-tool
|
|
7
|
+
# matching (Evilution vs Mutant). Orchestrates two collaborators along
|
|
8
|
+
# distinct change axes:
|
|
9
|
+
#
|
|
10
|
+
# - extractor: parses a tool-specific diff format into {minus:, plus:}
|
|
11
|
+
# - normalizer: collapses whitespace per line so cosmetic differences
|
|
12
|
+
# don't perturb the hash
|
|
13
|
+
class Evilution::Compare::Fingerprint
|
|
14
|
+
def initialize(extractor:, normalizer:)
|
|
15
|
+
@extractor = extractor
|
|
16
|
+
@normalizer = normalizer
|
|
74
17
|
end
|
|
75
|
-
# rubocop:enable Metrics/PerceivedComplexity, Style/MultipleComparison
|
|
76
18
|
|
|
77
|
-
def
|
|
78
|
-
|
|
79
|
-
|
|
19
|
+
def call(diff:, file_path:, line:)
|
|
20
|
+
body = @extractor.call(diff)
|
|
21
|
+
minus = body[:minus].map { |l| @normalizer.call(l) }
|
|
22
|
+
plus = body[:plus].map { |l| @normalizer.call(l) }
|
|
80
23
|
payload = [file_path, line.to_s, minus.join("\n"), plus.join("\n")].join("\x00")
|
|
81
24
|
Digest::SHA256.hexdigest(payload)
|
|
82
25
|
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../compare"
|
|
4
|
+
|
|
5
|
+
# Collapses whitespace runs in source-code text while preserving the contents
|
|
6
|
+
# of "..." and '...' string literals. Used for fingerprinting mutation diffs
|
|
7
|
+
# so that whitespace-only differences do not cause false fingerprint mismatches
|
|
8
|
+
# across tooling (evilution vs mutant).
|
|
9
|
+
#
|
|
10
|
+
# v1 limitation: only " and ' literals are preserved. Regex literals (/.../),
|
|
11
|
+
# heredocs, %w[], %q{} forms are treated as ordinary code — whitespace runs
|
|
12
|
+
# inside them collapse. A mutation touching whitespace inside a regex may
|
|
13
|
+
# false-match across tools.
|
|
14
|
+
class Evilution::Compare::LineNormalizer
|
|
15
|
+
QUOTES = ['"', "'"].freeze
|
|
16
|
+
WHITESPACE = [" ", "\t"].freeze
|
|
17
|
+
private_constant :QUOTES, :WHITESPACE
|
|
18
|
+
|
|
19
|
+
def call(line)
|
|
20
|
+
@chars = line.chars
|
|
21
|
+
@i = 0
|
|
22
|
+
@out = +""
|
|
23
|
+
@in_literal = nil
|
|
24
|
+
@last_was_space = false
|
|
25
|
+
|
|
26
|
+
@i += step while @i < @chars.length
|
|
27
|
+
result = @out.rstrip
|
|
28
|
+
@chars = nil
|
|
29
|
+
@out = nil
|
|
30
|
+
result
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def step
|
|
36
|
+
ch = @chars[@i]
|
|
37
|
+
return step_in_literal(ch) if @in_literal
|
|
38
|
+
return step_open_quote(ch) if QUOTES.include?(ch)
|
|
39
|
+
return step_whitespace if WHITESPACE.include?(ch)
|
|
40
|
+
|
|
41
|
+
append_regular(ch)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def step_in_literal(ch)
|
|
45
|
+
@out << ch
|
|
46
|
+
if ch == "\\" && @i + 1 < @chars.length
|
|
47
|
+
@out << @chars[@i + 1]
|
|
48
|
+
return 2
|
|
49
|
+
end
|
|
50
|
+
@in_literal = nil if ch == @in_literal
|
|
51
|
+
1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def step_open_quote(ch)
|
|
55
|
+
@in_literal = ch
|
|
56
|
+
@out << ch
|
|
57
|
+
@last_was_space = false
|
|
58
|
+
1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def step_whitespace
|
|
62
|
+
@out << " " unless @last_was_space || @out.empty?
|
|
63
|
+
@last_was_space = true
|
|
64
|
+
1
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def append_regular(ch)
|
|
68
|
+
@out << ch
|
|
69
|
+
@last_was_space = false
|
|
70
|
+
1
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require_relative "../compare"
|
|
4
4
|
require_relative "record"
|
|
5
5
|
require_relative "fingerprint"
|
|
6
|
+
require_relative "line_normalizer"
|
|
7
|
+
require_relative "diff_extractor/evilution"
|
|
8
|
+
require_relative "diff_extractor/mutant"
|
|
6
9
|
|
|
7
10
|
class Evilution::Compare::Normalizer
|
|
8
11
|
EVILUTION_BUCKETS = %w[killed survived timed_out errors neutral equivalent unresolved unparseable].freeze
|
|
@@ -17,6 +20,18 @@ class Evilution::Compare::Normalizer
|
|
|
17
20
|
"unparseable" => :unparseable
|
|
18
21
|
}.freeze
|
|
19
22
|
|
|
23
|
+
def initialize
|
|
24
|
+
line_normalizer = Evilution::Compare::LineNormalizer.new
|
|
25
|
+
@evilution_fingerprint = Evilution::Compare::Fingerprint.new(
|
|
26
|
+
extractor: Evilution::Compare::DiffExtractor::Evilution.new,
|
|
27
|
+
normalizer: line_normalizer
|
|
28
|
+
)
|
|
29
|
+
@mutant_fingerprint = Evilution::Compare::Fingerprint.new(
|
|
30
|
+
extractor: Evilution::Compare::DiffExtractor::Mutant.new,
|
|
31
|
+
normalizer: line_normalizer
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
20
35
|
def from_evilution(json)
|
|
21
36
|
records = []
|
|
22
37
|
EVILUTION_BUCKETS.each do |bucket|
|
|
@@ -47,13 +62,12 @@ class Evilution::Compare::Normalizer
|
|
|
47
62
|
diff = entry["diff"].to_s
|
|
48
63
|
status = EVILUTION_STATUS_MAP[entry["status"]] ||
|
|
49
64
|
raise(Evilution::Compare::InvalidInput.new("unknown status #{entry["status"].inspect}", index: index))
|
|
50
|
-
body = Evilution::Compare::Fingerprint.extract_from_evilution_diff(diff)
|
|
51
65
|
Evilution::Compare::Record.new(
|
|
52
66
|
source: :evilution,
|
|
53
67
|
file_path: file_path,
|
|
54
68
|
line: line,
|
|
55
69
|
status: status,
|
|
56
|
-
fingerprint:
|
|
70
|
+
fingerprint: @evilution_fingerprint.call(diff: diff, file_path: file_path, line: line),
|
|
57
71
|
operator: entry["operator"],
|
|
58
72
|
diff_body: diff,
|
|
59
73
|
raw: entry
|
|
@@ -67,13 +81,12 @@ class Evilution::Compare::Normalizer
|
|
|
67
81
|
line = parse_mutant_line(ident, index)
|
|
68
82
|
diff = mr["mutation_diff"].to_s
|
|
69
83
|
status = derive_mutant_status(mr, cr, index)
|
|
70
|
-
body = Evilution::Compare::Fingerprint.extract_from_mutant_diff(diff)
|
|
71
84
|
Evilution::Compare::Record.new(
|
|
72
85
|
source: :mutant,
|
|
73
86
|
file_path: source_path,
|
|
74
87
|
line: line,
|
|
75
88
|
status: status,
|
|
76
|
-
fingerprint:
|
|
89
|
+
fingerprint: @mutant_fingerprint.call(diff: diff, file_path: source_path, line: line),
|
|
77
90
|
operator: nil,
|
|
78
91
|
diff_body: diff,
|
|
79
92
|
raw: { "mutation_result" => mr, "criteria_result" => cr, "source_path" => source_path }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
class Evilution::Config::Validators::Profile < Evilution::Config::Validators::Base
|
|
6
|
+
ALLOWED = %i[default strict].freeze
|
|
7
|
+
|
|
8
|
+
def self.call(value)
|
|
9
|
+
coerce_symbol!(value, allowed: ALLOWED, name: "profile")
|
|
10
|
+
end
|
|
11
|
+
end
|
data/lib/evilution/config.rb
CHANGED
|
@@ -17,7 +17,8 @@ class Evilution::Config
|
|
|
17
17
|
spec_mappings: {}, spec_pattern: nil, example_targeting: true,
|
|
18
18
|
example_targeting_fallback: :full_file,
|
|
19
19
|
example_targeting_cache: { max_files: 50, max_blocks: 10_000 },
|
|
20
|
-
quiet_children: false, quiet_children_dir: "tmp/evilution_children"
|
|
20
|
+
quiet_children: false, quiet_children_dir: "tmp/evilution_children",
|
|
21
|
+
profile: :default
|
|
21
22
|
}.freeze
|
|
22
23
|
|
|
23
24
|
attr_reader :target_files, :timeout, :format,
|
|
@@ -28,7 +29,7 @@ class Evilution::Config
|
|
|
28
29
|
:skip_heredoc_literals, :related_specs_heuristic,
|
|
29
30
|
:fallback_to_full_suite, :preload, :spec_mappings, :spec_pattern,
|
|
30
31
|
:example_targeting, :example_targeting_fallback, :example_targeting_cache,
|
|
31
|
-
:spec_selector, :quiet_children, :quiet_children_dir
|
|
32
|
+
:spec_selector, :quiet_children, :quiet_children_dir, :profile
|
|
32
33
|
|
|
33
34
|
def initialize(**options)
|
|
34
35
|
skip_file = options.delete(:skip_config_file) ? true : false
|
|
@@ -183,6 +184,11 @@ class Evilution::Config
|
|
|
183
184
|
# ignore_patterns:
|
|
184
185
|
# - "call{name=info, receiver=call{name=logger}}"
|
|
185
186
|
# - "call{name=debug|warn}"
|
|
187
|
+
|
|
188
|
+
# Operator profile: default or strict (default: default).
|
|
189
|
+
# strict adds aggressive truthiness mutators (e.g. replaces
|
|
190
|
+
# `x.predicate?` with `nil`) intended for pre-merge audits.
|
|
191
|
+
# profile: default
|
|
186
192
|
YAML
|
|
187
193
|
end
|
|
188
194
|
|
|
@@ -200,28 +206,37 @@ class Evilution::Config
|
|
|
200
206
|
)
|
|
201
207
|
end
|
|
202
208
|
|
|
209
|
+
SIMPLE_ATTR_TRANSFORMS = {
|
|
210
|
+
target_files: ->(v) { Array(v) },
|
|
211
|
+
timeout: nil,
|
|
212
|
+
format: :to_sym.to_proc,
|
|
213
|
+
target: nil,
|
|
214
|
+
min_score: :to_f.to_proc,
|
|
215
|
+
verbose: nil,
|
|
216
|
+
quiet: nil,
|
|
217
|
+
baseline: nil,
|
|
218
|
+
incremental: nil,
|
|
219
|
+
suggest_tests: nil,
|
|
220
|
+
progress: nil,
|
|
221
|
+
save_session: nil,
|
|
222
|
+
line_ranges: ->(v) { v || {} },
|
|
223
|
+
spec_files: ->(v) { Array(v) },
|
|
224
|
+
show_disabled: nil,
|
|
225
|
+
baseline_session: nil,
|
|
226
|
+
skip_heredoc_literals: nil,
|
|
227
|
+
related_specs_heuristic: nil,
|
|
228
|
+
fallback_to_full_suite: nil,
|
|
229
|
+
quiet_children: nil,
|
|
230
|
+
quiet_children_dir: nil
|
|
231
|
+
}.freeze
|
|
232
|
+
private_constant :SIMPLE_ATTR_TRANSFORMS
|
|
233
|
+
|
|
203
234
|
def assign_simple_attributes(merged)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
@verbose = merged[:verbose]
|
|
210
|
-
@quiet = merged[:quiet]
|
|
211
|
-
@baseline = merged[:baseline]
|
|
212
|
-
@incremental = merged[:incremental]
|
|
213
|
-
@suggest_tests = merged[:suggest_tests]
|
|
214
|
-
@progress = merged[:progress]
|
|
215
|
-
@save_session = merged[:save_session]
|
|
216
|
-
@line_ranges = merged[:line_ranges] || {}
|
|
217
|
-
@spec_files = Array(merged[:spec_files])
|
|
218
|
-
@show_disabled = merged[:show_disabled]
|
|
219
|
-
@baseline_session = merged[:baseline_session]
|
|
220
|
-
@skip_heredoc_literals = merged[:skip_heredoc_literals]
|
|
221
|
-
@related_specs_heuristic = merged[:related_specs_heuristic]
|
|
222
|
-
@fallback_to_full_suite = merged[:fallback_to_full_suite]
|
|
223
|
-
@quiet_children = merged[:quiet_children]
|
|
224
|
-
@quiet_children_dir = merged[:quiet_children_dir]
|
|
235
|
+
SIMPLE_ATTR_TRANSFORMS.each do |key, transform|
|
|
236
|
+
value = merged[key]
|
|
237
|
+
value = transform.call(value) if transform
|
|
238
|
+
instance_variable_set(:"@#{key}", value)
|
|
239
|
+
end
|
|
225
240
|
end
|
|
226
241
|
|
|
227
242
|
def assign_validated_attributes(merged)
|
|
@@ -234,6 +249,7 @@ class Evilution::Config
|
|
|
234
249
|
@preload = Validators::Preload.call(merged[:preload])
|
|
235
250
|
@spec_mappings = Validators::SpecMappings.call(merged[:spec_mappings])
|
|
236
251
|
@spec_pattern = Validators::SpecPattern.call(merged[:spec_pattern])
|
|
252
|
+
@profile = Validators::Profile.call(merged[:profile])
|
|
237
253
|
end
|
|
238
254
|
|
|
239
255
|
def assign_example_targeting(merged)
|
|
@@ -259,6 +275,7 @@ require_relative "config/validators/spec_pattern"
|
|
|
259
275
|
require_relative "config/validators/spec_mappings"
|
|
260
276
|
require_relative "config/validators/example_targeting_fallback"
|
|
261
277
|
require_relative "config/validators/example_targeting_cache"
|
|
278
|
+
require_relative "config/validators/profile"
|
|
262
279
|
require_relative "config/builders"
|
|
263
280
|
require_relative "config/builders/spec_resolver"
|
|
264
281
|
require_relative "config/builders/spec_selector"
|
|
@@ -26,11 +26,11 @@ class Evilution::Integration::CrashDetector
|
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def
|
|
29
|
+
def assertion_failure?
|
|
30
30
|
@assertion_failures.positive?
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
def
|
|
33
|
+
def crashed?
|
|
34
34
|
@crashes.any?
|
|
35
35
|
end
|
|
36
36
|
|
|
@@ -5,11 +5,15 @@ require_relative "../loading"
|
|
|
5
5
|
# Evaluate source with __FILE__ set to the absolute original path so that
|
|
6
6
|
# `require_relative` and `__dir__` resolve against the real source tree, where
|
|
7
7
|
# sibling files actually exist.
|
|
8
|
+
#
|
|
9
|
+
# Trust boundary: `source` is never user-supplied. It is always the original
|
|
10
|
+
# on-disk source from a file the user already pointed Evilution at, with
|
|
11
|
+
# byte-level mutations applied by AST::SourceSurgeon. The only difference
|
|
12
|
+
# between this eval path and a plain `require` of the same file is that we
|
|
13
|
+
# substitute the mutated bytes — the privilege level is identical.
|
|
8
14
|
class Evilution::Integration::Loading::SourceEvaluator
|
|
9
15
|
def call(source, file_path)
|
|
10
16
|
absolute = File.expand_path(file_path)
|
|
11
|
-
# rubocop:disable Security/Eval
|
|
12
17
|
eval(source, TOPLEVEL_BINDING, absolute, 1)
|
|
13
|
-
# rubocop:enable Security/Eval
|
|
14
18
|
end
|
|
15
19
|
end
|
|
@@ -34,11 +34,11 @@ class Evilution::Integration::MinitestCrashDetector
|
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
def
|
|
37
|
+
def assertion_failure?
|
|
38
38
|
@assertion_failures.positive?
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
def
|
|
41
|
+
def crashed?
|
|
42
42
|
@crashes.any?
|
|
43
43
|
end
|
|
44
44
|
|