evilution 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.beads/interactions.jsonl +23 -0
- data/.rubocop_todo.yml +6 -0
- data/CHANGELOG.md +54 -0
- data/README.md +76 -3
- data/lib/evilution/baseline.rb +5 -4
- data/lib/evilution/cache.rb +2 -0
- data/lib/evilution/child_output.rb +24 -0
- data/lib/evilution/cli/commands/run.rb +9 -0
- data/lib/evilution/cli/commands/version.rb +2 -0
- data/lib/evilution/cli/parser/options_builder.rb +23 -2
- data/lib/evilution/compare/diff_extractor/evilution.rb +22 -0
- data/lib/evilution/compare/diff_extractor/mutant.rb +30 -0
- data/lib/evilution/compare/diff_extractor.rb +6 -0
- data/lib/evilution/compare/fingerprint.rb +15 -72
- data/lib/evilution/compare/line_normalizer.rb +72 -0
- data/lib/evilution/compare/normalizer.rb +17 -4
- data/lib/evilution/config/builders/spec_resolver.rb +15 -0
- data/lib/evilution/config/builders/spec_selector.rb +16 -0
- data/lib/evilution/config/builders.rb +4 -0
- data/lib/evilution/config/env_loader.rb +12 -0
- data/lib/evilution/config/file_loader.rb +22 -0
- data/lib/evilution/config/sources.rb +14 -0
- data/lib/evilution/config/validators/base.rb +37 -0
- data/lib/evilution/config/validators/example_targeting_cache.rb +37 -0
- data/lib/evilution/config/validators/example_targeting_fallback.rb +22 -0
- data/lib/evilution/config/validators/fail_fast.rb +11 -0
- data/lib/evilution/config/validators/hooks.rb +12 -0
- data/lib/evilution/config/validators/ignore_patterns.rb +16 -0
- data/lib/evilution/config/validators/integration.rb +11 -0
- data/lib/evilution/config/validators/isolation.rb +19 -0
- data/lib/evilution/config/validators/jobs.rb +9 -0
- data/lib/evilution/config/validators/preload.rb +13 -0
- data/lib/evilution/config/validators/profile.rb +11 -0
- data/lib/evilution/config/validators/spec_mappings.rb +56 -0
- data/lib/evilution/config/validators/spec_pattern.rb +12 -0
- data/lib/evilution/config/validators.rb +4 -0
- data/lib/evilution/config.rb +93 -266
- data/lib/evilution/feedback/detector.rb +15 -0
- data/lib/evilution/feedback/messages.rb +42 -0
- data/lib/evilution/feedback.rb +5 -0
- data/lib/evilution/integration/crash_detector.rb +2 -2
- data/lib/evilution/integration/loading/source_evaluator.rb +6 -2
- data/lib/evilution/integration/minitest_crash_detector.rb +2 -2
- data/lib/evilution/integration/rspec/baseline_runner.rb +16 -0
- data/lib/evilution/integration/rspec/crash_detector_lifecycle.rb +17 -0
- data/lib/evilution/integration/rspec/example_filter_applier.rb +21 -0
- data/lib/evilution/integration/rspec/framework_loader.rb +28 -0
- data/lib/evilution/integration/rspec/result_builder.rb +40 -0
- data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +28 -0
- data/lib/evilution/integration/rspec/state_guard/internals.rb +19 -0
- data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +43 -0
- data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +32 -0
- data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +20 -0
- data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +20 -0
- data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +20 -0
- data/lib/evilution/integration/rspec/state_guard.rb +40 -0
- data/lib/evilution/integration/rspec/test_file_resolver.rb +30 -0
- data/lib/evilution/integration/rspec/unresolved_spec_warner.rb +18 -0
- data/lib/evilution/integration/rspec.rb +61 -232
- data/lib/evilution/isolation/fork.rb +23 -13
- data/lib/evilution/isolation/in_process.rb +10 -6
- data/lib/evilution/mcp/info_tool/actions/base.rb +22 -0
- data/lib/evilution/mcp/info_tool/actions/environment.rb +42 -0
- data/lib/evilution/mcp/info_tool/actions/feedback.rb +16 -0
- data/lib/evilution/mcp/info_tool/actions/statuses.rb +10 -0
- data/lib/evilution/mcp/info_tool/actions/subjects.rb +47 -0
- data/lib/evilution/mcp/info_tool/actions/tests.rb +60 -0
- data/lib/evilution/mcp/info_tool/actions.rb +16 -0
- data/lib/evilution/mcp/info_tool/config_factory.rb +24 -0
- data/lib/evilution/mcp/info_tool/error_mapper.rb +15 -0
- data/lib/evilution/mcp/info_tool/request_parser.rb +34 -0
- data/lib/evilution/mcp/info_tool/response_formatter.rb +24 -0
- data/lib/evilution/mcp/info_tool/status_glossary.rb +75 -0
- data/lib/evilution/mcp/info_tool.rb +43 -263
- data/lib/evilution/mcp/mutate_tool/error_payload.rb +8 -1
- data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +5 -1
- data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +13 -1
- data/lib/evilution/mcp/mutate_tool.rb +5 -2
- data/lib/evilution/mcp/session_tool.rb +0 -2
- data/lib/evilution/mutation.rb +47 -27
- data/lib/evilution/mutator/base.rb +8 -8
- data/lib/evilution/mutator/operator/block_removal.rb +1 -1
- data/lib/evilution/mutator/operator/method_body_replacement.rb +18 -2
- data/lib/evilution/mutator/operator/predicate_to_nil.rb +20 -0
- data/lib/evilution/mutator/registry.rb +20 -0
- data/lib/evilution/parallel/work_queue/channel/frame.rb +25 -0
- data/lib/evilution/parallel/work_queue/channel.rb +23 -0
- data/lib/evilution/parallel/work_queue/collection_state.rb +14 -0
- data/lib/evilution/parallel/work_queue/dispatcher.rb +133 -0
- data/lib/evilution/parallel/work_queue/validators/optional_positive_int.rb +11 -0
- data/lib/evilution/parallel/work_queue/validators/optional_positive_number.rb +11 -0
- data/lib/evilution/parallel/work_queue/validators/positive_int.rb +11 -0
- data/lib/evilution/parallel/work_queue/validators.rb +6 -0
- data/lib/evilution/parallel/work_queue/worker/loop.rb +45 -0
- data/lib/evilution/parallel/work_queue/worker.rb +114 -0
- data/lib/evilution/parallel/work_queue/worker_stat.rb +17 -0
- data/lib/evilution/parallel/work_queue.rb +42 -327
- data/lib/evilution/process_cleanup.rb +19 -0
- data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +18 -0
- data/lib/evilution/reporter/cli/item_formatters/disabled.rb +9 -0
- data/lib/evilution/reporter/cli/item_formatters/error.rb +14 -0
- data/lib/evilution/reporter/cli/item_formatters/result_location.rb +10 -0
- data/lib/evilution/reporter/cli/item_formatters.rb +6 -0
- data/lib/evilution/reporter/cli/line_formatters/duration.rb +9 -0
- data/lib/evilution/reporter/cli/line_formatters/efficiency.rb +18 -0
- data/lib/evilution/reporter/cli/line_formatters/feedback_footer.rb +13 -0
- data/lib/evilution/reporter/cli/line_formatters/header.rb +10 -0
- data/lib/evilution/reporter/cli/line_formatters/mutations.rb +16 -0
- data/lib/evilution/reporter/cli/line_formatters/peak_memory.rb +12 -0
- data/lib/evilution/reporter/cli/line_formatters/result_line.rb +20 -0
- data/lib/evilution/reporter/cli/line_formatters/score.rb +14 -0
- data/lib/evilution/reporter/cli/line_formatters/truncation_notice.rb +11 -0
- data/lib/evilution/reporter/cli/line_formatters.rb +6 -0
- data/lib/evilution/reporter/cli/metrics_block.rb +26 -0
- data/lib/evilution/reporter/cli/pct.rb +9 -0
- data/lib/evilution/reporter/cli/section.rb +13 -0
- data/lib/evilution/reporter/cli/section_renderer.rb +15 -0
- data/lib/evilution/reporter/cli/trailer.rb +22 -0
- data/lib/evilution/reporter/cli.rb +79 -162
- data/lib/evilution/reporter/html/baseline_keys.rb +1 -1
- data/lib/evilution/reporter/html/diff_formatter.rb +1 -1
- data/lib/evilution/reporter/html/escape.rb +1 -1
- data/lib/evilution/reporter/html/section.rb +1 -1
- data/lib/evilution/reporter/html/sections.rb +4 -2
- data/lib/evilution/reporter/html/stylesheet.rb +1 -1
- data/lib/evilution/reporter/html.rb +8 -3
- data/lib/evilution/reporter/suggestion/registry.rb +1 -5
- data/lib/evilution/reporter/suggestion/templates/generic.rb +1 -1
- data/lib/evilution/reporter/suggestion/templates/minitest.rb +349 -643
- data/lib/evilution/reporter/suggestion/templates/rspec.rb +351 -598
- data/lib/evilution/reporter/suggestion/templates.rb +6 -0
- data/lib/evilution/result/error_info.rb +20 -0
- data/lib/evilution/result/memory_stats.rb +20 -0
- data/lib/evilution/result/mutation_result.rb +30 -14
- data/lib/evilution/runner/baseline_runner.rb +1 -2
- data/lib/evilution/runner/diagnostics.rb +1 -2
- data/lib/evilution/runner/isolation_resolver.rb +10 -4
- data/lib/evilution/runner/mutation_executor/mutation_runner.rb +30 -0
- data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +15 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +39 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +68 -0
- data/lib/evilution/runner/mutation_executor/neutralizer.rb +11 -0
- data/lib/evilution/runner/mutation_executor/result_cache.rb +67 -0
- data/lib/evilution/runner/mutation_executor/result_notifier.rb +46 -0
- data/lib/evilution/runner/mutation_executor/result_packer.rb +41 -0
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +78 -0
- data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +32 -0
- data/lib/evilution/runner/mutation_executor/strategy.rb +11 -0
- data/lib/evilution/runner/mutation_executor.rb +53 -292
- data/lib/evilution/runner/mutation_planner.rb +1 -2
- data/lib/evilution/runner/report_publisher.rb +1 -2
- data/lib/evilution/runner/subject_pipeline.rb +1 -2
- data/lib/evilution/runner.rb +53 -30
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +1 -0
- data/script/memory_check +3 -1
- metadata +125 -3
- data/lib/evilution/reporter/html/namespace.rb +0 -11
data/lib/evilution/config.rb
CHANGED
|
@@ -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
|
-
|
|
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",
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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,
|
|
189
|
-
#
|
|
190
|
-
#
|
|
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
|
|
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])
|
|
197
|
+
def assign_attributes(merged)
|
|
198
|
+
assign_simple_attributes(merged)
|
|
199
|
+
assign_validated_attributes(merged)
|
|
264
200
|
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(
|
|
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
|
-
|
|
205
|
+
integration: @integration
|
|
280
206
|
)
|
|
281
207
|
end
|
|
282
208
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
|
@@ -26,11 +26,11 @@ class Evilution::Integration::CrashDetector
|
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def
|
|
29
|
+
def assertion_failure?
|
|
30
30
|
@assertion_failures.positive?
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
def
|
|
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
|
|
37
|
+
def assertion_failure?
|
|
38
38
|
@assertion_failures.positive?
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
def
|
|
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
|