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.
Files changed (159) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +23 -0
  3. data/.rubocop_todo.yml +6 -0
  4. data/CHANGELOG.md +54 -0
  5. data/README.md +76 -3
  6. data/lib/evilution/baseline.rb +5 -4
  7. data/lib/evilution/cache.rb +2 -0
  8. data/lib/evilution/child_output.rb +24 -0
  9. data/lib/evilution/cli/commands/run.rb +9 -0
  10. data/lib/evilution/cli/commands/version.rb +2 -0
  11. data/lib/evilution/cli/parser/options_builder.rb +23 -2
  12. data/lib/evilution/compare/diff_extractor/evilution.rb +22 -0
  13. data/lib/evilution/compare/diff_extractor/mutant.rb +30 -0
  14. data/lib/evilution/compare/diff_extractor.rb +6 -0
  15. data/lib/evilution/compare/fingerprint.rb +15 -72
  16. data/lib/evilution/compare/line_normalizer.rb +72 -0
  17. data/lib/evilution/compare/normalizer.rb +17 -4
  18. data/lib/evilution/config/builders/spec_resolver.rb +15 -0
  19. data/lib/evilution/config/builders/spec_selector.rb +16 -0
  20. data/lib/evilution/config/builders.rb +4 -0
  21. data/lib/evilution/config/env_loader.rb +12 -0
  22. data/lib/evilution/config/file_loader.rb +22 -0
  23. data/lib/evilution/config/sources.rb +14 -0
  24. data/lib/evilution/config/validators/base.rb +37 -0
  25. data/lib/evilution/config/validators/example_targeting_cache.rb +37 -0
  26. data/lib/evilution/config/validators/example_targeting_fallback.rb +22 -0
  27. data/lib/evilution/config/validators/fail_fast.rb +11 -0
  28. data/lib/evilution/config/validators/hooks.rb +12 -0
  29. data/lib/evilution/config/validators/ignore_patterns.rb +16 -0
  30. data/lib/evilution/config/validators/integration.rb +11 -0
  31. data/lib/evilution/config/validators/isolation.rb +19 -0
  32. data/lib/evilution/config/validators/jobs.rb +9 -0
  33. data/lib/evilution/config/validators/preload.rb +13 -0
  34. data/lib/evilution/config/validators/profile.rb +11 -0
  35. data/lib/evilution/config/validators/spec_mappings.rb +56 -0
  36. data/lib/evilution/config/validators/spec_pattern.rb +12 -0
  37. data/lib/evilution/config/validators.rb +4 -0
  38. data/lib/evilution/config.rb +93 -266
  39. data/lib/evilution/feedback/detector.rb +15 -0
  40. data/lib/evilution/feedback/messages.rb +42 -0
  41. data/lib/evilution/feedback.rb +5 -0
  42. data/lib/evilution/integration/crash_detector.rb +2 -2
  43. data/lib/evilution/integration/loading/source_evaluator.rb +6 -2
  44. data/lib/evilution/integration/minitest_crash_detector.rb +2 -2
  45. data/lib/evilution/integration/rspec/baseline_runner.rb +16 -0
  46. data/lib/evilution/integration/rspec/crash_detector_lifecycle.rb +17 -0
  47. data/lib/evilution/integration/rspec/example_filter_applier.rb +21 -0
  48. data/lib/evilution/integration/rspec/framework_loader.rb +28 -0
  49. data/lib/evilution/integration/rspec/result_builder.rb +40 -0
  50. data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +28 -0
  51. data/lib/evilution/integration/rspec/state_guard/internals.rb +19 -0
  52. data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +43 -0
  53. data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +32 -0
  54. data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +20 -0
  55. data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +20 -0
  56. data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +20 -0
  57. data/lib/evilution/integration/rspec/state_guard.rb +40 -0
  58. data/lib/evilution/integration/rspec/test_file_resolver.rb +30 -0
  59. data/lib/evilution/integration/rspec/unresolved_spec_warner.rb +18 -0
  60. data/lib/evilution/integration/rspec.rb +61 -232
  61. data/lib/evilution/isolation/fork.rb +23 -13
  62. data/lib/evilution/isolation/in_process.rb +10 -6
  63. data/lib/evilution/mcp/info_tool/actions/base.rb +22 -0
  64. data/lib/evilution/mcp/info_tool/actions/environment.rb +42 -0
  65. data/lib/evilution/mcp/info_tool/actions/feedback.rb +16 -0
  66. data/lib/evilution/mcp/info_tool/actions/statuses.rb +10 -0
  67. data/lib/evilution/mcp/info_tool/actions/subjects.rb +47 -0
  68. data/lib/evilution/mcp/info_tool/actions/tests.rb +60 -0
  69. data/lib/evilution/mcp/info_tool/actions.rb +16 -0
  70. data/lib/evilution/mcp/info_tool/config_factory.rb +24 -0
  71. data/lib/evilution/mcp/info_tool/error_mapper.rb +15 -0
  72. data/lib/evilution/mcp/info_tool/request_parser.rb +34 -0
  73. data/lib/evilution/mcp/info_tool/response_formatter.rb +24 -0
  74. data/lib/evilution/mcp/info_tool/status_glossary.rb +75 -0
  75. data/lib/evilution/mcp/info_tool.rb +43 -263
  76. data/lib/evilution/mcp/mutate_tool/error_payload.rb +8 -1
  77. data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +5 -1
  78. data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +13 -1
  79. data/lib/evilution/mcp/mutate_tool.rb +5 -2
  80. data/lib/evilution/mcp/session_tool.rb +0 -2
  81. data/lib/evilution/mutation.rb +47 -27
  82. data/lib/evilution/mutator/base.rb +8 -8
  83. data/lib/evilution/mutator/operator/block_removal.rb +1 -1
  84. data/lib/evilution/mutator/operator/method_body_replacement.rb +18 -2
  85. data/lib/evilution/mutator/operator/predicate_to_nil.rb +20 -0
  86. data/lib/evilution/mutator/registry.rb +20 -0
  87. data/lib/evilution/parallel/work_queue/channel/frame.rb +25 -0
  88. data/lib/evilution/parallel/work_queue/channel.rb +23 -0
  89. data/lib/evilution/parallel/work_queue/collection_state.rb +14 -0
  90. data/lib/evilution/parallel/work_queue/dispatcher.rb +133 -0
  91. data/lib/evilution/parallel/work_queue/validators/optional_positive_int.rb +11 -0
  92. data/lib/evilution/parallel/work_queue/validators/optional_positive_number.rb +11 -0
  93. data/lib/evilution/parallel/work_queue/validators/positive_int.rb +11 -0
  94. data/lib/evilution/parallel/work_queue/validators.rb +6 -0
  95. data/lib/evilution/parallel/work_queue/worker/loop.rb +45 -0
  96. data/lib/evilution/parallel/work_queue/worker.rb +114 -0
  97. data/lib/evilution/parallel/work_queue/worker_stat.rb +17 -0
  98. data/lib/evilution/parallel/work_queue.rb +42 -327
  99. data/lib/evilution/process_cleanup.rb +19 -0
  100. data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +18 -0
  101. data/lib/evilution/reporter/cli/item_formatters/disabled.rb +9 -0
  102. data/lib/evilution/reporter/cli/item_formatters/error.rb +14 -0
  103. data/lib/evilution/reporter/cli/item_formatters/result_location.rb +10 -0
  104. data/lib/evilution/reporter/cli/item_formatters.rb +6 -0
  105. data/lib/evilution/reporter/cli/line_formatters/duration.rb +9 -0
  106. data/lib/evilution/reporter/cli/line_formatters/efficiency.rb +18 -0
  107. data/lib/evilution/reporter/cli/line_formatters/feedback_footer.rb +13 -0
  108. data/lib/evilution/reporter/cli/line_formatters/header.rb +10 -0
  109. data/lib/evilution/reporter/cli/line_formatters/mutations.rb +16 -0
  110. data/lib/evilution/reporter/cli/line_formatters/peak_memory.rb +12 -0
  111. data/lib/evilution/reporter/cli/line_formatters/result_line.rb +20 -0
  112. data/lib/evilution/reporter/cli/line_formatters/score.rb +14 -0
  113. data/lib/evilution/reporter/cli/line_formatters/truncation_notice.rb +11 -0
  114. data/lib/evilution/reporter/cli/line_formatters.rb +6 -0
  115. data/lib/evilution/reporter/cli/metrics_block.rb +26 -0
  116. data/lib/evilution/reporter/cli/pct.rb +9 -0
  117. data/lib/evilution/reporter/cli/section.rb +13 -0
  118. data/lib/evilution/reporter/cli/section_renderer.rb +15 -0
  119. data/lib/evilution/reporter/cli/trailer.rb +22 -0
  120. data/lib/evilution/reporter/cli.rb +79 -162
  121. data/lib/evilution/reporter/html/baseline_keys.rb +1 -1
  122. data/lib/evilution/reporter/html/diff_formatter.rb +1 -1
  123. data/lib/evilution/reporter/html/escape.rb +1 -1
  124. data/lib/evilution/reporter/html/section.rb +1 -1
  125. data/lib/evilution/reporter/html/sections.rb +4 -2
  126. data/lib/evilution/reporter/html/stylesheet.rb +1 -1
  127. data/lib/evilution/reporter/html.rb +8 -3
  128. data/lib/evilution/reporter/suggestion/registry.rb +1 -5
  129. data/lib/evilution/reporter/suggestion/templates/generic.rb +1 -1
  130. data/lib/evilution/reporter/suggestion/templates/minitest.rb +349 -643
  131. data/lib/evilution/reporter/suggestion/templates/rspec.rb +351 -598
  132. data/lib/evilution/reporter/suggestion/templates.rb +6 -0
  133. data/lib/evilution/result/error_info.rb +20 -0
  134. data/lib/evilution/result/memory_stats.rb +20 -0
  135. data/lib/evilution/result/mutation_result.rb +30 -14
  136. data/lib/evilution/runner/baseline_runner.rb +1 -2
  137. data/lib/evilution/runner/diagnostics.rb +1 -2
  138. data/lib/evilution/runner/isolation_resolver.rb +10 -4
  139. data/lib/evilution/runner/mutation_executor/mutation_runner.rb +30 -0
  140. data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +15 -0
  141. data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +39 -0
  142. data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +68 -0
  143. data/lib/evilution/runner/mutation_executor/neutralizer.rb +11 -0
  144. data/lib/evilution/runner/mutation_executor/result_cache.rb +67 -0
  145. data/lib/evilution/runner/mutation_executor/result_notifier.rb +46 -0
  146. data/lib/evilution/runner/mutation_executor/result_packer.rb +41 -0
  147. data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +78 -0
  148. data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +32 -0
  149. data/lib/evilution/runner/mutation_executor/strategy.rb +11 -0
  150. data/lib/evilution/runner/mutation_executor.rb +53 -292
  151. data/lib/evilution/runner/mutation_planner.rb +1 -2
  152. data/lib/evilution/runner/report_publisher.rb +1 -2
  153. data/lib/evilution/runner/subject_pipeline.rb +1 -2
  154. data/lib/evilution/runner.rb +53 -30
  155. data/lib/evilution/version.rb +1 -1
  156. data/lib/evilution.rb +1 -0
  157. data/script/memory_check +3 -1
  158. metadata +125 -3
  159. data/lib/evilution/reporter/html/namespace.rb +0 -11
@@ -8,40 +8,19 @@ 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",
21
+ profile: :default
40
22
  }.freeze
41
23
 
42
- EXAMPLE_TARGETING_FALLBACKS = %i[full_file unresolved].freeze
43
- private_constant :EXAMPLE_TARGETING_FALLBACKS
44
-
45
24
  attr_reader :target_files, :timeout, :format,
46
25
  :target, :min_score, :integration, :verbose, :quiet,
47
26
  :jobs, :fail_fast, :baseline, :isolation, :incremental, :suggest_tests,
@@ -50,12 +29,11 @@ class Evilution::Config
50
29
  :skip_heredoc_literals, :related_specs_heuristic,
51
30
  :fallback_to_full_suite, :preload, :spec_mappings, :spec_pattern,
52
31
  :example_targeting, :example_targeting_fallback, :example_targeting_cache,
53
- :spec_selector
32
+ :spec_selector, :quiet_children, :quiet_children_dir, :profile
54
33
 
55
34
  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)
35
+ skip_file = options.delete(:skip_config_file) ? true : false
36
+ merged = Sources.merge(explicit: options, skip_file: skip_file)
59
37
  assign_attributes(merged)
60
38
  freeze
61
39
  end
@@ -125,18 +103,7 @@ class Evilution::Config
125
103
  end
126
104
 
127
105
  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
- {}
106
+ FileLoader.load
140
107
  end
141
108
 
142
109
  # Generates a default config file template.
@@ -185,9 +152,10 @@ class Evilution::Config
185
152
  # fallback_to_full_suite: false
186
153
 
187
154
  # 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
155
+ # For Rails projects, the autodetect chain tries (in order):
156
+ # spec/rails_helper.rb -> spec/spec_helper.rb -> test/test_helper.rb
157
+ # when isolation resolves to :fork. Set to false to disable.
158
+ # preload: spec/rails_helper.rb # or spec/spec_helper.rb, test/test_helper.rb
191
159
 
192
160
  # Hooks: Ruby files returning a Proc, keyed by lifecycle event
193
161
  # hooks:
@@ -216,239 +184,98 @@ class Evilution::Config
216
184
  # ignore_patterns:
217
185
  # - "call{name=info, receiver=call{name=logger}}"
218
186
  # - "call{name=debug|warn}"
187
+
188
+ # Operator profile: default or strict (default: default).
189
+ # strict adds aggressive truthiness mutators (e.g. replaces
190
+ # `x.predicate?` with `nil`) intended for pre-merge audits.
191
+ # profile: default
219
192
  YAML
220
193
  end
221
194
 
222
195
  private
223
196
 
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])
197
+ def assign_attributes(merged)
198
+ assign_simple_attributes(merged)
199
+ assign_validated_attributes(merged)
264
200
  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(
201
+ @spec_selector = Builders::SpecSelector.call(
276
202
  spec_files: @spec_files,
277
203
  spec_mappings: @spec_mappings,
278
204
  spec_pattern: @spec_pattern,
279
- spec_resolver: build_spec_resolver
205
+ integration: @integration
280
206
  )
281
207
  end
282
208
 
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)
209
+ SIMPLE_ATTR_TRANSFORMS = {
210
+ target_files: ->(v) { Array(v) },
211
+ timeout: nil,
212
+ format: :to_sym.to_proc,
213
+ target: nil,
214
+ min_score: :to_f.to_proc,
215
+ verbose: nil,
216
+ quiet: nil,
217
+ baseline: nil,
218
+ incremental: nil,
219
+ suggest_tests: nil,
220
+ progress: nil,
221
+ save_session: nil,
222
+ line_ranges: ->(v) { v || {} },
223
+ spec_files: ->(v) { Array(v) },
224
+ show_disabled: nil,
225
+ baseline_session: nil,
226
+ skip_heredoc_literals: nil,
227
+ related_specs_heuristic: nil,
228
+ fallback_to_full_suite: nil,
229
+ quiet_children: nil,
230
+ quiet_children_dir: nil
231
+ }.freeze
232
+ private_constant :SIMPLE_ATTR_TRANSFORMS
415
233
 
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
234
+ def assign_simple_attributes(merged)
235
+ SIMPLE_ATTR_TRANSFORMS.each do |key, transform|
236
+ value = merged[key]
237
+ value = transform.call(value) if transform
238
+ instance_variable_set(:"@#{key}", value)
422
239
  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
240
  end
428
241
 
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}"
242
+ def assign_validated_attributes(merged)
243
+ @integration = Validators::Integration.call(merged[:integration])
244
+ @jobs = Validators::Jobs.call(merged[:jobs])
245
+ @fail_fast = Validators::FailFast.call(merged[:fail_fast])
246
+ @isolation = Validators::Isolation.call(merged[:isolation])
247
+ @ignore_patterns = Validators::IgnorePatterns.call(merged[:ignore_patterns])
248
+ @hooks = Validators::Hooks.call(merged[:hooks])
249
+ @preload = Validators::Preload.call(merged[:preload])
250
+ @spec_mappings = Validators::SpecMappings.call(merged[:spec_mappings])
251
+ @spec_pattern = Validators::SpecPattern.call(merged[:spec_pattern])
252
+ @profile = Validators::Profile.call(merged[:profile])
435
253
  end
436
254
 
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
442
- end
443
-
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
255
+ def assign_example_targeting(merged)
256
+ @example_targeting = merged[:example_targeting] ? true : false
257
+ @example_targeting_fallback = Validators::ExampleTargetingFallback.call(merged[:example_targeting_fallback])
258
+ @example_targeting_cache = Validators::ExampleTargetingCache.call(merged[:example_targeting_cache])
453
259
  end
454
260
  end
261
+
262
+ require_relative "config/file_loader"
263
+ require_relative "config/env_loader"
264
+ require_relative "config/sources"
265
+ require_relative "config/validators"
266
+ require_relative "config/validators/base"
267
+ require_relative "config/validators/integration"
268
+ require_relative "config/validators/isolation"
269
+ require_relative "config/validators/jobs"
270
+ require_relative "config/validators/fail_fast"
271
+ require_relative "config/validators/preload"
272
+ require_relative "config/validators/hooks"
273
+ require_relative "config/validators/ignore_patterns"
274
+ require_relative "config/validators/spec_pattern"
275
+ require_relative "config/validators/spec_mappings"
276
+ require_relative "config/validators/example_targeting_fallback"
277
+ require_relative "config/validators/example_targeting_cache"
278
+ require_relative "config/validators/profile"
279
+ require_relative "config/builders"
280
+ require_relative "config/builders/spec_resolver"
281
+ 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
@@ -26,11 +26,11 @@ class Evilution::Integration::CrashDetector
26
26
  end
27
27
  end
28
28
 
29
- def has_assertion_failure? # rubocop:disable Naming/PredicatePrefix
29
+ def assertion_failure?
30
30
  @assertion_failures.positive?
31
31
  end
32
32
 
33
- def has_crash? # rubocop:disable Naming/PredicatePrefix
33
+ def crashed?
34
34
  @crashes.any?
35
35
  end
36
36
 
@@ -5,11 +5,15 @@ require_relative "../loading"
5
5
  # Evaluate source with __FILE__ set to the absolute original path so that
6
6
  # `require_relative` and `__dir__` resolve against the real source tree, where
7
7
  # sibling files actually exist.
8
+ #
9
+ # Trust boundary: `source` is never user-supplied. It is always the original
10
+ # on-disk source from a file the user already pointed Evilution at, with
11
+ # byte-level mutations applied by AST::SourceSurgeon. The only difference
12
+ # between this eval path and a plain `require` of the same file is that we
13
+ # substitute the mutated bytes — the privilege level is identical.
8
14
  class Evilution::Integration::Loading::SourceEvaluator
9
15
  def call(source, file_path)
10
16
  absolute = File.expand_path(file_path)
11
- # rubocop:disable Security/Eval
12
17
  eval(source, TOPLEVEL_BINDING, absolute, 1)
13
- # rubocop:enable Security/Eval
14
18
  end
15
19
  end
@@ -34,11 +34,11 @@ class Evilution::Integration::MinitestCrashDetector
34
34
  end
35
35
  end
36
36
 
37
- def has_assertion_failure? # rubocop:disable Naming/PredicatePrefix
37
+ def assertion_failure?
38
38
  @assertion_failures.positive?
39
39
  end
40
40
 
41
- def has_crash? # rubocop:disable Naming/PredicatePrefix
41
+ def crashed?
42
42
  @crashes.any?
43
43
  end
44
44
 
@@ -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