evilution 0.26.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 +23 -0
- data/.rubocop_todo.yml +6 -0
- data/CHANGELOG.md +54 -0
- data/README.md +76 -3
- data/lib/evilution/baseline.rb +5 -4
- data/lib/evilution/cache.rb +2 -0
- data/lib/evilution/child_output.rb +24 -0
- data/lib/evilution/cli/commands/run.rb +9 -0
- data/lib/evilution/cli/commands/version.rb +2 -0
- data/lib/evilution/cli/parser/options_builder.rb +23 -2
- 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/builders/spec_resolver.rb +15 -0
- data/lib/evilution/config/builders/spec_selector.rb +16 -0
- data/lib/evilution/config/builders.rb +4 -0
- data/lib/evilution/config/env_loader.rb +12 -0
- data/lib/evilution/config/file_loader.rb +22 -0
- data/lib/evilution/config/sources.rb +14 -0
- data/lib/evilution/config/validators/base.rb +37 -0
- data/lib/evilution/config/validators/example_targeting_cache.rb +37 -0
- data/lib/evilution/config/validators/example_targeting_fallback.rb +22 -0
- data/lib/evilution/config/validators/fail_fast.rb +11 -0
- data/lib/evilution/config/validators/hooks.rb +12 -0
- data/lib/evilution/config/validators/ignore_patterns.rb +16 -0
- data/lib/evilution/config/validators/integration.rb +11 -0
- data/lib/evilution/config/validators/isolation.rb +19 -0
- data/lib/evilution/config/validators/jobs.rb +9 -0
- data/lib/evilution/config/validators/preload.rb +13 -0
- data/lib/evilution/config/validators/profile.rb +11 -0
- data/lib/evilution/config/validators/spec_mappings.rb +56 -0
- data/lib/evilution/config/validators/spec_pattern.rb +12 -0
- data/lib/evilution/config/validators.rb +4 -0
- data/lib/evilution/config.rb +93 -266
- data/lib/evilution/feedback/detector.rb +15 -0
- data/lib/evilution/feedback/messages.rb +42 -0
- data/lib/evilution/feedback.rb +5 -0
- 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/baseline_runner.rb +16 -0
- data/lib/evilution/integration/rspec/crash_detector_lifecycle.rb +17 -0
- data/lib/evilution/integration/rspec/example_filter_applier.rb +21 -0
- data/lib/evilution/integration/rspec/framework_loader.rb +28 -0
- data/lib/evilution/integration/rspec/result_builder.rb +40 -0
- data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +28 -0
- data/lib/evilution/integration/rspec/state_guard/internals.rb +19 -0
- data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +43 -0
- data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +32 -0
- data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +20 -0
- data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +20 -0
- data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +20 -0
- data/lib/evilution/integration/rspec/state_guard.rb +40 -0
- data/lib/evilution/integration/rspec/test_file_resolver.rb +30 -0
- data/lib/evilution/integration/rspec/unresolved_spec_warner.rb +18 -0
- data/lib/evilution/integration/rspec.rb +61 -232
- data/lib/evilution/isolation/fork.rb +23 -13
- data/lib/evilution/isolation/in_process.rb +10 -6
- data/lib/evilution/mcp/info_tool/actions/base.rb +22 -0
- data/lib/evilution/mcp/info_tool/actions/environment.rb +42 -0
- data/lib/evilution/mcp/info_tool/actions/feedback.rb +16 -0
- data/lib/evilution/mcp/info_tool/actions/statuses.rb +10 -0
- data/lib/evilution/mcp/info_tool/actions/subjects.rb +47 -0
- data/lib/evilution/mcp/info_tool/actions/tests.rb +60 -0
- data/lib/evilution/mcp/info_tool/actions.rb +16 -0
- data/lib/evilution/mcp/info_tool/config_factory.rb +24 -0
- data/lib/evilution/mcp/info_tool/error_mapper.rb +15 -0
- data/lib/evilution/mcp/info_tool/request_parser.rb +34 -0
- data/lib/evilution/mcp/info_tool/response_formatter.rb +24 -0
- data/lib/evilution/mcp/info_tool/status_glossary.rb +75 -0
- data/lib/evilution/mcp/info_tool.rb +43 -263
- data/lib/evilution/mcp/mutate_tool/error_payload.rb +8 -1
- data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +5 -1
- data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +13 -1
- data/lib/evilution/mcp/mutate_tool.rb +5 -2
- 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/block_removal.rb +1 -1
- data/lib/evilution/mutator/operator/method_body_replacement.rb +18 -2
- 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 +25 -0
- data/lib/evilution/parallel/work_queue/channel.rb +23 -0
- data/lib/evilution/parallel/work_queue/collection_state.rb +14 -0
- data/lib/evilution/parallel/work_queue/dispatcher.rb +133 -0
- data/lib/evilution/parallel/work_queue/validators/optional_positive_int.rb +11 -0
- data/lib/evilution/parallel/work_queue/validators/optional_positive_number.rb +11 -0
- data/lib/evilution/parallel/work_queue/validators/positive_int.rb +11 -0
- data/lib/evilution/parallel/work_queue/validators.rb +6 -0
- data/lib/evilution/parallel/work_queue/worker/loop.rb +45 -0
- data/lib/evilution/parallel/work_queue/worker.rb +114 -0
- data/lib/evilution/parallel/work_queue/worker_stat.rb +17 -0
- data/lib/evilution/parallel/work_queue.rb +42 -327
- data/lib/evilution/process_cleanup.rb +19 -0
- data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +18 -0
- data/lib/evilution/reporter/cli/item_formatters/disabled.rb +9 -0
- data/lib/evilution/reporter/cli/item_formatters/error.rb +14 -0
- data/lib/evilution/reporter/cli/item_formatters/result_location.rb +10 -0
- data/lib/evilution/reporter/cli/item_formatters.rb +6 -0
- data/lib/evilution/reporter/cli/line_formatters/duration.rb +9 -0
- data/lib/evilution/reporter/cli/line_formatters/efficiency.rb +18 -0
- data/lib/evilution/reporter/cli/line_formatters/feedback_footer.rb +13 -0
- data/lib/evilution/reporter/cli/line_formatters/header.rb +10 -0
- data/lib/evilution/reporter/cli/line_formatters/mutations.rb +16 -0
- data/lib/evilution/reporter/cli/line_formatters/peak_memory.rb +12 -0
- data/lib/evilution/reporter/cli/line_formatters/result_line.rb +20 -0
- data/lib/evilution/reporter/cli/line_formatters/score.rb +14 -0
- data/lib/evilution/reporter/cli/line_formatters/truncation_notice.rb +11 -0
- data/lib/evilution/reporter/cli/line_formatters.rb +6 -0
- data/lib/evilution/reporter/cli/metrics_block.rb +26 -0
- data/lib/evilution/reporter/cli/pct.rb +9 -0
- data/lib/evilution/reporter/cli/section.rb +13 -0
- data/lib/evilution/reporter/cli/section_renderer.rb +15 -0
- data/lib/evilution/reporter/cli/trailer.rb +22 -0
- data/lib/evilution/reporter/cli.rb +79 -162
- 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 +10 -4
- data/lib/evilution/runner/mutation_executor/mutation_runner.rb +30 -0
- data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +15 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +39 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +68 -0
- data/lib/evilution/runner/mutation_executor/neutralizer.rb +11 -0
- data/lib/evilution/runner/mutation_executor/result_cache.rb +67 -0
- data/lib/evilution/runner/mutation_executor/result_notifier.rb +46 -0
- data/lib/evilution/runner/mutation_executor/result_packer.rb +41 -0
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +78 -0
- data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +32 -0
- data/lib/evilution/runner/mutation_executor/strategy.rb +11 -0
- data/lib/evilution/runner/mutation_executor.rb +53 -292
- 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 +53 -30
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +1 -0
- data/script/memory_check +3 -1
- metadata +125 -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
|
@@ -235,3 +235,26 @@
|
|
|
235
235
|
{"id":"int-2abceed3","kind":"field_change","created_at":"2026-04-24T05:18:13.096916772Z","actor":"Denis Kiselev","issue_id":"EV-kjac","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
236
236
|
{"id":"int-03bc2f7f","kind":"field_change","created_at":"2026-04-24T05:40:10.83959826Z","actor":"Denis Kiselev","issue_id":"EV-hklf","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
237
237
|
{"id":"int-2781044c","kind":"field_change","created_at":"2026-04-24T05:40:11.975030418Z","actor":"Denis Kiselev","issue_id":"EV-cpku","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Folded into EV-hklf — error rename 'no method found' → 'no subject matched' shipped in PR #872."}}
|
|
238
|
+
{"id":"int-3fd6fda2","kind":"field_change","created_at":"2026-04-24T17:15:49.060178577Z","actor":"Denis Kiselev","issue_id":"EV-3ew5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"PR #884 merged to master"}}
|
|
239
|
+
{"id":"int-95f8d2f1","kind":"field_change","created_at":"2026-04-25T14:49:14.844074734Z","actor":"Denis Kiselev","issue_id":"EV-owgh","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #890"}}
|
|
240
|
+
{"id":"int-d786895b","kind":"field_change","created_at":"2026-04-25T15:46:53.977607499Z","actor":"Denis Kiselev","issue_id":"EV-a6de","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #891"}}
|
|
241
|
+
{"id":"int-a297634a","kind":"field_change","created_at":"2026-04-25T16:56:08.483381206Z","actor":"Denis Kiselev","issue_id":"EV-ilu3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #892"}}
|
|
242
|
+
{"id":"int-0647d42f","kind":"field_change","created_at":"2026-04-25T17:06:39.083006592Z","actor":"Denis Kiselev","issue_id":"EV-aa4x","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #893"}}
|
|
243
|
+
{"id":"int-86ea8eca","kind":"field_change","created_at":"2026-04-25T17:28:36.516431743Z","actor":"Denis Kiselev","issue_id":"EV-6x6g","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #894"}}
|
|
244
|
+
{"id":"int-811fef5f","kind":"field_change","created_at":"2026-04-25T17:39:31.325092593Z","actor":"Denis Kiselev","issue_id":"EV-6c51","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #895"}}
|
|
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
|
+
{"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
|
+
{"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
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
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
|
+
|
|
35
|
+
## [0.27.0] - 2026-04-26
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- **`prism` declared as a runtime dependency (`>= 1.5, < 2`)** — Rails 7.1 stacks pin `prism 0.19` (which lacks `IfNode#subsequent`), causing a `NoMethodError` on the first `if` evilution mutated. Bundler now refuses incompatible prism versions at install time instead of crashing at runtime (#876, PR #891)
|
|
40
|
+
- **`--[no-]incremental` CLI flag** — `--no-incremental` overrides `incremental: true` from the config file for one invocation (cold-cache debugging, CI escape hatch). Last flag wins when both forms are given (#878, PR #897)
|
|
41
|
+
- **`--quiet-children` and `--quiet-children-dir DIR` flags** — redirect each forked worker's stdout/stderr to per-pid files under `tmp/evilution_children/<pid>.{out,err}` (configurable). Keeps parent output clean when app initializers (Datadog, Bullet, etc.) emit warnings on every fork. Trade-off: live worker errors only appear in the side files (`tail -f tmp/evilution_children/*.err`) (#880, PR #899)
|
|
42
|
+
- **Preload autodetect chain extended** — `Runner::IsolationResolver` now probes `spec/rails_helper.rb` → `spec/spec_helper.rb` → `test/test_helper.rb` (was: rails_helper + test_helper only). Rails projects that consolidate everything into `spec/spec_helper.rb` no longer need an explicit `preload:` setting. When the chain finds nothing under a Rails project, raises a `ConfigError` listing every path tried and pointing at `--preload` / `--no-preload` (#879, PR #898)
|
|
43
|
+
- **`evilution version` prints the bundled `mcp` gem version** — second line shows `mcp gem X.Y.Z (server compatibility)`. Run inside the same bundle the MCP server uses to confirm what's loaded after a `bundle update` (#883, PR #894)
|
|
44
|
+
- **Public feedback channel exposed across CLI and MCP surfaces** — when a run hits friction (`errored`, `unparseable`, or `unresolved` buckets > 0; baseline failure; MCP error response), evilution surfaces a single GitHub Discussions URL so agents can suggest filing feedback. CLI text reports gain a one-line footer; the CLI error-exit path emits the same line on stderr (both suppressed by `--quiet`, `--format=json`, `--format=html`). MCP `evilution-mutate` responses embed `feedback_url` + `feedback_hint` on friction (and always on error payloads); the `minimal` verbosity contract is preserved (no extra keys). New MCP `evilution-info action=feedback` returns `{ discussion_url, version, guidance_for_agent }` on demand for any feedback intent including missing-capability requests on clean runs. Tool descriptions and the README MCP "Feedback channel" subsection prominently document the **explicit user-consent gate** (agents must never post on the user's behalf without explicit approval) and **privacy expectations** (never include secrets, env vars, project name, file paths, source code, or class/method names from user code — the channel is public). By construction, no run-derived data is embedded in any feedback field — agent + user compose any actual payload themselves (#900, PR #901)
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- **`Cache#store` crashed with `TypeError: no implicit conversion of nil into String` when `incremental: true` and `jobs >= 2`** — `Strategy::Parallel#run_batch` called `batch.each(&:strip_sources!)` before `@cache.store`, leaving `mutation.original_source = nil` for `Digest::SHA256.hexdigest`. Reordered so store runs before strip; added a defensive nil-source guard in `Cache#store` mirroring the existing one in `Cache#fetch` (#875, PR #890)
|
|
49
|
+
- **`block_removal` produced unparseable mutations on block-pass arguments (`map!(&:sym)`, `index_by(&:id)`, `flat_map(&block)`)** — operator stripped the `BlockArgumentNode` from inside the call's parens, leaving a dangling open paren. Operator now skips emission when `node.block.is_a?(Prism::BlockArgumentNode)`; explicit `{}` / `do..end` blocks are unaffected (#881, PR #895)
|
|
50
|
+
- **`method_body_replacement` errored at runtime when generating the `super`-replacement on methods whose enclosing class had no parent implementation** — `super`-replacement now only emitted when the original body already calls `super` (`SuperNode` or `ForwardingSuperNode`), using that as a heuristic that a super target exists in this context. Methods without `super` get only the `nil` and `self` replacements (#877, PR #892)
|
|
51
|
+
|
|
52
|
+
### Documentation
|
|
53
|
+
|
|
54
|
+
- **README "Installing on Rails 7.1 + Ruby 3.3" section** — covers the `cgi 0.5.0` (Rails-pinned) vs `cgi 0.5.1` (Ruby 3.3 default-gem) Bundler activation conflict, the sidecar `Gemfile.local` workaround (`eval_gemfile("Gemfile")` + add evilution + prism), the `BUNDLE_GEMFILE=Gemfile.local` invocation, and guidance on whether to commit or `.gitignore` the resulting `Gemfile.local.lock` (#882, PR #893)
|
|
55
|
+
- **README MCP "After upgrading the gem: restart the MCP server" subsection** — explains that the MCP server is a long-lived stdio process the agent host spawns; `bundle update evilution` swaps the gem on disk but the running process keeps the old code in memory until restart. Symptom is opaque "Internal error" responses to flags the old build doesn't recognize (#883, PR #894)
|
|
56
|
+
|
|
3
57
|
## [0.26.0] - 2026-04-24
|
|
4
58
|
|
|
5
59
|
### Removed
|
data/README.md
CHANGED
|
@@ -21,6 +21,44 @@ Then: `bundle install`
|
|
|
21
21
|
|
|
22
22
|
Or standalone: `gem install evilution`
|
|
23
23
|
|
|
24
|
+
Requires `prism >= 1.5, < 2`. Older Rails apps (e.g. Rails 7.1 pins `prism 0.19`) must upgrade prism — the gemspec constraint forces bundler to resolve a compatible 1.x version. If your app pins `prism 2.x`, bundler will reject the install until evilution widens its upper bound.
|
|
25
|
+
|
|
26
|
+
### Installing on Rails 7.1 + Ruby 3.3
|
|
27
|
+
|
|
28
|
+
Two Bundler conflicts hit fresh installs on this stack:
|
|
29
|
+
|
|
30
|
+
1. **`cgi` activation conflict.** Rails 7.1's `Gemfile.lock` pins `cgi 0.5.0`. Ruby 3.3.x ships `cgi 0.5.1` as a default gem. Loading evilution via Bundler aborts with:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
Gem::LoadError: can't activate cgi-0.5.1, already activated cgi-0.5.0.
|
|
34
|
+
Make sure all dependencies are added to Gemfile.
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
2. **`prism` pin.** Same lockfile pins `prism 0.19`, which lacks the `IfNode#subsequent` accessor evilution uses (older Prism releases exposed it as `consequent`). Symptom: `NoMethodError: undefined method 'subsequent' for an instance of Prism::IfNode`.
|
|
38
|
+
|
|
39
|
+
Both resolve cleanly with a sidecar `Gemfile.local` that re-evaluates the project Gemfile and adds evilution + prism on top — no edits to the main `Gemfile.lock`:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
# Gemfile.local
|
|
43
|
+
eval_gemfile("Gemfile")
|
|
44
|
+
|
|
45
|
+
group :test, :development do
|
|
46
|
+
gem "evilution"
|
|
47
|
+
gem "prism", "~> 1.5"
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Then invoke evilution against that Gemfile:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
BUNDLE_GEMFILE=Gemfile.local bundle install
|
|
55
|
+
BUNDLE_GEMFILE=Gemfile.local bundle exec evilution run lib/foo.rb
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The first command writes a sibling `Gemfile.local.lock`. Decide whether to commit or `.gitignore` it the same way you would for any developer-only Gemfile — typically gitignored when only one or two engineers run mutation testing locally, committed when CI also runs evilution against the sidecar Gemfile.
|
|
59
|
+
|
|
60
|
+
The evilution gemspec already declares `prism >= 1.5, < 2`, so adding the `gem "prism"` line above is only necessary on stacks that also pin prism in `Gemfile.lock`.
|
|
61
|
+
|
|
24
62
|
## Command Reference
|
|
25
63
|
|
|
26
64
|
```
|
|
@@ -67,17 +105,30 @@ The shorter alias `evil` ships alongside `evilution` and accepts identical argum
|
|
|
67
105
|
| `-q`, `--quiet` | Boolean | false | Suppress output. |
|
|
68
106
|
| `--stdin` | Boolean | false | Read target file paths from stdin (one per line). |
|
|
69
107
|
| `--integration NAME` | String | `rspec` | Test framework integration: `rspec` or `minitest`. |
|
|
70
|
-
| `--incremental`
|
|
108
|
+
| `--[no-]incremental` | Boolean | false | Cache killed/timeout results; skip unchanged mutations on re-runs. Pass `--no-incremental` to override `incremental: true` from the config file for one invocation (e.g. cold-cache debugging). Last flag wins when both are given. |
|
|
71
109
|
| `--save-session` | Boolean | false | Persist results as timestamped JSON under `.evilution/results/`. |
|
|
72
110
|
| `--no-progress` | Boolean | _(enabled)_ | Disable the TTY progress bar. |
|
|
111
|
+
| `--quiet-children` | Boolean | false | Redirect each forked worker's stdout/stderr to per-pid files under `tmp/evilution_children/<pid>.{out,err}` so noisy app initializers (Datadog, Bullet, etc.) don't merge with parent output. Trade-off: live worker errors only appear in the side files, not the terminal — `tail -f tmp/evilution_children/*.err` to watch them. |
|
|
112
|
+
| `--quiet-children-dir DIR` | String | `tmp/evilution_children` | Override the directory used by `--quiet-children`. |
|
|
73
113
|
| `--isolation MODE` | String | `auto` | Isolation strategy: `auto`, `fork`, or `in_process`. `auto` selects `fork` for Rails projects. See [docs/isolation.md](docs/isolation.md). |
|
|
74
|
-
| `--preload FILE` | String | _(auto)_ | File to require in parent before forking workers
|
|
114
|
+
| `--preload FILE` | String | _(auto)_ | File to require in parent before forking workers. Auto-detect chain for Rails projects: `spec/rails_helper.rb` → `spec/spec_helper.rb` → `test/test_helper.rb`. Errors with the full chain listed if none exist; pass `--no-preload` to opt out. |
|
|
75
115
|
| `--no-preload` | Boolean | _(enabled)_ | Disable parent-process preload. |
|
|
76
116
|
| `--skip-heredoc-literals` | Boolean | false | Skip all string literal mutations inside heredocs. |
|
|
77
117
|
| `--show-disabled` | Boolean | false | Report mutations skipped by `# evilution:disable` comments. |
|
|
78
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. |
|
|
79
119
|
| `--baseline-session PATH` | String | _(none)_ | Saved session file for HTML report comparison. |
|
|
80
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`.
|
|
81
132
|
|
|
82
133
|
### Exit Codes
|
|
83
134
|
|
|
@@ -323,7 +374,7 @@ The server exposes the following tools:
|
|
|
323
374
|
|---|---|
|
|
324
375
|
| `evilution-mutate` | Run mutation testing on target files with structured JSON results |
|
|
325
376
|
| `evilution-session` | Inspect mutation testing history — `action: list` browses saved sessions, `action: show` displays one, `action: diff` compares two (fixed/new/persistent survivors, score delta) |
|
|
326
|
-
| `evilution-info` | Discovery before mutation — `action: subjects` lists mutatable methods with mutation counts, `action: tests` resolves which specs cover given sources, `action: environment` dumps the effective config |
|
|
377
|
+
| `evilution-info` | Discovery before mutation — `action: subjects` lists mutatable methods with mutation counts, `action: tests` resolves which specs cover given sources, `action: environment` dumps the effective config, `action: statuses` returns the mutation-result status glossary, `action: feedback` returns the public Discussions URL plus consent + privacy guidance for posting feedback |
|
|
327
378
|
|
|
328
379
|
### Verbosity Control
|
|
329
380
|
|
|
@@ -375,6 +426,20 @@ Pass `skip_config: true` to ignore the project config file. This skips loading `
|
|
|
375
426
|
|
|
376
427
|
> **Note**: `.mcp.json` is gitignored by default since it is a local editor/agent configuration file.
|
|
377
428
|
|
|
429
|
+
### After upgrading the gem: restart the MCP server
|
|
430
|
+
|
|
431
|
+
The MCP server is a long-lived stdio process spawned by the agent host. `bundle update evilution` swaps the gem on disk but the running process keeps the old code in memory — symptom is opaque "Internal error" responses to flags or shapes the old build doesn't recognize. Restart the server (reload the workspace in Claude Code / Copilot / etc.) so the new gem loads.
|
|
432
|
+
|
|
433
|
+
`evilution version` prints the gem version and the bundled `mcp` gem version on separate lines — run it in the same bundle the MCP server uses to confirm what's loaded.
|
|
434
|
+
|
|
435
|
+
### Feedback channel
|
|
436
|
+
|
|
437
|
+
When evilution causes friction (errors, usage problems, missing capabilities you wish were there), the MCP responses include a `feedback_url` plus `feedback_hint`. The `evilution-info` tool also exposes `action=feedback`, which returns the channel URL and posting guidance on demand.
|
|
438
|
+
|
|
439
|
+
**Agents must never post on the user's behalf without explicit user permission.** Show the user exactly what you would post, get explicit approval, then post. Never include secrets, environment variables, the project name, file paths, source code, or class/method names from user code — the feedback channel is public.
|
|
440
|
+
|
|
441
|
+
Discussion URL: <https://github.com/marinazzio/evilution/discussions>
|
|
442
|
+
|
|
378
443
|
## Recommended Workflows for AI Agents
|
|
379
444
|
|
|
380
445
|
### 1. Full project scan
|
|
@@ -430,6 +495,14 @@ bundle exec evilution run lib/specific_file.rb --format json
|
|
|
430
495
|
|
|
431
496
|
Use when you know which file was modified and want to verify its test coverage.
|
|
432
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
|
+
|
|
433
506
|
### 6. Fixing surviving mutants
|
|
434
507
|
|
|
435
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
|
data/lib/evilution/cache.rb
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../evilution"
|
|
4
|
+
|
|
5
|
+
module Evilution::ChildOutput
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
attr_accessor :log_dir
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Per-run truncation happens once in the parent (Runner#configure_child_output);
|
|
13
|
+
# within a run, multiple forks reusing the same PID (pool worker recycle, per-mutation
|
|
14
|
+
# forks) append so cross-fork output isn't lost.
|
|
15
|
+
def redirect!
|
|
16
|
+
return unless log_dir
|
|
17
|
+
|
|
18
|
+
pid = Process.pid
|
|
19
|
+
$stdout.reopen(File.join(log_dir, "#{pid}.out"), "a")
|
|
20
|
+
$stderr.reopen(File.join(log_dir, "#{pid}.err"), "a")
|
|
21
|
+
$stdout.sync = true
|
|
22
|
+
$stderr.sync = true
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -10,6 +10,7 @@ require_relative "../../runner"
|
|
|
10
10
|
require_relative "../../hooks"
|
|
11
11
|
require_relative "../../hooks/registry"
|
|
12
12
|
require_relative "../../hooks/loader"
|
|
13
|
+
require_relative "../../feedback/messages"
|
|
13
14
|
|
|
14
15
|
class Evilution::CLI::Commands::Run < Evilution::CLI::Command
|
|
15
16
|
def call
|
|
@@ -34,10 +35,18 @@ class Evilution::CLI::Commands::Run < Evilution::CLI::Command
|
|
|
34
35
|
@stdout.puts(JSON.generate(error_payload(error)))
|
|
35
36
|
Evilution::CLI::Result.new(exit_code: 2, error: error, error_rendered: true)
|
|
36
37
|
else
|
|
38
|
+
@stderr.puts(Evilution::Feedback::Messages.cli_footer) unless quiet?(config, file_options)
|
|
37
39
|
Evilution::CLI::Result.new(exit_code: 2, error: error)
|
|
38
40
|
end
|
|
39
41
|
end
|
|
40
42
|
|
|
43
|
+
def quiet?(config, file_options)
|
|
44
|
+
return config.quiet unless config.nil?
|
|
45
|
+
return true if @options[:quiet]
|
|
46
|
+
|
|
47
|
+
file_options && file_options[:quiet]
|
|
48
|
+
end
|
|
49
|
+
|
|
41
50
|
def build_hooks(config)
|
|
42
51
|
return nil if config.hooks.empty?
|
|
43
52
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "mcp"
|
|
3
4
|
require_relative "../commands"
|
|
4
5
|
require_relative "../command"
|
|
5
6
|
require_relative "../dispatcher"
|
|
@@ -10,6 +11,7 @@ class Evilution::CLI::Commands::Version < Evilution::CLI::Command
|
|
|
10
11
|
|
|
11
12
|
def perform
|
|
12
13
|
@stdout.puts(Evilution::VERSION)
|
|
14
|
+
@stdout.puts("mcp gem #{::MCP::VERSION} (server compatibility)")
|
|
13
15
|
0
|
|
14
16
|
end
|
|
15
17
|
end
|
|
@@ -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)
|
|
@@ -70,15 +71,35 @@ class Evilution::CLI::Parser::OptionsBuilder
|
|
|
70
71
|
opts.on("--fail-fast", "Stop after N surviving mutants " \
|
|
71
72
|
"(default: disabled; if provided without N, uses 1; use --fail-fast=N)") { @options[:fail_fast] ||= 1 }
|
|
72
73
|
opts.on("--no-baseline", "Skip baseline test suite check") { @options[:baseline] = false }
|
|
73
|
-
opts.on("--incremental",
|
|
74
|
+
opts.on("--[no-]incremental",
|
|
75
|
+
"Cache killed/timeout results; skip re-running them on unchanged files. " \
|
|
76
|
+
"Use --no-incremental to override `incremental: true` from the config file for one run.") do |v|
|
|
77
|
+
@options[:incremental] = v
|
|
78
|
+
end
|
|
74
79
|
opts.on("--integration NAME", "Test integration: rspec, minitest (default: rspec)") { |i| @options[:integration] = i }
|
|
75
80
|
opts.on("--isolation STRATEGY", "Isolation: auto, fork, in_process (default: auto)") { |s| @options[:isolation] = s }
|
|
76
81
|
opts.on("--preload FILE", "Preload FILE in the parent process before forking " \
|
|
77
|
-
"(default: auto-detect spec/rails_helper.rb
|
|
82
|
+
"(default: auto-detect spec/rails_helper.rb -> spec/spec_helper.rb -> " \
|
|
83
|
+
"test/test_helper.rb for Rails projects)") { |f| @options[:preload] = f }
|
|
78
84
|
opts.on("--no-preload", "Disable parent-process preload even for Rails projects") { @options[:preload] = false }
|
|
79
85
|
opts.on("--stdin", "Read target file paths from stdin (one per line)") { @options[:stdin] = true }
|
|
80
86
|
opts.on("--suggest-tests", "Generate concrete test code in suggestions (RSpec or Minitest)") { @options[:suggest_tests] = true }
|
|
81
87
|
opts.on("--no-progress", "Disable progress bar") { @options[:progress] = false }
|
|
88
|
+
opts.on("--quiet-children",
|
|
89
|
+
"Redirect each child process's stdout/stderr to per-pid files under " \
|
|
90
|
+
"tmp/evilution_children (or --quiet-children-dir DIR), keeping parent output clean.") do
|
|
91
|
+
@options[:quiet_children] = true
|
|
92
|
+
end
|
|
93
|
+
opts.on("--quiet-children-dir DIR",
|
|
94
|
+
"Directory for --quiet-children per-pid log files (default: tmp/evilution_children)") do |d|
|
|
95
|
+
@options[:quiet_children_dir] = d
|
|
96
|
+
end
|
|
97
|
+
end
|
|
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" }
|
|
82
103
|
end
|
|
83
104
|
|
|
84
105
|
def add_extra_flag_options(opts)
|
|
@@ -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
|