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
data/lib/evilution/config.rb
CHANGED
|
@@ -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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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,
|
|
189
|
-
#
|
|
190
|
-
#
|
|
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
|
|
225
|
-
|
|
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 =
|
|
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
|
-
|
|
199
|
+
integration: @integration
|
|
280
200
|
)
|
|
281
201
|
end
|
|
282
202
|
|
|
283
|
-
def
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
|
445
|
-
|
|
446
|
-
|
|
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,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
|