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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +10 -0
  3. data/.rubocop_todo.yml +7 -0
  4. data/CHANGELOG.md +22 -0
  5. data/README.md +57 -3
  6. data/lib/evilution/cache.rb +2 -0
  7. data/lib/evilution/child_output.rb +24 -0
  8. data/lib/evilution/cli/commands/run.rb +9 -0
  9. data/lib/evilution/cli/commands/version.rb +2 -0
  10. data/lib/evilution/cli/parser/options_builder.rb +16 -2
  11. data/lib/evilution/config/builders/spec_resolver.rb +15 -0
  12. data/lib/evilution/config/builders/spec_selector.rb +16 -0
  13. data/lib/evilution/config/builders.rb +4 -0
  14. data/lib/evilution/config/env_loader.rb +12 -0
  15. data/lib/evilution/config/file_loader.rb +22 -0
  16. data/lib/evilution/config/sources.rb +14 -0
  17. data/lib/evilution/config/validators/base.rb +37 -0
  18. data/lib/evilution/config/validators/example_targeting_cache.rb +37 -0
  19. data/lib/evilution/config/validators/example_targeting_fallback.rb +22 -0
  20. data/lib/evilution/config/validators/fail_fast.rb +11 -0
  21. data/lib/evilution/config/validators/hooks.rb +12 -0
  22. data/lib/evilution/config/validators/ignore_patterns.rb +16 -0
  23. data/lib/evilution/config/validators/integration.rb +11 -0
  24. data/lib/evilution/config/validators/isolation.rb +19 -0
  25. data/lib/evilution/config/validators/jobs.rb +9 -0
  26. data/lib/evilution/config/validators/preload.rb +13 -0
  27. data/lib/evilution/config/validators/spec_mappings.rb +56 -0
  28. data/lib/evilution/config/validators/spec_pattern.rb +12 -0
  29. data/lib/evilution/config/validators.rb +4 -0
  30. data/lib/evilution/config.rb +78 -268
  31. data/lib/evilution/feedback/detector.rb +15 -0
  32. data/lib/evilution/feedback/messages.rb +42 -0
  33. data/lib/evilution/feedback.rb +5 -0
  34. data/lib/evilution/integration/rspec/baseline_runner.rb +16 -0
  35. data/lib/evilution/integration/rspec/crash_detector_lifecycle.rb +17 -0
  36. data/lib/evilution/integration/rspec/example_filter_applier.rb +21 -0
  37. data/lib/evilution/integration/rspec/framework_loader.rb +28 -0
  38. data/lib/evilution/integration/rspec/result_builder.rb +40 -0
  39. data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +28 -0
  40. data/lib/evilution/integration/rspec/state_guard/internals.rb +19 -0
  41. data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +35 -0
  42. data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +32 -0
  43. data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +20 -0
  44. data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +20 -0
  45. data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +20 -0
  46. data/lib/evilution/integration/rspec/state_guard.rb +40 -0
  47. data/lib/evilution/integration/rspec/test_file_resolver.rb +30 -0
  48. data/lib/evilution/integration/rspec/unresolved_spec_warner.rb +18 -0
  49. data/lib/evilution/integration/rspec.rb +61 -232
  50. data/lib/evilution/isolation/fork.rb +7 -2
  51. data/lib/evilution/mcp/info_tool/actions/base.rb +22 -0
  52. data/lib/evilution/mcp/info_tool/actions/environment.rb +42 -0
  53. data/lib/evilution/mcp/info_tool/actions/feedback.rb +16 -0
  54. data/lib/evilution/mcp/info_tool/actions/statuses.rb +10 -0
  55. data/lib/evilution/mcp/info_tool/actions/subjects.rb +47 -0
  56. data/lib/evilution/mcp/info_tool/actions/tests.rb +60 -0
  57. data/lib/evilution/mcp/info_tool/actions.rb +16 -0
  58. data/lib/evilution/mcp/info_tool/config_factory.rb +24 -0
  59. data/lib/evilution/mcp/info_tool/error_mapper.rb +15 -0
  60. data/lib/evilution/mcp/info_tool/request_parser.rb +34 -0
  61. data/lib/evilution/mcp/info_tool/response_formatter.rb +24 -0
  62. data/lib/evilution/mcp/info_tool/status_glossary.rb +75 -0
  63. data/lib/evilution/mcp/info_tool.rb +43 -261
  64. data/lib/evilution/mcp/mutate_tool/error_payload.rb +8 -1
  65. data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +13 -1
  66. data/lib/evilution/mcp/mutate_tool.rb +5 -2
  67. data/lib/evilution/mutator/operator/block_removal.rb +1 -1
  68. data/lib/evilution/mutator/operator/method_body_replacement.rb +18 -2
  69. data/lib/evilution/parallel/work_queue/channel/frame.rb +21 -0
  70. data/lib/evilution/parallel/work_queue/channel.rb +23 -0
  71. data/lib/evilution/parallel/work_queue/collection_state.rb +14 -0
  72. data/lib/evilution/parallel/work_queue/dispatcher.rb +133 -0
  73. data/lib/evilution/parallel/work_queue/validators/optional_positive_int.rb +11 -0
  74. data/lib/evilution/parallel/work_queue/validators/optional_positive_number.rb +11 -0
  75. data/lib/evilution/parallel/work_queue/validators/positive_int.rb +11 -0
  76. data/lib/evilution/parallel/work_queue/validators.rb +6 -0
  77. data/lib/evilution/parallel/work_queue/worker/loop.rb +45 -0
  78. data/lib/evilution/parallel/work_queue/worker.rb +114 -0
  79. data/lib/evilution/parallel/work_queue/worker_stat.rb +17 -0
  80. data/lib/evilution/parallel/work_queue.rb +42 -327
  81. data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +18 -0
  82. data/lib/evilution/reporter/cli/item_formatters/disabled.rb +9 -0
  83. data/lib/evilution/reporter/cli/item_formatters/error.rb +14 -0
  84. data/lib/evilution/reporter/cli/item_formatters/result_location.rb +10 -0
  85. data/lib/evilution/reporter/cli/item_formatters.rb +6 -0
  86. data/lib/evilution/reporter/cli/line_formatters/duration.rb +9 -0
  87. data/lib/evilution/reporter/cli/line_formatters/efficiency.rb +18 -0
  88. data/lib/evilution/reporter/cli/line_formatters/feedback_footer.rb +13 -0
  89. data/lib/evilution/reporter/cli/line_formatters/header.rb +10 -0
  90. data/lib/evilution/reporter/cli/line_formatters/mutations.rb +16 -0
  91. data/lib/evilution/reporter/cli/line_formatters/peak_memory.rb +12 -0
  92. data/lib/evilution/reporter/cli/line_formatters/result_line.rb +20 -0
  93. data/lib/evilution/reporter/cli/line_formatters/score.rb +14 -0
  94. data/lib/evilution/reporter/cli/line_formatters/truncation_notice.rb +11 -0
  95. data/lib/evilution/reporter/cli/line_formatters.rb +6 -0
  96. data/lib/evilution/reporter/cli/metrics_block.rb +26 -0
  97. data/lib/evilution/reporter/cli/pct.rb +9 -0
  98. data/lib/evilution/reporter/cli/section.rb +13 -0
  99. data/lib/evilution/reporter/cli/section_renderer.rb +15 -0
  100. data/lib/evilution/reporter/cli/trailer.rb +22 -0
  101. data/lib/evilution/reporter/cli.rb +79 -162
  102. data/lib/evilution/runner/isolation_resolver.rb +9 -2
  103. data/lib/evilution/runner/mutation_executor/mutation_runner.rb +32 -0
  104. data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +16 -0
  105. data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +46 -0
  106. data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +75 -0
  107. data/lib/evilution/runner/mutation_executor/result_cache.rb +69 -0
  108. data/lib/evilution/runner/mutation_executor/result_notifier.rb +48 -0
  109. data/lib/evilution/runner/mutation_executor/result_packer.rb +39 -0
  110. data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +80 -0
  111. data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +34 -0
  112. data/lib/evilution/runner/mutation_executor.rb +58 -289
  113. data/lib/evilution/runner.rb +21 -0
  114. data/lib/evilution/version.rb +1 -1
  115. metadata +113 -2
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution::Config::Validators
4
+ end
@@ -8,40 +8,18 @@ class Evilution::Config
8
8
  CONFIG_FILES = %w[.evilution.yml config/evilution.yml].freeze
9
9
 
10
10
  DEFAULTS = {
11
- timeout: 30,
12
- format: :text,
13
- target: nil,
14
- min_score: 0.0,
15
- integration: :rspec,
16
- verbose: false,
17
- quiet: false,
18
- jobs: 1,
19
- fail_fast: nil,
20
- baseline: true,
21
- isolation: :auto,
22
- incremental: false,
23
- suggest_tests: false,
24
- progress: true,
25
- save_session: false,
26
- line_ranges: {},
27
- spec_files: [],
28
- ignore_patterns: [],
29
- show_disabled: false,
30
- baseline_session: nil,
31
- skip_heredoc_literals: false,
32
- related_specs_heuristic: false,
33
- fallback_to_full_suite: false,
34
- preload: nil,
35
- spec_mappings: {},
36
- spec_pattern: nil,
37
- example_targeting: true,
11
+ timeout: 30, format: :text, target: nil, min_score: 0.0, integration: :rspec,
12
+ verbose: false, quiet: false, jobs: 1, fail_fast: nil, baseline: true,
13
+ isolation: :auto, incremental: false, suggest_tests: false, progress: true,
14
+ save_session: false, line_ranges: {}, spec_files: [], ignore_patterns: [],
15
+ show_disabled: false, baseline_session: nil, skip_heredoc_literals: false,
16
+ related_specs_heuristic: false, fallback_to_full_suite: false, preload: nil,
17
+ spec_mappings: {}, spec_pattern: nil, example_targeting: true,
38
18
  example_targeting_fallback: :full_file,
39
- example_targeting_cache: { max_files: 50, max_blocks: 10_000 }
19
+ example_targeting_cache: { max_files: 50, max_blocks: 10_000 },
20
+ quiet_children: false, quiet_children_dir: "tmp/evilution_children"
40
21
  }.freeze
41
22
 
42
- EXAMPLE_TARGETING_FALLBACKS = %i[full_file unresolved].freeze
43
- private_constant :EXAMPLE_TARGETING_FALLBACKS
44
-
45
23
  attr_reader :target_files, :timeout, :format,
46
24
  :target, :min_score, :integration, :verbose, :quiet,
47
25
  :jobs, :fail_fast, :baseline, :isolation, :incremental, :suggest_tests,
@@ -50,12 +28,11 @@ class Evilution::Config
50
28
  :skip_heredoc_literals, :related_specs_heuristic,
51
29
  :fallback_to_full_suite, :preload, :spec_mappings, :spec_pattern,
52
30
  :example_targeting, :example_targeting_fallback, :example_targeting_cache,
53
- :spec_selector
31
+ :spec_selector, :quiet_children, :quiet_children_dir
54
32
 
55
33
  def initialize(**options)
56
- file_options = options.delete(:skip_config_file) ? {} : load_config_file
57
- env_options = load_env_options
58
- merged = DEFAULTS.merge(file_options).merge(env_options).merge(options)
34
+ skip_file = options.delete(:skip_config_file) ? true : false
35
+ merged = Sources.merge(explicit: options, skip_file: skip_file)
59
36
  assign_attributes(merged)
60
37
  freeze
61
38
  end
@@ -125,18 +102,7 @@ class Evilution::Config
125
102
  end
126
103
 
127
104
  def self.file_options
128
- CONFIG_FILES.each do |path|
129
- next unless File.exist?(path)
130
-
131
- data = YAML.safe_load_file(path, symbolize_names: true)
132
- return data.is_a?(Hash) ? data : {}
133
- rescue Psych::SyntaxError, Psych::DisallowedClass => e
134
- raise Evilution::ConfigError.new("failed to parse config file #{path}: #{e.message}", file: path)
135
- rescue SystemCallError => e
136
- raise Evilution::ConfigError.new("cannot read config file #{path}: #{e.message}", file: path)
137
- end
138
-
139
- {}
105
+ FileLoader.load
140
106
  end
141
107
 
142
108
  # Generates a default config file template.
@@ -185,9 +151,10 @@ class Evilution::Config
185
151
  # fallback_to_full_suite: false
186
152
 
187
153
  # Preload file required in the parent process before forking workers.
188
- # For Rails projects, spec/rails_helper.rb or test/test_helper.rb is
189
- # auto-detected when isolation resolves to :fork. Set to false to disable.
190
- # preload: spec/rails_helper.rb # or test/test_helper.rb
154
+ # For Rails projects, the autodetect chain tries (in order):
155
+ # spec/rails_helper.rb -> spec/spec_helper.rb -> test/test_helper.rb
156
+ # when isolation resolves to :fork. Set to false to disable.
157
+ # preload: spec/rails_helper.rb # or spec/spec_helper.rb, test/test_helper.rb
191
158
 
192
159
  # Hooks: Ruby files returning a Proc, keyed by lifecycle event
193
160
  # hooks:
@@ -221,234 +188,77 @@ class Evilution::Config
221
188
 
222
189
  private
223
190
 
224
- def validate_fail_fast(value)
225
- return nil if value.nil?
226
-
227
- value = Integer(value)
228
- raise Evilution::ConfigError, "fail_fast must be a positive integer, got #{value}" unless value >= 1
229
-
230
- value
231
- rescue ::ArgumentError, ::TypeError
232
- raise Evilution::ConfigError, "fail_fast must be a positive integer, got #{value.inspect}"
233
- end
234
-
235
- def assign_attributes(merged) # rubocop:disable Metrics/AbcSize
236
- @target_files = Array(merged[:target_files])
237
- @timeout = merged[:timeout]
238
- @format = merged[:format].to_sym
239
- @target = merged[:target]
240
- @min_score = merged[:min_score].to_f
241
- @integration = validate_integration(merged[:integration])
242
- @verbose = merged[:verbose]
243
- @quiet = merged[:quiet]
244
- @jobs = validate_jobs(merged[:jobs])
245
- @fail_fast = validate_fail_fast(merged[:fail_fast])
246
- @baseline = merged[:baseline]
247
- @isolation = validate_isolation(merged[:isolation])
248
- @incremental = merged[:incremental]
249
- @suggest_tests = merged[:suggest_tests]
250
- @progress = merged[:progress]
251
- @save_session = merged[:save_session]
252
- @line_ranges = merged[:line_ranges] || {}
253
- @spec_files = Array(merged[:spec_files])
254
- @ignore_patterns = validate_ignore_patterns(merged[:ignore_patterns])
255
- @show_disabled = merged[:show_disabled]
256
- @baseline_session = merged[:baseline_session]
257
- @skip_heredoc_literals = merged[:skip_heredoc_literals]
258
- @related_specs_heuristic = merged[:related_specs_heuristic]
259
- @fallback_to_full_suite = merged[:fallback_to_full_suite]
260
- @hooks = validate_hooks(merged[:hooks])
261
- @preload = validate_preload(merged[:preload])
262
- @spec_mappings = validate_spec_mappings(merged[:spec_mappings])
263
- @spec_pattern = validate_spec_pattern(merged[:spec_pattern])
191
+ def assign_attributes(merged)
192
+ assign_simple_attributes(merged)
193
+ assign_validated_attributes(merged)
264
194
  assign_example_targeting(merged)
265
- @spec_selector = build_spec_selector
266
- end
267
-
268
- def assign_example_targeting(merged)
269
- @example_targeting = merged[:example_targeting] ? true : false
270
- @example_targeting_fallback = validate_example_targeting_fallback(merged[:example_targeting_fallback])
271
- @example_targeting_cache = validate_example_targeting_cache(merged[:example_targeting_cache])
272
- end
273
-
274
- def build_spec_selector
275
- Evilution::SpecSelector.new(
195
+ @spec_selector = Builders::SpecSelector.call(
276
196
  spec_files: @spec_files,
277
197
  spec_mappings: @spec_mappings,
278
198
  spec_pattern: @spec_pattern,
279
- spec_resolver: build_spec_resolver
199
+ integration: @integration
280
200
  )
281
201
  end
282
202
 
283
- def build_spec_resolver
284
- case @integration
285
- when :minitest
286
- Evilution::SpecResolver.new(test_dir: "test", test_suffix: "_test.rb", request_dir: "integration")
287
- else
288
- Evilution::SpecResolver.new
289
- end
290
- end
291
-
292
- def validate_spec_mappings(value)
293
- return {} if value.nil?
294
-
295
- raise Evilution::ConfigError, "spec_mappings must be a Hash, got #{value.class}" unless value.is_a?(Hash)
296
-
297
- normalized = value.each_with_object({}) do |(source, specs), acc|
298
- key = normalize_spec_mappings_key(source)
299
- acc[key] = normalize_spec_mappings_value(key, specs)
300
- end
301
-
302
- warn_missing_spec_mappings(normalized)
303
- normalized
304
- end
305
-
306
- def normalize_spec_mappings_key(source)
307
- key = source.to_s
308
- key = key.delete_prefix("#{Dir.pwd}/") if key.start_with?("/")
309
- key.delete_prefix("./")
310
- end
311
-
312
- def normalize_spec_mappings_value(source, specs)
313
- case specs
314
- when String then [specs]
315
- when Array
316
- specs.each do |entry|
317
- unless entry.is_a?(String)
318
- raise Evilution::ConfigError,
319
- "spec_mappings[#{source.inspect}] entries must be string paths, got #{entry.class}"
320
- end
321
- end
322
- specs
323
- else
324
- raise Evilution::ConfigError,
325
- "spec_mappings[#{source.inspect}] must be a string or array of strings, got #{specs.class}"
326
- end
327
- end
328
-
329
- def warn_missing_spec_mappings(mappings)
330
- mappings.each do |source, specs|
331
- specs.each do |spec_path|
332
- next if File.exist?(spec_path)
333
-
334
- warn "[evilution] spec_mappings[#{source.inspect}]: #{spec_path} not found, skipping"
335
- end
336
- end
337
- end
338
-
339
- def validate_spec_pattern(value)
340
- return nil if value.nil?
341
- return value if value.is_a?(String)
342
-
343
- raise Evilution::ConfigError, "spec_pattern must be nil or a String glob, got #{value.class}"
344
- end
345
-
346
- def validate_preload(value)
347
- return nil if value.nil?
348
- return false if value == false
349
- return value if value.is_a?(String)
350
-
351
- raise Evilution::ConfigError, "preload must be nil, false, or a String path, got #{value.inspect}"
352
- end
353
-
354
- def validate_integration(value)
355
- raise Evilution::ConfigError, "integration must be rspec or minitest, got nil" if value.nil?
356
-
357
- value = value.to_sym
358
- unless %i[rspec minitest].include?(value)
359
- raise Evilution::ConfigError,
360
- "integration must be rspec or minitest, got #{value.inspect}"
361
- end
362
-
363
- value
364
- end
365
-
366
- def validate_isolation(value)
367
- raise Evilution::ConfigError, "isolation must be auto, fork, or in_process, got nil" if value.nil?
368
-
369
- value = value.to_sym
370
- raise Evilution::ConfigError, "isolation must be auto, fork, or in_process, got #{value.inspect}" unless %i[auto fork
371
- in_process].include?(value)
372
-
373
- value
374
- end
375
-
376
- def validate_jobs(value)
377
- raise Evilution::ConfigError, "jobs must be a positive integer, got #{value.inspect}" if value.is_a?(Float)
378
-
379
- value = Integer(value)
380
- raise Evilution::ConfigError, "jobs must be a positive integer, got #{value}" unless value >= 1
381
-
382
- value
383
- rescue ::ArgumentError, ::TypeError
384
- raise Evilution::ConfigError, "jobs must be a positive integer, got #{value.inspect}"
385
- end
386
-
387
- def validate_ignore_patterns(value)
388
- patterns = Array(value)
389
- patterns.each do |pattern|
390
- unless pattern.is_a?(String)
391
- raise Evilution::ConfigError,
392
- "ignore_patterns must be an array of strings, got #{pattern.class} (#{pattern.inspect})"
393
- end
394
- end
395
- patterns
396
- end
397
-
398
- def validate_example_targeting_fallback(value)
399
- unless value.is_a?(String) || value.is_a?(Symbol)
400
- raise Evilution::ConfigError,
401
- "example_targeting_fallback must be full_file or unresolved, got #{value.inspect}"
402
- end
403
-
404
- sym = value.to_sym
405
- unless EXAMPLE_TARGETING_FALLBACKS.include?(sym)
406
- raise Evilution::ConfigError,
407
- "example_targeting_fallback must be full_file or unresolved, got #{sym.inspect}"
408
- end
409
-
410
- sym
411
- end
412
-
413
- def validate_example_targeting_cache(value)
414
- raise Evilution::ConfigError, "example_targeting_cache must be a Hash, got #{value.class}" unless value.is_a?(Hash)
415
-
416
- normalized = value.each_with_object({}) do |(k, v), acc|
417
- unless k.is_a?(String) || k.is_a?(Symbol)
418
- raise Evilution::ConfigError,
419
- "example_targeting_cache keys must be Strings or Symbols, got #{k.inspect}"
420
- end
421
- acc[k.to_sym] = v
422
- end
423
- merged = DEFAULTS[:example_targeting_cache].merge(normalized)
424
- validate_positive_int!(merged, :max_files)
425
- validate_positive_int!(merged, :max_blocks)
426
- merged
427
- end
428
-
429
- def validate_positive_int!(cache, key)
430
- v = cache[key]
431
- return if v.is_a?(Integer) && v >= 1
432
-
433
- raise Evilution::ConfigError,
434
- "example_targeting_cache.#{key} must be a positive integer, got #{v.inspect}"
203
+ def assign_simple_attributes(merged)
204
+ @target_files = Array(merged[:target_files])
205
+ @timeout = merged[:timeout]
206
+ @format = merged[:format].to_sym
207
+ @target = merged[:target]
208
+ @min_score = merged[:min_score].to_f
209
+ @verbose = merged[:verbose]
210
+ @quiet = merged[:quiet]
211
+ @baseline = merged[:baseline]
212
+ @incremental = merged[:incremental]
213
+ @suggest_tests = merged[:suggest_tests]
214
+ @progress = merged[:progress]
215
+ @save_session = merged[:save_session]
216
+ @line_ranges = merged[:line_ranges] || {}
217
+ @spec_files = Array(merged[:spec_files])
218
+ @show_disabled = merged[:show_disabled]
219
+ @baseline_session = merged[:baseline_session]
220
+ @skip_heredoc_literals = merged[:skip_heredoc_literals]
221
+ @related_specs_heuristic = merged[:related_specs_heuristic]
222
+ @fallback_to_full_suite = merged[:fallback_to_full_suite]
223
+ @quiet_children = merged[:quiet_children]
224
+ @quiet_children_dir = merged[:quiet_children_dir]
435
225
  end
436
226
 
437
- def load_env_options
438
- opts = {}
439
- val = ENV.fetch("EV_DISABLE_EXAMPLE_TARGETING", nil)
440
- opts[:example_targeting] = false if val && !val.empty? && val != "0"
441
- opts
227
+ def assign_validated_attributes(merged)
228
+ @integration = Validators::Integration.call(merged[:integration])
229
+ @jobs = Validators::Jobs.call(merged[:jobs])
230
+ @fail_fast = Validators::FailFast.call(merged[:fail_fast])
231
+ @isolation = Validators::Isolation.call(merged[:isolation])
232
+ @ignore_patterns = Validators::IgnorePatterns.call(merged[:ignore_patterns])
233
+ @hooks = Validators::Hooks.call(merged[:hooks])
234
+ @preload = Validators::Preload.call(merged[:preload])
235
+ @spec_mappings = Validators::SpecMappings.call(merged[:spec_mappings])
236
+ @spec_pattern = Validators::SpecPattern.call(merged[:spec_pattern])
442
237
  end
443
238
 
444
- def validate_hooks(value)
445
- return {} if value.nil?
446
- raise Evilution::ConfigError, "hooks must be a mapping of event names to file paths, got #{value.class}" unless value.is_a?(Hash)
447
-
448
- value
449
- end
450
-
451
- def load_config_file
452
- self.class.file_options
239
+ def assign_example_targeting(merged)
240
+ @example_targeting = merged[:example_targeting] ? true : false
241
+ @example_targeting_fallback = Validators::ExampleTargetingFallback.call(merged[:example_targeting_fallback])
242
+ @example_targeting_cache = Validators::ExampleTargetingCache.call(merged[:example_targeting_cache])
453
243
  end
454
244
  end
245
+
246
+ require_relative "config/file_loader"
247
+ require_relative "config/env_loader"
248
+ require_relative "config/sources"
249
+ require_relative "config/validators"
250
+ require_relative "config/validators/base"
251
+ require_relative "config/validators/integration"
252
+ require_relative "config/validators/isolation"
253
+ require_relative "config/validators/jobs"
254
+ require_relative "config/validators/fail_fast"
255
+ require_relative "config/validators/preload"
256
+ require_relative "config/validators/hooks"
257
+ require_relative "config/validators/ignore_patterns"
258
+ require_relative "config/validators/spec_pattern"
259
+ require_relative "config/validators/spec_mappings"
260
+ require_relative "config/validators/example_targeting_fallback"
261
+ require_relative "config/validators/example_targeting_cache"
262
+ require_relative "config/builders"
263
+ require_relative "config/builders/spec_resolver"
264
+ require_relative "config/builders/spec_selector"
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../feedback"
4
+
5
+ module Evilution::Feedback::Detector
6
+ module_function
7
+
8
+ def friction?(summary)
9
+ return false if summary.nil?
10
+
11
+ summary.errors.positive? ||
12
+ summary.unparseable.positive? ||
13
+ summary.unresolved.positive?
14
+ end
15
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../feedback"
4
+ require_relative "../version"
5
+
6
+ module Evilution::Feedback::Messages
7
+ module_function
8
+
9
+ def cli_footer
10
+ "> Hit friction with evilution v#{Evilution::VERSION}? " \
11
+ "Share details: #{Evilution::Feedback::DISCUSSION_URL}"
12
+ end
13
+
14
+ def mcp_hint
15
+ "Feedback welcome on errors, usage problems, friction, or missing " \
16
+ "capabilities you wish were there. Do NOT post on the user's " \
17
+ "behalf without explicit user permission. Show the user exactly " \
18
+ "what you would post and only submit after their explicit approval. " \
19
+ "Never include secrets, env vars, project name, file paths, source " \
20
+ "code, or class/method names from user code."
21
+ end
22
+
23
+ def info_guidance
24
+ <<~GUIDANCE.chomp
25
+ The maintainer welcomes feedback in four cases:
26
+ 1. Errors raised by evilution
27
+ 2. Usage problems (confusing behavior, unexpected output)
28
+ 3. Friction (slow runs, awkward workflow, hard-to-interpret output)
29
+ 4. Missing capabilities or features you wish were there
30
+
31
+ CONSENT (required):
32
+ Never post on the user's behalf without explicit user permission.
33
+ Show the user exactly what you would post. Submit only after the
34
+ user explicitly approves.
35
+
36
+ PRIVACY (required):
37
+ Never include secrets, tokens, environment variables, the project
38
+ name, absolute or project-relative file paths, source code, or
39
+ class/method names from user code. The feedback channel is public.
40
+ GUIDANCE
41
+ end
42
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution::Feedback
4
+ DISCUSSION_URL = "https://github.com/marinazzio/evilution/discussions"
5
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../rspec"
4
+
5
+ class Evilution::Integration::RSpec::BaselineRunner
6
+ def call(spec_file)
7
+ require "rspec/core"
8
+ spec_dir = File.expand_path("spec")
9
+ $LOAD_PATH.unshift(spec_dir) unless $LOAD_PATH.include?(spec_dir)
10
+ ::RSpec.reset
11
+ status = ::RSpec::Core::Runner.run(
12
+ ["--format", "progress", "--no-color", "--order", "defined", spec_file]
13
+ )
14
+ status.zero?
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+ require_relative "../rspec"
5
+ require_relative "../crash_detector"
6
+
7
+ class Evilution::Integration::RSpec::CrashDetectorLifecycle
8
+ def current
9
+ if @detector
10
+ @detector.reset
11
+ else
12
+ @detector = Evilution::Integration::CrashDetector.new(StringIO.new)
13
+ ::RSpec.configuration.add_formatter(@detector)
14
+ end
15
+ @detector
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../rspec"
4
+
5
+ module Evilution::Integration::RSpec::ExampleFilterApplier
6
+ class Identity
7
+ def call(_mutation, files)
8
+ files
9
+ end
10
+ end
11
+
12
+ class Custom
13
+ def initialize(filter)
14
+ @filter = filter
15
+ end
16
+
17
+ def call(mutation, files)
18
+ @filter.call(mutation, files)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../rspec"
4
+ require_relative "../crash_detector"
5
+
6
+ class Evilution::Integration::RSpec::FrameworkLoader
7
+ def loaded?
8
+ @loaded == true
9
+ end
10
+
11
+ def call
12
+ return if @loaded
13
+
14
+ require "rspec/core"
15
+ add_spec_load_path
16
+ Evilution::Integration::CrashDetector.register_with_rspec
17
+ @loaded = true
18
+ rescue LoadError => e
19
+ raise Evilution::Error, "rspec-core is required but not available: #{e.message}"
20
+ end
21
+
22
+ private
23
+
24
+ def add_spec_load_path
25
+ spec_dir = File.expand_path("spec")
26
+ $LOAD_PATH.unshift(spec_dir) unless $LOAD_PATH.include?(spec_dir)
27
+ end
28
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../rspec"
4
+
5
+ class Evilution::Integration::RSpec::ResultBuilder
6
+ def unresolved(mutation)
7
+ {
8
+ passed: false,
9
+ unresolved: true,
10
+ error: "no matching spec resolved for #{mutation.file_path}",
11
+ test_command: "rspec (skipped: no spec resolved for #{mutation.file_path})"
12
+ }
13
+ end
14
+
15
+ def unresolved_example(mutation)
16
+ {
17
+ passed: false,
18
+ unresolved: true,
19
+ error: "no matching example found for #{mutation.file_path}",
20
+ test_command: "rspec (skipped: no matching example for #{mutation.file_path})"
21
+ }
22
+ end
23
+
24
+ def from_run(status, command, detector)
25
+ return { passed: true, test_command: command } if status.zero?
26
+
27
+ if detector.only_crashes?
28
+ classes = detector.unique_crash_classes
29
+ return {
30
+ passed: false,
31
+ test_crashed: true,
32
+ error: "test crashes: #{detector.crash_summary}",
33
+ error_class: (classes.first if classes.length == 1),
34
+ test_command: command
35
+ }
36
+ end
37
+
38
+ { passed: false, test_command: command }
39
+ end
40
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../rspec"
4
+
5
+ class Evilution::Integration::RSpec::StateGuard; end unless defined?(Evilution::Integration::RSpec::StateGuard) # rubocop:disable Lint/EmptyClass
6
+
7
+ class Evilution::Integration::RSpec::StateGuard::ExampleGroupsConstants
8
+ def snapshot
9
+ return nil unless defined?(::RSpec::ExampleGroups)
10
+
11
+ Set.new(::RSpec::ExampleGroups.constants(false))
12
+ end
13
+
14
+ def release(before)
15
+ return unless before
16
+ return unless defined?(::RSpec::ExampleGroups)
17
+
18
+ ::RSpec::ExampleGroups.constants(false).each do |c|
19
+ next if before.include?(c)
20
+
21
+ begin
22
+ ::RSpec::ExampleGroups.send(:remove_const, c)
23
+ rescue NameError
24
+ next
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../rspec"
4
+
5
+ class Evilution::Integration::RSpec::StateGuard; end unless defined?(Evilution::Integration::RSpec::StateGuard) # rubocop:disable Lint/EmptyClass
6
+
7
+ module Evilution::Integration::RSpec::StateGuard::Internals
8
+ module_function
9
+
10
+ def world_ivar(name)
11
+ world = ::RSpec.world
12
+ world.instance_variable_defined?(name) ? world.instance_variable_get(name) : nil
13
+ end
14
+
15
+ def config_ivar(name)
16
+ config = ::RSpec.configuration
17
+ config.instance_variable_defined?(name) ? config.instance_variable_get(name) : nil
18
+ end
19
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../rspec"
4
+
5
+ class Evilution::Integration::RSpec::StateGuard; end unless defined?(Evilution::Integration::RSpec::StateGuard) # rubocop:disable Lint/EmptyClass
6
+
7
+ class Evilution::Integration::RSpec::StateGuard::ObjectSpaceExampleGroups
8
+ def snapshot
9
+ groups = Set.new
10
+ ObjectSpace.each_object(Class) do |klass|
11
+ groups << klass.object_id if klass < ::RSpec::Core::ExampleGroup
12
+ rescue TypeError # rubocop:disable Lint/SuppressedException
13
+ end
14
+ groups
15
+ end
16
+
17
+ def release(eg_before)
18
+ return unless eg_before
19
+
20
+ ObjectSpace.each_object(Class) do |klass|
21
+ next unless klass < ::RSpec::Core::ExampleGroup
22
+ next if eg_before.include?(klass.object_id)
23
+
24
+ klass.constants(false).each do |const|
25
+ klass.send(:remove_const, const)
26
+ rescue NameError # rubocop:disable Lint/SuppressedException
27
+ end
28
+
29
+ klass.instance_variables.each do |ivar|
30
+ klass.remove_instance_variable(ivar)
31
+ end
32
+ rescue TypeError # rubocop:disable Lint/SuppressedException
33
+ end
34
+ end
35
+ end