evilution 0.28.0 → 0.30.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 +106 -0
- data/.rubocop_todo.yml +7 -0
- data/CHANGELOG.md +49 -0
- data/README.md +194 -8
- data/docs/versioning.md +53 -0
- data/lib/evilution/ast/constant_names.rb +28 -11
- data/lib/evilution/ast/heredoc_span.rb +99 -0
- data/lib/evilution/ast/pattern/parser.rb +29 -17
- data/lib/evilution/baseline.rb +15 -2
- data/lib/evilution/cli/commands/compare.rb +13 -0
- data/lib/evilution/cli/commands/session_diff.rb +6 -4
- data/lib/evilution/cli/commands/subjects.rb +6 -3
- data/lib/evilution/cli/commands/util_mutation.rb +24 -19
- data/lib/evilution/cli/parser/command_extractor.rb +12 -12
- data/lib/evilution/cli/parser/file_args.rb +3 -1
- data/lib/evilution/cli/parser/options_builder.rb +31 -3
- data/lib/evilution/cli/parser/stdin_reader.rb +2 -2
- data/lib/evilution/cli/parser.rb +18 -20
- data/lib/evilution/cli/printers/environment.rb +19 -19
- data/lib/evilution/cli/printers/session_diff.rb +8 -8
- data/lib/evilution/compare/normalizer.rb +10 -5
- data/lib/evilution/config/file_loader.rb +40 -1
- data/lib/evilution/config.rb +21 -11
- data/lib/evilution/disable_comment.rb +21 -12
- data/lib/evilution/equivalent/heuristic/dead_code.rb +8 -1
- data/lib/evilution/feedback/setup_warning.rb +79 -0
- data/lib/evilution/gem_detector.rb +132 -0
- data/lib/evilution/integration/loading/body_call_neutralizer.rb +190 -0
- data/lib/evilution/integration/loading/mutation_applier.rb +35 -15
- data/lib/evilution/integration/loading/redefinition_recovery.rb +58 -1
- data/lib/evilution/integration/minitest.rb +60 -16
- data/lib/evilution/integration/rspec/result_builder.rb +20 -1
- data/lib/evilution/integration/rspec.rb +20 -1
- data/lib/evilution/isolation/fork.rb +104 -27
- data/lib/evilution/mcp/info_tool/actions/subjects.rb +32 -23
- data/lib/evilution/mcp/info_tool/actions/tests.rb +22 -12
- data/lib/evilution/mcp/info_tool/request_parser.rb +3 -1
- data/lib/evilution/mcp/info_tool/response_formatter.rb +14 -1
- data/lib/evilution/mcp/info_tool.rb +10 -2
- data/lib/evilution/mcp/mutate_tool/option_parser.rb +4 -2
- data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +58 -1
- data/lib/evilution/mcp/mutate_tool/survived_enricher.rb +19 -9
- data/lib/evilution/mcp/mutate_tool.rb +49 -17
- data/lib/evilution/mcp/session_tool.rb +34 -22
- data/lib/evilution/mcp.rb +6 -0
- data/lib/evilution/mutation.rb +26 -16
- data/lib/evilution/mutator/base.rb +66 -16
- data/lib/evilution/mutator/operator/argument_method_call_replacement.rb +59 -0
- data/lib/evilution/mutator/operator/argument_nil_substitution.rb +11 -14
- data/lib/evilution/mutator/operator/argument_removal.rb +11 -14
- data/lib/evilution/mutator/operator/begin_unwrap.rb +17 -5
- data/lib/evilution/mutator/operator/bitwise_complement.rb +26 -19
- data/lib/evilution/mutator/operator/block_param_removal.rb +50 -8
- data/lib/evilution/mutator/operator/block_pass_removal.rb +19 -15
- data/lib/evilution/mutator/operator/case_when.rb +7 -5
- data/lib/evilution/mutator/operator/conditional_branch.rb +22 -22
- data/lib/evilution/mutator/operator/equality_to_identity.rb +8 -3
- data/lib/evilution/mutator/operator/explicit_super_mutation.rb +36 -14
- data/lib/evilution/mutator/operator/index_to_at.rb +18 -5
- data/lib/evilution/mutator/operator/index_to_dig.rb +12 -6
- data/lib/evilution/mutator/operator/index_to_fetch.rb +5 -4
- data/lib/evilution/mutator/operator/keyword_argument.rb +30 -25
- data/lib/evilution/mutator/operator/last_expression_removal.rb +46 -0
- data/lib/evilution/mutator/operator/mixin_removal.rb +20 -14
- data/lib/evilution/mutator/operator/multiple_assignment.rb +12 -13
- data/lib/evilution/mutator/operator/receiver_replacement.rb +38 -7
- data/lib/evilution/mutator/operator/regex_simplification.rb +62 -67
- data/lib/evilution/mutator/operator/rescue_body_replacement.rb +9 -8
- data/lib/evilution/mutator/operator/rescue_removal.rb +58 -12
- data/lib/evilution/mutator/operator/splat_operator.rb +28 -1
- data/lib/evilution/mutator/operator/string_literal.rb +83 -6
- data/lib/evilution/mutator/operator/superclass_removal.rb +21 -15
- data/lib/evilution/mutator/registry.rb +2 -0
- data/lib/evilution/parallel/work_queue/dispatcher.rb +15 -8
- data/lib/evilution/parallel/work_queue/worker.rb +10 -7
- data/lib/evilution/parallel/work_queue.rb +35 -18
- data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +13 -8
- data/lib/evilution/reporter/cli/line_formatters/error_rate_warning.rb +29 -0
- data/lib/evilution/reporter/cli/line_formatters/mutations.rb +17 -8
- data/lib/evilution/reporter/cli/metrics_block.rb +2 -0
- data/lib/evilution/reporter/json.rb +54 -18
- data/lib/evilution/reporter/suggestion/diff_helpers.rb +0 -13
- data/lib/evilution/reporter/suggestion/diff_lines.rb +28 -0
- data/lib/evilution/reporter/suggestion/templates/minitest.rb +20 -14
- data/lib/evilution/reporter/suggestion/templates/rspec.rb +19 -13
- data/lib/evilution/result/mutation_result.rb +12 -6
- data/lib/evilution/runner/baseline_runner.rb +20 -9
- data/lib/evilution/runner/diagnostics.rb +13 -9
- data/lib/evilution/runner/isolation_resolver.rb +75 -12
- data/lib/evilution/runner/mutation_executor/result_cache.rb +3 -1
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +32 -10
- data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +1 -1
- data/lib/evilution/runner/mutation_executor.rb +2 -0
- data/lib/evilution/runner/mutation_planner.rb +53 -16
- data/lib/evilution/runner/subject_pipeline.rb +21 -11
- data/lib/evilution/runner.rb +3 -3
- data/lib/evilution/session/diff.rb +15 -6
- data/lib/evilution/session/schema.rb +44 -0
- data/lib/evilution/session/store.rb +5 -1
- data/lib/evilution/spec_ast_cache.rb +26 -12
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +2 -0
- data/schema/evilution.config.schema.json +205 -0
- data/script/build_runtime_snapshot +88 -0
- data/script/memory_check +11 -5
- data/script/run_self_baseline +79 -0
- data/script/run_self_validation +54 -0
- data/scripts/benchmark_density +10 -9
- data/scripts/compare_mutations +38 -21
- data/scripts/mutant_json_adapter +7 -4
- metadata +16 -2
|
@@ -5,14 +5,19 @@ require_relative "../item_formatters"
|
|
|
5
5
|
class Evilution::Reporter::CLI::ItemFormatters::CoverageGap
|
|
6
6
|
def format(gap)
|
|
7
7
|
location = "#{gap.file_path}:#{gap.line}"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
"#{format_header(gap, location)}\n#{format_body(gap)}"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def format_header(gap, location)
|
|
14
|
+
return " #{gap.primary_operator}: #{location} (#{gap.subject_name})" if gap.single?
|
|
15
|
+
|
|
16
|
+
" #{location} (#{gap.subject_name}) [#{gap.count} mutations: #{gap.operator_names.join(", ")}]"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def format_body(gap)
|
|
14
20
|
body = gap.mutation_results.first.mutation.unified_diff || gap.primary_diff
|
|
15
|
-
|
|
16
|
-
"#{header}\n#{indented}"
|
|
21
|
+
body.split("\n").map { |l| " #{l}" }.join("\n")
|
|
17
22
|
end
|
|
18
23
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../line_formatters"
|
|
4
|
+
|
|
5
|
+
# EV-nrgw / GH #1168: Score is `killed / score_denominator` where
|
|
6
|
+
# `score_denominator = total - errors - neutral - equivalent - unresolved -
|
|
7
|
+
# unparseable` — errors are excluded from the denominator entirely. A run
|
|
8
|
+
# can read PASS at 100% while 16 of 19 mutations silently errored. This
|
|
9
|
+
# formatter surfaces a warning right under the metrics block when the error
|
|
10
|
+
# rate crosses the threshold so the silent failure mode becomes loud.
|
|
11
|
+
class Evilution::Reporter::CLI::LineFormatters::ErrorRateWarning
|
|
12
|
+
DEFAULT_THRESHOLD = 0.25
|
|
13
|
+
|
|
14
|
+
def initialize(threshold: DEFAULT_THRESHOLD)
|
|
15
|
+
@threshold = threshold
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def format(summary)
|
|
19
|
+
return nil if summary.total.zero?
|
|
20
|
+
return nil if summary.errors.zero?
|
|
21
|
+
|
|
22
|
+
rate = summary.errors.to_f / summary.total
|
|
23
|
+
return nil if rate <= @threshold
|
|
24
|
+
|
|
25
|
+
pct = (rate * 100).round(1)
|
|
26
|
+
"! High error rate: #{summary.errors}/#{summary.total} (#{pct}%) mutations errored — " \
|
|
27
|
+
"score may be unreliable. See the \"Errored mutations:\" section for the underlying cause."
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -3,14 +3,23 @@
|
|
|
3
3
|
require_relative "../line_formatters"
|
|
4
4
|
|
|
5
5
|
class Evilution::Reporter::CLI::LineFormatters::Mutations
|
|
6
|
+
OPTIONAL_FIELDS = %i[neutral equivalent unresolved unparseable skipped].freeze
|
|
7
|
+
|
|
6
8
|
def format(summary)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
base_line(summary) + optional_sections(summary)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def base_line(summary)
|
|
15
|
+
"Mutations: #{summary.total} total, #{summary.killed} killed, " \
|
|
16
|
+
"#{summary.survived} survived, #{summary.timed_out} timed out"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def optional_sections(summary)
|
|
20
|
+
OPTIONAL_FIELDS.filter_map do |field|
|
|
21
|
+
count = summary.public_send(field)
|
|
22
|
+
", #{count} #{field}" if count.positive?
|
|
23
|
+
end.join
|
|
15
24
|
end
|
|
16
25
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../cli"
|
|
4
4
|
require_relative "line_formatters/mutations"
|
|
5
5
|
require_relative "line_formatters/score"
|
|
6
|
+
require_relative "line_formatters/error_rate_warning"
|
|
6
7
|
require_relative "line_formatters/duration"
|
|
7
8
|
require_relative "line_formatters/efficiency"
|
|
8
9
|
require_relative "line_formatters/peak_memory"
|
|
@@ -11,6 +12,7 @@ class Evilution::Reporter::CLI::MetricsBlock
|
|
|
11
12
|
DEFAULT_LINES = [
|
|
12
13
|
Evilution::Reporter::CLI::LineFormatters::Mutations.new,
|
|
13
14
|
Evilution::Reporter::CLI::LineFormatters::Score.new,
|
|
15
|
+
Evilution::Reporter::CLI::LineFormatters::ErrorRateWarning.new,
|
|
14
16
|
Evilution::Reporter::CLI::LineFormatters::Duration.new,
|
|
15
17
|
Evilution::Reporter::CLI::LineFormatters::Efficiency.new,
|
|
16
18
|
Evilution::Reporter::CLI::LineFormatters::PeakMemory.new
|
|
@@ -5,6 +5,7 @@ require "time"
|
|
|
5
5
|
require_relative "suggestion"
|
|
6
6
|
|
|
7
7
|
require_relative "../reporter"
|
|
8
|
+
require_relative "../session/schema"
|
|
8
9
|
|
|
9
10
|
class Evilution::Reporter::JSON
|
|
10
11
|
def initialize(suggest_tests: false, integration: :rspec)
|
|
@@ -19,21 +20,37 @@ class Evilution::Reporter::JSON
|
|
|
19
20
|
|
|
20
21
|
def build_report(summary)
|
|
21
22
|
report = {
|
|
23
|
+
schema_version: Evilution::Session::Schema::CURRENT_VERSION,
|
|
22
24
|
version: Evilution::VERSION,
|
|
23
25
|
timestamp: Time.now.iso8601,
|
|
24
26
|
summary: build_summary(summary),
|
|
25
|
-
survived: map_details(summary.survived_results),
|
|
26
27
|
coverage_gaps: build_coverage_gaps(summary),
|
|
28
|
+
**result_categories(summary)
|
|
29
|
+
}
|
|
30
|
+
append_disabled_to_report(report, summary)
|
|
31
|
+
report
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def result_categories(summary)
|
|
35
|
+
direct_categories(summary).merge(derived_categories(summary))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def direct_categories(summary)
|
|
39
|
+
{
|
|
40
|
+
survived: map_details(summary.survived_results),
|
|
27
41
|
killed: map_details(summary.killed_results),
|
|
28
42
|
neutral: map_details(summary.neutral_results),
|
|
29
|
-
timed_out: map_details(summary.results.select(&:timeout?)),
|
|
30
|
-
errors: map_details(summary.results.select(&:error?)),
|
|
31
43
|
equivalent: map_details(summary.equivalent_results),
|
|
32
44
|
unresolved: map_details(summary.unresolved_results),
|
|
33
45
|
unparseable: map_details(summary.unparseable_results)
|
|
34
46
|
}
|
|
35
|
-
|
|
36
|
-
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def derived_categories(summary)
|
|
50
|
+
{
|
|
51
|
+
timed_out: map_details(summary.results.select(&:timeout?)),
|
|
52
|
+
errors: map_details(summary.results.select(&:error?))
|
|
53
|
+
}
|
|
37
54
|
end
|
|
38
55
|
|
|
39
56
|
def map_details(results)
|
|
@@ -47,7 +64,13 @@ class Evilution::Reporter::JSON
|
|
|
47
64
|
end
|
|
48
65
|
|
|
49
66
|
def build_summary(summary)
|
|
50
|
-
data =
|
|
67
|
+
data = build_core_summary(summary).merge(build_metrics_summary(summary))
|
|
68
|
+
append_optional_summary_fields(data, summary)
|
|
69
|
+
data
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def build_core_summary(summary)
|
|
73
|
+
{
|
|
51
74
|
total: summary.total,
|
|
52
75
|
killed: summary.killed,
|
|
53
76
|
survived: summary.survived,
|
|
@@ -56,23 +79,39 @@ class Evilution::Reporter::JSON
|
|
|
56
79
|
neutral: summary.neutral,
|
|
57
80
|
equivalent: summary.equivalent,
|
|
58
81
|
unresolved: summary.unresolved,
|
|
59
|
-
unparseable: summary.unparseable
|
|
82
|
+
unparseable: summary.unparseable
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def build_metrics_summary(summary)
|
|
87
|
+
{
|
|
60
88
|
score: summary.score.round(4),
|
|
61
89
|
duration: summary.duration.round(4),
|
|
62
90
|
killtime: summary.killtime.round(4),
|
|
63
91
|
efficiency: summary.efficiency.round(4),
|
|
64
92
|
mutations_per_second: summary.mutations_per_second.round(2)
|
|
65
93
|
}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def append_optional_summary_fields(data, summary)
|
|
66
97
|
data[:truncated] = true if summary.truncated?
|
|
67
98
|
data[:skipped] = summary.skipped if summary.skipped.positive?
|
|
68
99
|
peak = summary.peak_memory_mb
|
|
69
100
|
data[:peak_memory_mb] = peak.round(1) if peak
|
|
70
|
-
data
|
|
71
101
|
end
|
|
72
102
|
|
|
73
103
|
def build_mutation_detail(result)
|
|
74
104
|
mutation = result.mutation
|
|
75
|
-
detail =
|
|
105
|
+
detail = base_mutation_fields(mutation, result)
|
|
106
|
+
append_survived_fields(detail, mutation) if result.status == :survived
|
|
107
|
+
detail[:test_command] = result.test_command if result.test_command
|
|
108
|
+
append_memory_fields(detail, result)
|
|
109
|
+
append_error_fields(detail, result)
|
|
110
|
+
detail
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def base_mutation_fields(mutation, result)
|
|
114
|
+
{
|
|
76
115
|
operator: mutation.operator_name,
|
|
77
116
|
file: mutation.file_path,
|
|
78
117
|
line: mutation.line,
|
|
@@ -80,15 +119,12 @@ class Evilution::Reporter::JSON
|
|
|
80
119
|
duration: result.duration.round(4),
|
|
81
120
|
diff: mutation.diff
|
|
82
121
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
detail[:
|
|
89
|
-
append_memory_fields(detail, result)
|
|
90
|
-
append_error_fields(detail, result)
|
|
91
|
-
detail
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def append_survived_fields(detail, mutation)
|
|
125
|
+
detail[:suggestion] = @suggestion.suggestion_for(mutation)
|
|
126
|
+
unified = mutation.unified_diff
|
|
127
|
+
detail[:unified_diff] = unified if unified
|
|
92
128
|
end
|
|
93
129
|
|
|
94
130
|
def append_memory_fields(detail, result)
|
|
@@ -12,17 +12,4 @@ module Evilution::Reporter::Suggestion::DiffHelpers
|
|
|
12
12
|
def sanitize_method_name(name)
|
|
13
13
|
name.gsub(/[^a-zA-Z0-9_]/, "_").gsub(/_+/, "_").gsub(/\A_|_\z/, "")
|
|
14
14
|
end
|
|
15
|
-
|
|
16
|
-
def extract_diff_lines(diff)
|
|
17
|
-
lines = diff.split("\n")
|
|
18
|
-
original = lines.find { |l| l.start_with?("- ") }
|
|
19
|
-
mutated = lines.find { |l| l.start_with?("+ ") }
|
|
20
|
-
[clean_diff_line(original, "- "), clean_diff_line(mutated, "+ ")]
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def clean_diff_line(line, prefix)
|
|
24
|
-
return nil if line.nil?
|
|
25
|
-
|
|
26
|
-
line.sub(/^#{Regexp.escape(prefix)}/, "").strip
|
|
27
|
-
end
|
|
28
15
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../suggestion"
|
|
4
|
+
|
|
5
|
+
class Evilution::Reporter::Suggestion::DiffLines
|
|
6
|
+
def self.from_diff(raw_diff)
|
|
7
|
+
lines = raw_diff.split("\n")
|
|
8
|
+
new(
|
|
9
|
+
original: clean(lines.find { |l| l.start_with?("- ") }, "- "),
|
|
10
|
+
mutated: clean(lines.find { |l| l.start_with?("+ ") }, "+ ")
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.clean(line, prefix)
|
|
15
|
+
return nil if line.nil?
|
|
16
|
+
|
|
17
|
+
line.sub(/^#{Regexp.escape(prefix)}/, "").strip
|
|
18
|
+
end
|
|
19
|
+
private_class_method :clean
|
|
20
|
+
|
|
21
|
+
attr_reader :original, :mutated
|
|
22
|
+
|
|
23
|
+
def initialize(original:, mutated:)
|
|
24
|
+
@original = original
|
|
25
|
+
@mutated = mutated
|
|
26
|
+
freeze
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../templates"
|
|
4
4
|
require_relative "../diff_helpers"
|
|
5
|
+
require_relative "../diff_lines"
|
|
5
6
|
|
|
6
7
|
module Evilution::Reporter::Suggestion::Templates::Minitest
|
|
7
8
|
H = Evilution::Reporter::Suggestion::DiffHelpers
|
|
@@ -17,21 +18,26 @@ module Evilution::Reporter::Suggestion::Templates::Minitest
|
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def self.build(test_name:, action: :changed, &body_block)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
safe_name = H.sanitize_method_name(method_name)
|
|
23
|
-
original_line, mutated_line = H.extract_diff_lines(mutation.diff)
|
|
24
|
-
body = body_block.call(method_name)
|
|
25
|
-
indented = body.lines.map { |l| " #{l}" }.join.chomp
|
|
21
|
+
->(mutation) { render(test_name, action, body_block, mutation) }
|
|
22
|
+
end
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
def self.render(test_name, action, body_block, mutation)
|
|
25
|
+
method_name = H.parse_method_name(mutation.subject.name)
|
|
26
|
+
safe_name = H.sanitize_method_name(method_name)
|
|
27
|
+
diff_lines = Evilution::Reporter::Suggestion::DiffLines.from_diff(mutation.diff)
|
|
28
|
+
indented = indent_body(body_block.call(method_name))
|
|
29
|
+
|
|
30
|
+
<<~MINITEST.strip
|
|
31
|
+
# Mutation: #{format_header(action, diff_lines.original, diff_lines.mutated, mutation.subject.name)}
|
|
32
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
33
|
+
def test_#{test_name}_#{safe_name}
|
|
34
|
+
#{indented}
|
|
35
|
+
end
|
|
36
|
+
MINITEST
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.indent_body(body)
|
|
40
|
+
body.lines.map { |l| " #{l}" }.join.chomp
|
|
35
41
|
end
|
|
36
42
|
|
|
37
43
|
MINITEST_ENTRIES = {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../templates"
|
|
4
4
|
require_relative "../diff_helpers"
|
|
5
|
+
require_relative "../diff_lines"
|
|
5
6
|
|
|
6
7
|
module Evilution::Reporter::Suggestion::Templates::Rspec
|
|
7
8
|
H = Evilution::Reporter::Suggestion::DiffHelpers
|
|
@@ -17,20 +18,25 @@ module Evilution::Reporter::Suggestion::Templates::Rspec
|
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def self.build(it_desc:, action: :changed, &body_block)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
original_line, mutated_line = H.extract_diff_lines(mutation.diff)
|
|
23
|
-
body = body_block.call(method_name)
|
|
24
|
-
indented = body.lines.map { |l| " #{l}" }.join.chomp
|
|
21
|
+
->(mutation) { render(it_desc, action, body_block, mutation) }
|
|
22
|
+
end
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
def self.render(it_desc, action, body_block, mutation)
|
|
25
|
+
method_name = H.parse_method_name(mutation.subject.name)
|
|
26
|
+
diff_lines = Evilution::Reporter::Suggestion::DiffLines.from_diff(mutation.diff)
|
|
27
|
+
indented = indent_body(body_block.call(method_name))
|
|
28
|
+
|
|
29
|
+
<<~RSPEC.strip
|
|
30
|
+
# Mutation: #{format_header(action, diff_lines.original, diff_lines.mutated, mutation.subject.name)}
|
|
31
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
32
|
+
it '#{it_desc} ##{method_name}' do
|
|
33
|
+
#{indented}
|
|
34
|
+
end
|
|
35
|
+
RSPEC
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.indent_body(body)
|
|
39
|
+
body.lines.map { |l| " #{l}" }.join.chomp
|
|
34
40
|
end
|
|
35
41
|
|
|
36
42
|
RSPEC_ENTRIES = {
|
|
@@ -23,28 +23,34 @@ class Evilution::Result::MutationResult
|
|
|
23
23
|
freeze
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# Positive type checks, not nil checks. EV-s5br / GH #1174: the
|
|
27
|
+
# nil_replacement mutator can swap a nil default into `false` (or some other
|
|
28
|
+
# non-typed value), and `nil?` then returns false, sending a missing method
|
|
29
|
+
# to the wrong receiver and crashing the parent worker. Asking the field
|
|
30
|
+
# explicitly whether it is the expected struct keeps the parent process
|
|
31
|
+
# alive and lets the mutation count as a measured (errored) result.
|
|
26
32
|
def child_rss_kb
|
|
27
|
-
@memory.
|
|
33
|
+
@memory.is_a?(Evilution::Result::MemoryStats) ? @memory.child_rss_kb : nil
|
|
28
34
|
end
|
|
29
35
|
|
|
30
36
|
def memory_delta_kb
|
|
31
|
-
@memory.
|
|
37
|
+
@memory.is_a?(Evilution::Result::MemoryStats) ? @memory.memory_delta_kb : nil
|
|
32
38
|
end
|
|
33
39
|
|
|
34
40
|
def parent_rss_kb
|
|
35
|
-
@memory.
|
|
41
|
+
@memory.is_a?(Evilution::Result::MemoryStats) ? @memory.parent_rss_kb : nil
|
|
36
42
|
end
|
|
37
43
|
|
|
38
44
|
def error_message
|
|
39
|
-
@error.
|
|
45
|
+
@error.is_a?(Evilution::Result::ErrorInfo) ? @error.message : nil
|
|
40
46
|
end
|
|
41
47
|
|
|
42
48
|
def error_class
|
|
43
|
-
@error.
|
|
49
|
+
@error.is_a?(Evilution::Result::ErrorInfo) ? @error.klass : nil
|
|
44
50
|
end
|
|
45
51
|
|
|
46
52
|
def error_backtrace
|
|
47
|
-
@error.
|
|
53
|
+
@error.is_a?(Evilution::Result::ErrorInfo) ? @error.backtrace : nil
|
|
48
54
|
end
|
|
49
55
|
|
|
50
56
|
def killed?
|
|
@@ -30,25 +30,36 @@ class Evilution::Runner::BaselineRunner
|
|
|
30
30
|
|
|
31
31
|
def build_integration
|
|
32
32
|
klass = integration_class
|
|
33
|
-
|
|
34
|
-
kwargs
|
|
35
|
-
|
|
33
|
+
kwargs = base_integration_kwargs
|
|
34
|
+
kwargs.merge!(rspec_integration_kwargs) if klass == Evilution::Integration::RSpec
|
|
35
|
+
klass.new(**kwargs)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def base_integration_kwargs
|
|
39
|
+
{
|
|
40
|
+
test_files: config.spec_files.empty? ? nil : config.spec_files,
|
|
36
41
|
hooks: hooks,
|
|
37
42
|
fallback_to_full_suite: config.fallback_to_full_suite?,
|
|
38
43
|
spec_selector: config.spec_selector
|
|
39
44
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def rspec_integration_kwargs
|
|
48
|
+
{
|
|
49
|
+
related_specs_heuristic: config.related_specs_heuristic?,
|
|
50
|
+
example_filter: build_example_filter
|
|
51
|
+
}
|
|
45
52
|
end
|
|
46
53
|
|
|
47
54
|
def call(subjects)
|
|
48
55
|
return nil unless config.baseline? && subjects.any?
|
|
49
56
|
|
|
50
57
|
log_start
|
|
51
|
-
baseline = Evilution::Baseline.new(
|
|
58
|
+
baseline = Evilution::Baseline.new(
|
|
59
|
+
timeout: config.timeout,
|
|
60
|
+
test_files: config.spec_files.empty? ? nil : config.spec_files,
|
|
61
|
+
**integration_class.baseline_options
|
|
62
|
+
)
|
|
52
63
|
result = baseline.call(subjects)
|
|
53
64
|
log_complete(result)
|
|
54
65
|
result
|
|
@@ -32,19 +32,23 @@ class Evilution::Runner::Diagnostics
|
|
|
32
32
|
def log_mutation_diagnostics(result)
|
|
33
33
|
return unless verbose?
|
|
34
34
|
|
|
35
|
-
parts =
|
|
36
|
-
|
|
35
|
+
parts = mutation_metric_parts(result)
|
|
36
|
+
stderr.write("[verbose] #{result.mutation}: #{parts.join(", ")}\n") unless parts.empty?
|
|
37
37
|
|
|
38
|
-
if result.
|
|
39
|
-
|
|
40
|
-
parts << format("delta: %<sign>s%<mb>.1f MB", sign: sign, mb: result.memory_delta_kb / 1024.0)
|
|
41
|
-
end
|
|
38
|
+
log_mutation_error(result) if result.error?
|
|
39
|
+
end
|
|
42
40
|
|
|
41
|
+
def mutation_metric_parts(result)
|
|
42
|
+
parts = []
|
|
43
|
+
parts << format("child_rss: %<mb>.1f MB", mb: result.child_rss_kb / 1024.0) if result.child_rss_kb
|
|
44
|
+
parts << format_memory_delta(result.memory_delta_kb) if result.memory_delta_kb
|
|
43
45
|
parts << gc_stats_string
|
|
46
|
+
parts
|
|
47
|
+
end
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
def format_memory_delta(delta_kb)
|
|
50
|
+
sign = delta_kb.negative? ? "" : "+"
|
|
51
|
+
format("delta: %<sign>s%<mb>.1f MB", sign: sign, mb: delta_kb / 1024.0)
|
|
48
52
|
end
|
|
49
53
|
|
|
50
54
|
def log_worker_stats(stats)
|
|
@@ -4,6 +4,7 @@ require_relative "../runner"
|
|
|
4
4
|
require_relative "../isolation/fork"
|
|
5
5
|
require_relative "../isolation/in_process"
|
|
6
6
|
require_relative "../rails_detector"
|
|
7
|
+
require_relative "../gem_detector"
|
|
7
8
|
|
|
8
9
|
class Evilution::Runner::IsolationResolver
|
|
9
10
|
PRELOAD_CANDIDATES = [
|
|
@@ -108,16 +109,52 @@ class Evilution::Runner::IsolationResolver
|
|
|
108
109
|
end
|
|
109
110
|
|
|
110
111
|
def resolve_preload_path
|
|
111
|
-
if config.preload.is_a?(String)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
112
|
+
return resolve_explicit_with_fallback(config.preload) if config.preload.is_a?(String)
|
|
113
|
+
|
|
114
|
+
resolve_autodetected_preload
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Explicit preload path resolution with auto-detect fallthrough under :fork.
|
|
118
|
+
# When the user-configured path is missing, surface a stderr warning naming
|
|
119
|
+
# the missing path and try the auto-detect chain so a stale .evilution.yml
|
|
120
|
+
# entry doesn't silently disable preloading. Fallthrough requires both
|
|
121
|
+
# :fork isolation AND a detected Rails root (otherwise the chain has nowhere
|
|
122
|
+
# to look and the explicit-missing error is raised directly).
|
|
123
|
+
def resolve_explicit_with_fallback(explicit)
|
|
124
|
+
return explicit if File.file?(explicit)
|
|
125
|
+
|
|
126
|
+
raise_explicit_preload_missing(explicit) unless can_fallthrough_to_autodetect?
|
|
127
|
+
|
|
128
|
+
warn_missing_explicit_preload(explicit)
|
|
129
|
+
fallback = find_first_existing_candidate
|
|
130
|
+
return fallback if fallback
|
|
131
|
+
|
|
132
|
+
raise build_combined_missing_error(explicit)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def can_fallthrough_to_autodetect?
|
|
136
|
+
resolve_isolation == :fork && !detected_rails_root.nil?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def resolve_autodetected_preload
|
|
140
|
+
if detected_rails_root
|
|
141
|
+
fallback = find_first_existing_candidate
|
|
142
|
+
return fallback if fallback
|
|
143
|
+
|
|
144
|
+
raise Evilution::ConfigError, autodetect_missing_message
|
|
119
145
|
end
|
|
120
146
|
|
|
147
|
+
detected_gem_entry
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def detected_gem_entry
|
|
151
|
+
return @detected_gem_entry if defined?(@detected_gem_entry)
|
|
152
|
+
|
|
153
|
+
root = Evilution::GemDetector.gem_root_for_any(target_files)
|
|
154
|
+
@detected_gem_entry = root && Evilution::GemDetector.gem_entry_for(root, target_paths: target_files)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def find_first_existing_candidate
|
|
121
158
|
root = detected_rails_root
|
|
122
159
|
return nil unless root
|
|
123
160
|
|
|
@@ -125,11 +162,37 @@ class Evilution::Runner::IsolationResolver
|
|
|
125
162
|
abs = File.join(root, rel)
|
|
126
163
|
return abs if File.file?(abs)
|
|
127
164
|
end
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def autodetect_missing_message
|
|
169
|
+
"Preload file not found. Tried: [#{PRELOAD_CANDIDATES.join(", ")}]. " \
|
|
170
|
+
"Pass --preload <file> or set preload: in .evilution.yml. " \
|
|
171
|
+
"Use --no-preload (or preload: false) to disable preloading entirely."
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def build_combined_missing_error(explicit)
|
|
175
|
+
Evilution::ConfigError.new(
|
|
176
|
+
"Preload file not found. Configured preload #{explicit.inspect} does not exist, " \
|
|
177
|
+
"and none of the auto-detect candidates exist either. " \
|
|
178
|
+
"Tried: [#{PRELOAD_CANDIDATES.join(", ")}]. " \
|
|
179
|
+
"Pass --preload <file> or set preload: in .evilution.yml. " \
|
|
180
|
+
"Use --no-preload (or preload: false) to disable preloading entirely.",
|
|
181
|
+
file: explicit
|
|
182
|
+
)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def raise_explicit_preload_missing(path)
|
|
186
|
+
raise Evilution::ConfigError.new("preload file not found: #{path.inspect}", file: path)
|
|
187
|
+
end
|
|
128
188
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
189
|
+
def warn_missing_explicit_preload(path)
|
|
190
|
+
return if config.quiet
|
|
191
|
+
|
|
192
|
+
$stderr.write(
|
|
193
|
+
"[evilution] warning: configured preload #{path.inspect} not found; " \
|
|
194
|
+
"falling through to auto-detect chain.\n"
|
|
195
|
+
)
|
|
133
196
|
end
|
|
134
197
|
|
|
135
198
|
# When the user explicitly requests InProcess on a Rails project, warn once
|
|
@@ -7,6 +7,8 @@ class Evilution::Runner::MutationExecutor::ResultCache
|
|
|
7
7
|
CACHEABLE_STATUSES = %i[killed timeout].freeze
|
|
8
8
|
private_constant :CACHEABLE_STATUSES
|
|
9
9
|
|
|
10
|
+
Partition = Data.define(:uncached_indices, :cached_results)
|
|
11
|
+
|
|
10
12
|
def initialize(backend)
|
|
11
13
|
@backend = backend
|
|
12
14
|
end
|
|
@@ -56,7 +58,7 @@ class Evilution::Runner::MutationExecutor::ResultCache
|
|
|
56
58
|
end
|
|
57
59
|
end
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
Partition.new(uncached_indices: uncached_indices, cached_results: cached_results)
|
|
60
62
|
end
|
|
61
63
|
|
|
62
64
|
private
|