evilution 0.23.0 → 0.25.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 +210 -0
- data/CHANGELOG.md +51 -0
- data/README.md +81 -4
- data/exe/evil +6 -0
- data/lib/evilution/ast/source_surgeon.rb +15 -1
- data/lib/evilution/cli/commands/compare.rb +68 -0
- data/lib/evilution/cli/parser/command_extractor.rb +78 -0
- data/lib/evilution/cli/parser/file_args.rb +41 -0
- data/lib/evilution/cli/parser/options_builder.rb +123 -0
- data/lib/evilution/cli/parser/stdin_reader.rb +28 -0
- data/lib/evilution/cli/parser.rb +27 -196
- data/lib/evilution/cli/printers/compare.rb +159 -0
- data/lib/evilution/cli.rb +1 -0
- data/lib/evilution/compare/categorizer.rb +109 -0
- data/lib/evilution/compare/detector.rb +21 -0
- data/lib/evilution/compare/fingerprint.rb +83 -0
- data/lib/evilution/compare/normalizer.rb +106 -0
- data/lib/evilution/compare/record.rb +16 -0
- data/lib/evilution/compare.rb +15 -0
- data/lib/evilution/config.rb +178 -3
- data/lib/evilution/example_filter.rb +143 -0
- data/lib/evilution/integration/base.rb +11 -57
- data/lib/evilution/integration/crash_detector.rb +5 -2
- data/lib/evilution/integration/minitest.rb +25 -7
- data/lib/evilution/integration/minitest_crash_detector.rb +5 -2
- data/lib/evilution/integration/rspec.rb +99 -12
- data/lib/evilution/isolation/fork.rb +26 -0
- data/lib/evilution/isolation/in_process.rb +1 -0
- data/lib/evilution/mcp/info_tool.rb +77 -5
- data/lib/evilution/mcp/mutate_tool/config_builder.rb +20 -0
- data/lib/evilution/mcp/mutate_tool/error_payload.rb +17 -0
- data/lib/evilution/mcp/mutate_tool/option_parser.rb +54 -0
- data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +37 -0
- data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +31 -0
- data/lib/evilution/mcp/mutate_tool/survived_enricher.rb +52 -0
- data/lib/evilution/mcp/mutate_tool.rb +34 -186
- data/lib/evilution/mutation.rb +43 -3
- data/lib/evilution/mutator/base.rb +39 -1
- data/lib/evilution/mutator/operator/argument_nil_substitution.rb +5 -1
- data/lib/evilution/mutator/operator/argument_removal.rb +5 -1
- data/lib/evilution/parallel/work_queue.rb +149 -31
- data/lib/evilution/parallel_db_warning.rb +68 -0
- data/lib/evilution/reporter/cli.rb +38 -11
- data/lib/evilution/reporter/html/assets/style.css +85 -0
- data/lib/evilution/reporter/html/baseline_keys.rb +28 -0
- data/lib/evilution/reporter/html/diff_formatter.rb +27 -0
- data/lib/evilution/reporter/html/escape.rb +12 -0
- data/lib/evilution/reporter/html/namespace.rb +11 -0
- data/lib/evilution/reporter/html/report.rb +68 -0
- data/lib/evilution/reporter/html/section.rb +21 -0
- data/lib/evilution/reporter/html/sections/baseline_comparison.rb +46 -0
- data/lib/evilution/reporter/html/sections/error_details.rb +30 -0
- data/lib/evilution/reporter/html/sections/error_entry.rb +22 -0
- data/lib/evilution/reporter/html/sections/file_section.rb +62 -0
- data/lib/evilution/reporter/html/sections/header.rb +29 -0
- data/lib/evilution/reporter/html/sections/mutation_map.rb +32 -0
- data/lib/evilution/reporter/html/sections/neutral_details.rb +25 -0
- data/lib/evilution/reporter/html/sections/summary_cards.rb +11 -0
- data/lib/evilution/reporter/html/sections/survived_details.rb +35 -0
- data/lib/evilution/reporter/html/sections/survived_entry.rb +36 -0
- data/lib/evilution/reporter/html/sections/truncation_notice.rb +17 -0
- data/lib/evilution/reporter/html/sections/unparseable_details.rb +25 -0
- data/lib/evilution/reporter/html/sections/unresolved_details.rb +25 -0
- data/lib/evilution/reporter/html/sections.rb +4 -0
- data/lib/evilution/reporter/html/stylesheet.rb +14 -0
- data/lib/evilution/reporter/html/templates/baseline_comparison.html.erb +8 -0
- data/lib/evilution/reporter/html/templates/error_details.html.erb +6 -0
- data/lib/evilution/reporter/html/templates/error_entry.html.erb +10 -0
- data/lib/evilution/reporter/html/templates/file_section.html.erb +12 -0
- data/lib/evilution/reporter/html/templates/header.html.erb +4 -0
- data/lib/evilution/reporter/html/templates/mutation_map.html.erb +6 -0
- data/lib/evilution/reporter/html/templates/neutral_details.html.erb +14 -0
- data/lib/evilution/reporter/html/templates/report.html.erb +17 -0
- data/lib/evilution/reporter/html/templates/summary_cards.html.erb +26 -0
- data/lib/evilution/reporter/html/templates/survived_details.html.erb +21 -0
- data/lib/evilution/reporter/html/templates/survived_entry.html.erb +8 -0
- data/lib/evilution/reporter/html/templates/truncation_notice.html.erb +1 -0
- data/lib/evilution/reporter/html/templates/unparseable_details.html.erb +11 -0
- data/lib/evilution/reporter/html/templates/unresolved_details.html.erb +11 -0
- data/lib/evilution/reporter/html.rb +11 -390
- data/lib/evilution/reporter/json.rb +19 -9
- data/lib/evilution/reporter/suggestion/diff_helpers.rb +28 -0
- data/lib/evilution/reporter/suggestion/registry.rb +64 -0
- data/lib/evilution/reporter/suggestion/templates/generic.rb +55 -0
- data/lib/evilution/reporter/suggestion/templates/minitest.rb +659 -0
- data/lib/evilution/reporter/suggestion/templates/rspec.rb +613 -0
- data/lib/evilution/reporter/suggestion.rb +8 -1327
- data/lib/evilution/result/mutation_result.rb +9 -1
- data/lib/evilution/result/summary.rb +21 -1
- data/lib/evilution/runner/baseline_runner.rb +92 -0
- data/lib/evilution/runner/diagnostics.rb +105 -0
- data/lib/evilution/runner/isolation_resolver.rb +134 -0
- data/lib/evilution/runner/mutation_executor.rb +325 -0
- data/lib/evilution/runner/mutation_planner.rb +126 -0
- data/lib/evilution/runner/report_publisher.rb +60 -0
- data/lib/evilution/runner/subject_pipeline.rb +121 -0
- data/lib/evilution/runner.rb +61 -692
- data/lib/evilution/source_ast_cache.rb +39 -0
- data/lib/evilution/spec_ast_cache.rb +166 -0
- data/lib/evilution/spec_resolver.rb +6 -1
- data/lib/evilution/spec_selector.rb +39 -0
- data/lib/evilution/temp_dir_tracker.rb +23 -3
- data/lib/evilution/version.rb +1 -1
- data/script/memory_check +7 -5
- metadata +75 -2
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require_relative "../result"
|
|
4
4
|
|
|
5
5
|
class Evilution::Result::MutationResult
|
|
6
|
-
STATUSES = %i[killed survived timeout error neutral equivalent].freeze
|
|
6
|
+
STATUSES = %i[killed survived timeout error neutral equivalent unresolved unparseable].freeze
|
|
7
7
|
|
|
8
8
|
attr_reader :mutation, :status, :duration, :killing_test, :test_command,
|
|
9
9
|
:child_rss_kb, :memory_delta_kb, :parent_rss_kb,
|
|
@@ -54,4 +54,12 @@ class Evilution::Result::MutationResult
|
|
|
54
54
|
def equivalent?
|
|
55
55
|
status == :equivalent
|
|
56
56
|
end
|
|
57
|
+
|
|
58
|
+
def unresolved?
|
|
59
|
+
status == :unresolved
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def unparseable?
|
|
63
|
+
status == :unparseable
|
|
64
|
+
end
|
|
57
65
|
end
|
|
@@ -47,8 +47,20 @@ class Evilution::Result::Summary
|
|
|
47
47
|
results.count(&:equivalent?)
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
def unresolved
|
|
51
|
+
results.count(&:unresolved?)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def unparseable
|
|
55
|
+
results.count(&:unparseable?)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def score_denominator
|
|
59
|
+
total - errors - neutral - equivalent - unresolved - unparseable
|
|
60
|
+
end
|
|
61
|
+
|
|
50
62
|
def score
|
|
51
|
-
denominator =
|
|
63
|
+
denominator = score_denominator
|
|
52
64
|
return 0.0 if denominator.zero?
|
|
53
65
|
|
|
54
66
|
killed.to_f / denominator
|
|
@@ -74,6 +86,14 @@ class Evilution::Result::Summary
|
|
|
74
86
|
results.select(&:equivalent?)
|
|
75
87
|
end
|
|
76
88
|
|
|
89
|
+
def unresolved_results
|
|
90
|
+
results.select(&:unresolved?)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def unparseable_results
|
|
94
|
+
results.select(&:unparseable?)
|
|
95
|
+
end
|
|
96
|
+
|
|
77
97
|
def coverage_gaps
|
|
78
98
|
Evilution::Result::CoverageGapGrouper.new.call(survived_results)
|
|
79
99
|
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../baseline"
|
|
4
|
+
require_relative "../spec_resolver"
|
|
5
|
+
require_relative "../integration/rspec"
|
|
6
|
+
require_relative "../integration/minitest"
|
|
7
|
+
require_relative "../example_filter"
|
|
8
|
+
require_relative "../spec_ast_cache"
|
|
9
|
+
require_relative "../source_ast_cache"
|
|
10
|
+
|
|
11
|
+
class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
|
|
12
|
+
|
|
13
|
+
unless defined?(Evilution::Runner::INTEGRATIONS)
|
|
14
|
+
Evilution::Runner::INTEGRATIONS = {
|
|
15
|
+
rspec: Evilution::Integration::RSpec,
|
|
16
|
+
minitest: Evilution::Integration::Minitest
|
|
17
|
+
}.freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class Evilution::Runner::BaselineRunner
|
|
21
|
+
def initialize(config, hooks: nil)
|
|
22
|
+
@config = config
|
|
23
|
+
@hooks = hooks
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def integration_class
|
|
27
|
+
@integration_class ||= Evilution::Runner::INTEGRATIONS.fetch(config.integration) do
|
|
28
|
+
raise Evilution::Error, "unknown integration: #{config.integration}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def build_integration
|
|
33
|
+
klass = integration_class
|
|
34
|
+
test_files = config.spec_files.empty? ? nil : config.spec_files
|
|
35
|
+
kwargs = {
|
|
36
|
+
test_files: test_files,
|
|
37
|
+
hooks: hooks,
|
|
38
|
+
fallback_to_full_suite: config.fallback_to_full_suite?,
|
|
39
|
+
spec_selector: config.spec_selector
|
|
40
|
+
}
|
|
41
|
+
if klass == Evilution::Integration::RSpec
|
|
42
|
+
kwargs[:related_specs_heuristic] = config.related_specs_heuristic?
|
|
43
|
+
kwargs[:example_filter] = build_example_filter
|
|
44
|
+
end
|
|
45
|
+
klass.new(**kwargs)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def call(subjects)
|
|
49
|
+
return nil unless config.baseline? && subjects.any?
|
|
50
|
+
|
|
51
|
+
log_start
|
|
52
|
+
baseline = Evilution::Baseline.new(timeout: config.timeout, **integration_class.baseline_options)
|
|
53
|
+
result = baseline.call(subjects)
|
|
54
|
+
log_complete(result)
|
|
55
|
+
result
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def neutralization_resolver
|
|
59
|
+
integration_class.baseline_options[:spec_resolver] || Evilution::SpecResolver.new
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def neutralization_fallback_dir
|
|
63
|
+
integration_class.baseline_options[:fallback_dir] || "spec"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
attr_reader :config, :hooks
|
|
69
|
+
|
|
70
|
+
def build_example_filter
|
|
71
|
+
return nil unless config.example_targeting?
|
|
72
|
+
|
|
73
|
+
Evilution::ExampleFilter.new(
|
|
74
|
+
cache: Evilution::SpecAstCache.new(**config.example_targeting_cache),
|
|
75
|
+
fallback: config.example_targeting_fallback,
|
|
76
|
+
source_cache: Evilution::SourceAstCache.new
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def log_start
|
|
81
|
+
return if config.quiet || !config.text? || !$stderr.tty?
|
|
82
|
+
|
|
83
|
+
$stderr.write("Running baseline test suite...\n")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def log_complete(result)
|
|
87
|
+
return if config.quiet || !config.text? || !$stderr.tty?
|
|
88
|
+
|
|
89
|
+
count = result.failed_spec_files.size
|
|
90
|
+
$stderr.write("Baseline complete: #{count} failing spec file#{"s" unless count == 1}\n")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../memory"
|
|
4
|
+
require_relative "../parallel/pool"
|
|
5
|
+
|
|
6
|
+
class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
|
|
7
|
+
|
|
8
|
+
class Evilution::Runner::Diagnostics
|
|
9
|
+
def initialize(config, stderr: $stderr)
|
|
10
|
+
@config = config
|
|
11
|
+
@stderr = stderr
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def log_memory(phase, context = nil)
|
|
15
|
+
return unless verbose?
|
|
16
|
+
|
|
17
|
+
rss = Evilution::Memory.rss_mb
|
|
18
|
+
return unless rss
|
|
19
|
+
|
|
20
|
+
gc = gc_stats_string
|
|
21
|
+
msg = format("[memory] %<phase>s: %<rss>.1f MB", phase: phase, rss: rss)
|
|
22
|
+
ctx = [context, gc].compact.join(", ")
|
|
23
|
+
msg += " (#{ctx})" unless ctx.empty?
|
|
24
|
+
stderr.write("#{msg}\n")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def log_progress(current, status)
|
|
28
|
+
return unless text_tty?
|
|
29
|
+
|
|
30
|
+
stderr.write("mutation #{current} #{status}\n")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def log_mutation_diagnostics(result)
|
|
34
|
+
return unless verbose?
|
|
35
|
+
|
|
36
|
+
parts = []
|
|
37
|
+
parts << format("child_rss: %<mb>.1f MB", mb: result.child_rss_kb / 1024.0) if result.child_rss_kb
|
|
38
|
+
|
|
39
|
+
if result.memory_delta_kb
|
|
40
|
+
sign = result.memory_delta_kb.negative? ? "" : "+"
|
|
41
|
+
parts << format("delta: %<sign>s%<mb>.1f MB", sign: sign, mb: result.memory_delta_kb / 1024.0)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
parts << gc_stats_string
|
|
45
|
+
|
|
46
|
+
stderr.write("[verbose] #{result.mutation}: #{parts.join(", ")}\n") unless parts.empty?
|
|
47
|
+
|
|
48
|
+
log_mutation_error(result) if result.error?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def log_worker_stats(stats)
|
|
52
|
+
return unless verbose? && stats.any?
|
|
53
|
+
|
|
54
|
+
stats.each do |stat|
|
|
55
|
+
pct = format("%.1f", stat.utilization * 100)
|
|
56
|
+
stderr.write("[verbose] worker #{stat.pid}: #{stat.items_completed} items, utilization #{pct}%\n")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def aggregate_worker_stats(stats)
|
|
61
|
+
return stats if stats.empty?
|
|
62
|
+
|
|
63
|
+
stats.group_by(&:pid).map do |pid, entries|
|
|
64
|
+
Evilution::Parallel::WorkQueue::WorkerStat.new(
|
|
65
|
+
pid,
|
|
66
|
+
entries.sum(&:items_completed),
|
|
67
|
+
entries.sum(&:busy_time),
|
|
68
|
+
entries.sum(&:wall_time)
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
attr_reader :config, :stderr
|
|
76
|
+
|
|
77
|
+
def verbose?
|
|
78
|
+
config.verbose && !config.quiet
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def text_tty?
|
|
82
|
+
!config.quiet && config.text? && stderr.tty?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def log_mutation_error(result)
|
|
86
|
+
header = "[verbose] #{result.mutation}: error"
|
|
87
|
+
header += " #{result.error_class}" if result.error_class
|
|
88
|
+
header += ": #{result.error_message}" if result.error_message
|
|
89
|
+
stderr.write("#{header}\n")
|
|
90
|
+
|
|
91
|
+
Array(result.error_backtrace).first(5).each do |line|
|
|
92
|
+
stderr.write("[verbose] #{line}\n")
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def gc_stats_string
|
|
97
|
+
stats = GC.stat
|
|
98
|
+
format(
|
|
99
|
+
"heap_live_slots: %<live>d, allocated: %<alloc>d, freed: %<freed>d",
|
|
100
|
+
live: stats[:heap_live_slots],
|
|
101
|
+
alloc: stats[:total_allocated_objects],
|
|
102
|
+
freed: stats[:total_freed_objects]
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../isolation/fork"
|
|
4
|
+
require_relative "../isolation/in_process"
|
|
5
|
+
require_relative "../rails_detector"
|
|
6
|
+
|
|
7
|
+
class Evilution::Runner; end unless defined?(Evilution::Runner) # rubocop:disable Lint/EmptyClass
|
|
8
|
+
|
|
9
|
+
class Evilution::Runner::IsolationResolver
|
|
10
|
+
PRELOAD_CANDIDATES = [
|
|
11
|
+
File.join("spec", "rails_helper.rb"),
|
|
12
|
+
File.join("test", "test_helper.rb")
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
def initialize(config, target_files:, hooks:)
|
|
16
|
+
@config = config
|
|
17
|
+
@target_files_callback = target_files
|
|
18
|
+
@hooks = hooks
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def isolator
|
|
22
|
+
@isolator ||= build_isolator
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def rails_root_detected?
|
|
26
|
+
return @rails_root_detected if defined?(@rails_root_detected)
|
|
27
|
+
|
|
28
|
+
@rails_root_detected = !detected_rails_root.nil?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def perform_preload
|
|
32
|
+
return if config.preload == false
|
|
33
|
+
return unless resolve_isolation == :fork
|
|
34
|
+
|
|
35
|
+
path = resolve_preload_path
|
|
36
|
+
return unless path
|
|
37
|
+
|
|
38
|
+
prepare_load_path_for_preload
|
|
39
|
+
require File.expand_path(path)
|
|
40
|
+
rescue ScriptError, StandardError => e
|
|
41
|
+
raise Evilution::ConfigError.new(
|
|
42
|
+
"failed to preload #{path.inspect}: #{e.class}: #{e.message}",
|
|
43
|
+
file: path
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
attr_reader :config, :hooks
|
|
50
|
+
|
|
51
|
+
def target_files
|
|
52
|
+
@target_files ||= @target_files_callback.call
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def build_isolator
|
|
56
|
+
case resolve_isolation
|
|
57
|
+
when :fork then Evilution::Isolation::Fork.new(hooks: hooks)
|
|
58
|
+
when :in_process then Evilution::Isolation::InProcess.new
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def resolve_isolation
|
|
63
|
+
case config.isolation
|
|
64
|
+
when :fork
|
|
65
|
+
:fork
|
|
66
|
+
when :in_process
|
|
67
|
+
warn_in_process_under_rails if rails_root_detected?
|
|
68
|
+
:in_process
|
|
69
|
+
else # :auto
|
|
70
|
+
rails_root_detected? ? :fork : :in_process
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def detected_rails_root
|
|
75
|
+
return @detected_rails_root if defined?(@detected_rails_root)
|
|
76
|
+
|
|
77
|
+
@detected_rails_root = Evilution::RailsDetector.rails_root_for_any(target_files)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Preload files (e.g. spec/rails_helper.rb) typically `require 'spec_helper'`
|
|
81
|
+
# which needs spec/ on $LOAD_PATH, and use `RSpec.configure` which needs
|
|
82
|
+
# rspec/core loaded. The RSpec CLI normally sets this up, but evilution
|
|
83
|
+
# calls Runner.run directly.
|
|
84
|
+
def prepare_load_path_for_preload
|
|
85
|
+
spec_dir = File.expand_path(resolve_spec_dir)
|
|
86
|
+
$LOAD_PATH.unshift(spec_dir) unless $LOAD_PATH.include?(spec_dir)
|
|
87
|
+
require "rspec/core" if config.integration == :rspec
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def resolve_spec_dir
|
|
91
|
+
root = detected_rails_root
|
|
92
|
+
return File.join(root, "spec") if root
|
|
93
|
+
|
|
94
|
+
"spec"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def resolve_preload_path
|
|
98
|
+
if config.preload.is_a?(String)
|
|
99
|
+
unless File.file?(config.preload)
|
|
100
|
+
raise Evilution::ConfigError.new(
|
|
101
|
+
"preload file not found: #{config.preload.inspect}",
|
|
102
|
+
file: config.preload
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
return config.preload
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
root = detected_rails_root
|
|
109
|
+
return nil unless root
|
|
110
|
+
|
|
111
|
+
PRELOAD_CANDIDATES.each do |rel|
|
|
112
|
+
abs = File.join(root, rel)
|
|
113
|
+
return abs if File.file?(abs)
|
|
114
|
+
end
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# When the user explicitly requests InProcess on a Rails project, warn once
|
|
119
|
+
# per run. Rails wraps ActiveRecord transactions in
|
|
120
|
+
# Thread.handle_interrupt(Exception => :never), which defers Timeout's
|
|
121
|
+
# Thread#raise indefinitely — making InProcess unable to kill runaway mutants.
|
|
122
|
+
def warn_in_process_under_rails
|
|
123
|
+
return if config.quiet
|
|
124
|
+
return if @warned_in_process_under_rails
|
|
125
|
+
|
|
126
|
+
@warned_in_process_under_rails = true
|
|
127
|
+
$stderr.write(
|
|
128
|
+
"[evilution] warning: --isolation in_process is unsafe on Rails projects. " \
|
|
129
|
+
"ActiveRecord wraps transactions in Thread.handle_interrupt(Exception => :never), " \
|
|
130
|
+
"which swallows Timeout.timeout and can cause evilution to hang indefinitely on " \
|
|
131
|
+
"mutants that introduce infinite loops. Use --isolation fork for reliable interruption.\n"
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
end
|