evilution 0.26.0 → 0.27.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 +10 -0
- data/.rubocop_todo.yml +7 -0
- data/CHANGELOG.md +22 -0
- data/README.md +57 -3
- 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 +16 -2
- 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/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 +78 -268
- 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/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 +35 -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 +7 -2
- 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 -261
- data/lib/evilution/mcp/mutate_tool/error_payload.rb +8 -1
- data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +13 -1
- data/lib/evilution/mcp/mutate_tool.rb +5 -2
- data/lib/evilution/mutator/operator/block_removal.rb +1 -1
- data/lib/evilution/mutator/operator/method_body_replacement.rb +18 -2
- data/lib/evilution/parallel/work_queue/channel/frame.rb +21 -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/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/runner/isolation_resolver.rb +9 -2
- data/lib/evilution/runner/mutation_executor/mutation_runner.rb +32 -0
- data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +16 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +46 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +75 -0
- data/lib/evilution/runner/mutation_executor/result_cache.rb +69 -0
- data/lib/evilution/runner/mutation_executor/result_notifier.rb +48 -0
- data/lib/evilution/runner/mutation_executor/result_packer.rb +39 -0
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +80 -0
- data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +34 -0
- data/lib/evilution/runner/mutation_executor.rb +58 -289
- data/lib/evilution/runner.rb +21 -0
- data/lib/evilution/version.rb +1 -1
- metadata +113 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 12d485d3cce9569229a95a1e8f29403fbb11f8f067e0c438256919521f6d82dc
|
|
4
|
+
data.tar.gz: 11a1adeca7c61fa905757137d6a737c6e389eae584b37fca91a8c9eed57926d0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ce9208fa3f3ed3160d2844cdd5e1479cf7b2fa90985371cb95d6978177f371d38ec4bdccf473f79af318cd6c617fda2b1962efe4f389eab1912ab348cc48b475
|
|
7
|
+
data.tar.gz: 57ff4d971829430d5d47525ac0bc5e488b326e6916657aaf9acb3faf0e8795e909523ec8238b6b9fbaace13bdd60c0f437f30d94efbfa82861b42da802a54aac
|
data/.beads/interactions.jsonl
CHANGED
|
@@ -235,3 +235,13 @@
|
|
|
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"}}
|
data/.rubocop_todo.yml
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.27.0] - 2026-04-26
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`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)
|
|
8
|
+
- **`--[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)
|
|
9
|
+
- **`--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)
|
|
10
|
+
- **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)
|
|
11
|
+
- **`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)
|
|
12
|
+
- **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)
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **`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)
|
|
17
|
+
- **`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)
|
|
18
|
+
- **`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)
|
|
19
|
+
|
|
20
|
+
### Documentation
|
|
21
|
+
|
|
22
|
+
- **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)
|
|
23
|
+
- **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)
|
|
24
|
+
|
|
3
25
|
## [0.26.0] - 2026-04-24
|
|
4
26
|
|
|
5
27
|
### 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,11 +105,13 @@ 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. |
|
|
@@ -323,7 +363,7 @@ The server exposes the following tools:
|
|
|
323
363
|
|---|---|
|
|
324
364
|
| `evilution-mutate` | Run mutation testing on target files with structured JSON results |
|
|
325
365
|
| `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 |
|
|
366
|
+
| `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
367
|
|
|
328
368
|
### Verbosity Control
|
|
329
369
|
|
|
@@ -375,6 +415,20 @@ Pass `skip_config: true` to ignore the project config file. This skips loading `
|
|
|
375
415
|
|
|
376
416
|
> **Note**: `.mcp.json` is gitignored by default since it is a local editor/agent configuration file.
|
|
377
417
|
|
|
418
|
+
### After upgrading the gem: restart the MCP server
|
|
419
|
+
|
|
420
|
+
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.
|
|
421
|
+
|
|
422
|
+
`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.
|
|
423
|
+
|
|
424
|
+
### Feedback channel
|
|
425
|
+
|
|
426
|
+
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.
|
|
427
|
+
|
|
428
|
+
**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.
|
|
429
|
+
|
|
430
|
+
Discussion URL: <https://github.com/marinazzio/evilution/discussions>
|
|
431
|
+
|
|
378
432
|
## Recommended Workflows for AI Agents
|
|
379
433
|
|
|
380
434
|
### 1. Full project scan
|
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
|
|
@@ -70,15 +70,29 @@ class Evilution::CLI::Parser::OptionsBuilder
|
|
|
70
70
|
opts.on("--fail-fast", "Stop after N surviving mutants " \
|
|
71
71
|
"(default: disabled; if provided without N, uses 1; use --fail-fast=N)") { @options[:fail_fast] ||= 1 }
|
|
72
72
|
opts.on("--no-baseline", "Skip baseline test suite check") { @options[:baseline] = false }
|
|
73
|
-
opts.on("--incremental",
|
|
73
|
+
opts.on("--[no-]incremental",
|
|
74
|
+
"Cache killed/timeout results; skip re-running them on unchanged files. " \
|
|
75
|
+
"Use --no-incremental to override `incremental: true` from the config file for one run.") do |v|
|
|
76
|
+
@options[:incremental] = v
|
|
77
|
+
end
|
|
74
78
|
opts.on("--integration NAME", "Test integration: rspec, minitest (default: rspec)") { |i| @options[:integration] = i }
|
|
75
79
|
opts.on("--isolation STRATEGY", "Isolation: auto, fork, in_process (default: auto)") { |s| @options[:isolation] = s }
|
|
76
80
|
opts.on("--preload FILE", "Preload FILE in the parent process before forking " \
|
|
77
|
-
"(default: auto-detect spec/rails_helper.rb
|
|
81
|
+
"(default: auto-detect spec/rails_helper.rb -> spec/spec_helper.rb -> " \
|
|
82
|
+
"test/test_helper.rb for Rails projects)") { |f| @options[:preload] = f }
|
|
78
83
|
opts.on("--no-preload", "Disable parent-process preload even for Rails projects") { @options[:preload] = false }
|
|
79
84
|
opts.on("--stdin", "Read target file paths from stdin (one per line)") { @options[:stdin] = true }
|
|
80
85
|
opts.on("--suggest-tests", "Generate concrete test code in suggestions (RSpec or Minitest)") { @options[:suggest_tests] = true }
|
|
81
86
|
opts.on("--no-progress", "Disable progress bar") { @options[:progress] = false }
|
|
87
|
+
opts.on("--quiet-children",
|
|
88
|
+
"Redirect each child process's stdout/stderr to per-pid files under " \
|
|
89
|
+
"tmp/evilution_children (or --quiet-children-dir DIR), keeping parent output clean.") do
|
|
90
|
+
@options[:quiet_children] = true
|
|
91
|
+
end
|
|
92
|
+
opts.on("--quiet-children-dir DIR",
|
|
93
|
+
"Directory for --quiet-children per-pid log files (default: tmp/evilution_children)") do |d|
|
|
94
|
+
@options[:quiet_children_dir] = d
|
|
95
|
+
end
|
|
82
96
|
end
|
|
83
97
|
|
|
84
98
|
def add_extra_flag_options(opts)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../builders"
|
|
4
|
+
require_relative "../../spec_resolver"
|
|
5
|
+
|
|
6
|
+
class Evilution::Config::Builders::SpecResolver
|
|
7
|
+
def self.call(integration:)
|
|
8
|
+
case integration
|
|
9
|
+
when :minitest
|
|
10
|
+
Evilution::SpecResolver.new(test_dir: "test", test_suffix: "_test.rb", request_dir: "integration")
|
|
11
|
+
else
|
|
12
|
+
Evilution::SpecResolver.new
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../builders"
|
|
4
|
+
require_relative "spec_resolver"
|
|
5
|
+
require_relative "../../spec_selector"
|
|
6
|
+
|
|
7
|
+
class Evilution::Config::Builders::SpecSelector
|
|
8
|
+
def self.call(spec_files:, spec_mappings:, spec_pattern:, integration:)
|
|
9
|
+
Evilution::SpecSelector.new(
|
|
10
|
+
spec_files: spec_files,
|
|
11
|
+
spec_mappings: spec_mappings,
|
|
12
|
+
spec_pattern: spec_pattern,
|
|
13
|
+
spec_resolver: Evilution::Config::Builders::SpecResolver.call(integration: integration)
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Evilution::Config::EnvLoader
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
def load
|
|
7
|
+
opts = {}
|
|
8
|
+
val = ENV.fetch("EV_DISABLE_EXAMPLE_TARGETING", nil)
|
|
9
|
+
opts[:example_targeting] = false if val && !val.empty? && val != "0"
|
|
10
|
+
opts
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Evilution::Config::FileLoader
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def load
|
|
9
|
+
Evilution::Config::CONFIG_FILES.each do |path|
|
|
10
|
+
next unless File.exist?(path)
|
|
11
|
+
|
|
12
|
+
data = YAML.safe_load_file(path, symbolize_names: true)
|
|
13
|
+
return data.is_a?(Hash) ? data : {}
|
|
14
|
+
rescue Psych::SyntaxError, Psych::DisallowedClass => e
|
|
15
|
+
raise Evilution::ConfigError.new("failed to parse config file #{path}: #{e.message}", file: path)
|
|
16
|
+
rescue SystemCallError => e
|
|
17
|
+
raise Evilution::ConfigError.new("cannot read config file #{path}: #{e.message}", file: path)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
{}
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "file_loader"
|
|
4
|
+
require_relative "env_loader"
|
|
5
|
+
|
|
6
|
+
module Evilution::Config::Sources
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def merge(explicit:, skip_file:)
|
|
10
|
+
file = skip_file ? {} : Evilution::Config::FileLoader.load
|
|
11
|
+
env = Evilution::Config::EnvLoader.load
|
|
12
|
+
Evilution::Config::DEFAULTS.merge(file).merge(env).merge(explicit)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../validators"
|
|
4
|
+
|
|
5
|
+
class Evilution::Config::Validators::Base
|
|
6
|
+
def self.call(_value)
|
|
7
|
+
raise NotImplementedError
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def coerce_symbol!(value, allowed:, name:)
|
|
14
|
+
raise Evilution::ConfigError, "#{name} must be #{allowed.join(" or ")}, got nil" if value.nil?
|
|
15
|
+
|
|
16
|
+
unless value.is_a?(String) || value.is_a?(Symbol)
|
|
17
|
+
raise Evilution::ConfigError, "#{name} must be #{allowed.join(" or ")}, got #{value.inspect}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sym = value.to_sym
|
|
21
|
+
return sym if allowed.include?(sym)
|
|
22
|
+
|
|
23
|
+
raise Evilution::ConfigError, "#{name} must be #{allowed.join(" or ")}, got #{sym.inspect}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def coerce_positive_int!(value, name:)
|
|
27
|
+
raise Evilution::ConfigError, "#{name} must be a positive integer, got #{value.inspect}" if value.is_a?(Float)
|
|
28
|
+
|
|
29
|
+
int = Integer(value)
|
|
30
|
+
raise Evilution::ConfigError, "#{name} must be a positive integer, got #{int}" unless int >= 1
|
|
31
|
+
|
|
32
|
+
int
|
|
33
|
+
rescue ::ArgumentError, ::TypeError
|
|
34
|
+
raise Evilution::ConfigError, "#{name} must be a positive integer, got #{value.inspect}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
class Evilution::Config::Validators::ExampleTargetingCache < Evilution::Config::Validators::Base
|
|
6
|
+
def self.call(value)
|
|
7
|
+
raise Evilution::ConfigError, "example_targeting_cache must be a Hash, got #{value.class}" unless value.is_a?(Hash)
|
|
8
|
+
|
|
9
|
+
normalized = normalize_keys(value)
|
|
10
|
+
merged = Evilution::Config::DEFAULTS[:example_targeting_cache].merge(normalized)
|
|
11
|
+
require_positive_int!(merged, :max_files)
|
|
12
|
+
require_positive_int!(merged, :max_blocks)
|
|
13
|
+
merged
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def normalize_keys(value)
|
|
20
|
+
value.each_with_object({}) do |(k, v), acc|
|
|
21
|
+
unless k.is_a?(String) || k.is_a?(Symbol)
|
|
22
|
+
raise Evilution::ConfigError,
|
|
23
|
+
"example_targeting_cache keys must be Strings or Symbols, got #{k.inspect}"
|
|
24
|
+
end
|
|
25
|
+
acc[k.to_sym] = v
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def require_positive_int!(cache, key)
|
|
30
|
+
v = cache[key]
|
|
31
|
+
return if v.is_a?(Integer) && v >= 1
|
|
32
|
+
|
|
33
|
+
raise Evilution::ConfigError,
|
|
34
|
+
"example_targeting_cache.#{key} must be a positive integer, got #{v.inspect}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
class Evilution::Config::Validators::ExampleTargetingFallback < Evilution::Config::Validators::Base
|
|
6
|
+
FALLBACKS = %i[full_file unresolved].freeze
|
|
7
|
+
|
|
8
|
+
def self.call(value)
|
|
9
|
+
unless value.is_a?(String) || value.is_a?(Symbol)
|
|
10
|
+
raise Evilution::ConfigError,
|
|
11
|
+
"example_targeting_fallback must be full_file or unresolved, got #{value.inspect}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
sym = value.to_sym
|
|
15
|
+
unless FALLBACKS.include?(sym)
|
|
16
|
+
raise Evilution::ConfigError,
|
|
17
|
+
"example_targeting_fallback must be full_file or unresolved, got #{sym.inspect}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sym
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
class Evilution::Config::Validators::FailFast < Evilution::Config::Validators::Base
|
|
6
|
+
def self.call(value)
|
|
7
|
+
return nil if value.nil?
|
|
8
|
+
|
|
9
|
+
coerce_positive_int!(value, name: "fail_fast")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
class Evilution::Config::Validators::Hooks < Evilution::Config::Validators::Base
|
|
6
|
+
def self.call(value)
|
|
7
|
+
return {} if value.nil?
|
|
8
|
+
raise Evilution::ConfigError, "hooks must be a mapping of event names to file paths, got #{value.class}" unless value.is_a?(Hash)
|
|
9
|
+
|
|
10
|
+
value
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
class Evilution::Config::Validators::IgnorePatterns < Evilution::Config::Validators::Base
|
|
6
|
+
def self.call(value)
|
|
7
|
+
patterns = Array(value)
|
|
8
|
+
patterns.each do |pattern|
|
|
9
|
+
unless pattern.is_a?(String)
|
|
10
|
+
raise Evilution::ConfigError,
|
|
11
|
+
"ignore_patterns must be an array of strings, got #{pattern.class} (#{pattern.inspect})"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
patterns
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
class Evilution::Config::Validators::Integration < Evilution::Config::Validators::Base
|
|
6
|
+
ALLOWED = %i[rspec minitest].freeze
|
|
7
|
+
|
|
8
|
+
def self.call(value)
|
|
9
|
+
coerce_symbol!(value, allowed: ALLOWED, name: "integration")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
class Evilution::Config::Validators::Isolation < Evilution::Config::Validators::Base
|
|
6
|
+
ALLOWED = %i[auto fork in_process].freeze
|
|
7
|
+
MESSAGE = "isolation must be auto, fork, or in_process"
|
|
8
|
+
|
|
9
|
+
def self.call(value)
|
|
10
|
+
raise Evilution::ConfigError, "#{MESSAGE}, got nil" if value.nil?
|
|
11
|
+
|
|
12
|
+
raise Evilution::ConfigError, "#{MESSAGE}, got #{value.inspect}" unless value.is_a?(String) || value.is_a?(Symbol)
|
|
13
|
+
|
|
14
|
+
sym = value.to_sym
|
|
15
|
+
return sym if ALLOWED.include?(sym)
|
|
16
|
+
|
|
17
|
+
raise Evilution::ConfigError, "#{MESSAGE}, got #{sym.inspect}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
class Evilution::Config::Validators::Preload < Evilution::Config::Validators::Base
|
|
6
|
+
def self.call(value)
|
|
7
|
+
return nil if value.nil?
|
|
8
|
+
return false if value == false
|
|
9
|
+
return value if value.is_a?(String)
|
|
10
|
+
|
|
11
|
+
raise Evilution::ConfigError, "preload must be nil, false, or a String path, got #{value.inspect}"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
class Evilution::Config::Validators::SpecMappings < Evilution::Config::Validators::Base
|
|
6
|
+
def self.call(value)
|
|
7
|
+
return {} if value.nil?
|
|
8
|
+
|
|
9
|
+
raise Evilution::ConfigError, "spec_mappings must be a Hash, got #{value.class}" unless value.is_a?(Hash)
|
|
10
|
+
|
|
11
|
+
normalized = value.each_with_object({}) do |(source, specs), acc|
|
|
12
|
+
key = normalize_key(source)
|
|
13
|
+
acc[key] = normalize_value(key, specs)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
warn_missing(normalized)
|
|
17
|
+
normalized
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def normalize_key(source)
|
|
24
|
+
key = source.to_s
|
|
25
|
+
key = key.delete_prefix("#{Dir.pwd}/") if key.start_with?("/")
|
|
26
|
+
key.delete_prefix("./")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def normalize_value(source, specs)
|
|
30
|
+
case specs
|
|
31
|
+
when String then [specs]
|
|
32
|
+
when Array
|
|
33
|
+
specs.each do |entry|
|
|
34
|
+
unless entry.is_a?(String)
|
|
35
|
+
raise Evilution::ConfigError,
|
|
36
|
+
"spec_mappings[#{source.inspect}] entries must be string paths, got #{entry.class}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
specs
|
|
40
|
+
else
|
|
41
|
+
raise Evilution::ConfigError,
|
|
42
|
+
"spec_mappings[#{source.inspect}] must be a string or array of strings, got #{specs.class}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def warn_missing(mappings)
|
|
47
|
+
mappings.each do |source, specs|
|
|
48
|
+
specs.each do |spec_path|
|
|
49
|
+
next if File.exist?(spec_path)
|
|
50
|
+
|
|
51
|
+
warn "[evilution] spec_mappings[#{source.inspect}]: #{spec_path} not found, skipping"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
class Evilution::Config::Validators::SpecPattern < Evilution::Config::Validators::Base
|
|
6
|
+
def self.call(value)
|
|
7
|
+
return nil if value.nil?
|
|
8
|
+
return value if value.is_a?(String)
|
|
9
|
+
|
|
10
|
+
raise Evilution::ConfigError, "spec_pattern must be nil or a String glob, got #{value.class}"
|
|
11
|
+
end
|
|
12
|
+
end
|