evilution 0.19.0 → 0.21.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/.migration-hint-ts +1 -1
- data/.beads/issues.jsonl +35 -35
- data/CHANGELOG.md +36 -0
- data/README.md +25 -4
- data/lib/evilution/cli.rb +11 -2
- data/lib/evilution/config.rb +12 -2
- data/lib/evilution/equivalent/detector.rb +3 -1
- data/lib/evilution/equivalent/heuristic/alias_swap.rb +2 -1
- data/lib/evilution/equivalent/heuristic/void_context.rb +77 -0
- data/lib/evilution/integration/crash_detector.rb +55 -0
- data/lib/evilution/integration/rspec.rb +64 -29
- data/lib/evilution/isolation/fork.rb +3 -6
- data/lib/evilution/mutator/base.rb +1 -1
- data/lib/evilution/mutator/operator/begin_unwrap.rb +21 -0
- data/lib/evilution/mutator/operator/block_param_removal.rb +57 -0
- data/lib/evilution/mutator/operator/case_when.rb +55 -0
- data/lib/evilution/mutator/operator/equality_to_identity.rb +22 -0
- data/lib/evilution/mutator/operator/index_to_dig.rb +1 -1
- data/lib/evilution/mutator/operator/lambda_body.rb +18 -0
- data/lib/evilution/mutator/operator/loop_flip.rb +27 -0
- data/lib/evilution/mutator/operator/method_body_replacement.rb +10 -6
- data/lib/evilution/mutator/operator/predicate_replacement.rb +27 -0
- data/lib/evilution/mutator/operator/retry_removal.rb +16 -0
- data/lib/evilution/mutator/operator/send_mutation.rb +8 -1
- data/lib/evilution/mutator/operator/string_interpolation.rb +32 -0
- data/lib/evilution/mutator/operator/string_literal.rb +18 -0
- data/lib/evilution/mutator/registry.rb +19 -3
- data/lib/evilution/related_spec_heuristic.rb +63 -0
- data/lib/evilution/reporter/cli.rb +14 -8
- data/lib/evilution/reporter/html.rb +32 -2
- data/lib/evilution/reporter/json.rb +14 -0
- data/lib/evilution/result/coverage_gap.rb +35 -0
- data/lib/evilution/result/coverage_gap_grouper.rb +22 -0
- data/lib/evilution/result/summary.rb +5 -0
- data/lib/evilution/runner.rb +28 -1
- data/lib/evilution/session/store.rb +13 -0
- data/lib/evilution/temp_dir_tracker.rb +39 -0
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +9 -0
- metadata +17 -2
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::RetryRemoval < Evilution::Mutator::Base
|
|
6
|
+
def visit_retry_node(node)
|
|
7
|
+
add_mutation(
|
|
8
|
+
offset: node.location.start_offset,
|
|
9
|
+
length: node.location.length,
|
|
10
|
+
replacement: "nil",
|
|
11
|
+
node: node
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -29,7 +29,14 @@ class Evilution::Mutator::Operator::SendMutation < Evilution::Mutator::Base
|
|
|
29
29
|
to_i: [:to_s],
|
|
30
30
|
to_f: [:to_i],
|
|
31
31
|
to_a: [:to_h],
|
|
32
|
-
to_h: [:to_a]
|
|
32
|
+
to_h: [:to_a],
|
|
33
|
+
downcase: [:upcase],
|
|
34
|
+
upcase: [:downcase],
|
|
35
|
+
strip: %i[lstrip rstrip],
|
|
36
|
+
lstrip: [:strip],
|
|
37
|
+
rstrip: [:strip],
|
|
38
|
+
chomp: [:chop],
|
|
39
|
+
chop: [:chomp]
|
|
33
40
|
}.freeze
|
|
34
41
|
|
|
35
42
|
def visit_call_node(node)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::StringInterpolation < Evilution::Mutator::Base
|
|
6
|
+
def visit_interpolated_string_node(node)
|
|
7
|
+
mutate_embedded_statements(node)
|
|
8
|
+
super
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def visit_interpolated_symbol_node(node)
|
|
12
|
+
mutate_embedded_statements(node)
|
|
13
|
+
super
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def mutate_embedded_statements(node)
|
|
19
|
+
node.parts.each do |part|
|
|
20
|
+
next unless part.is_a?(Prism::EmbeddedStatementsNode)
|
|
21
|
+
next if part.statements.nil? || part.statements.body.empty?
|
|
22
|
+
|
|
23
|
+
stmt = part.statements
|
|
24
|
+
add_mutation(
|
|
25
|
+
offset: stmt.location.start_offset,
|
|
26
|
+
length: stmt.location.length,
|
|
27
|
+
replacement: "nil",
|
|
28
|
+
node: part
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -3,7 +3,25 @@
|
|
|
3
3
|
require_relative "../operator"
|
|
4
4
|
|
|
5
5
|
class Evilution::Mutator::Operator::StringLiteral < Evilution::Mutator::Base
|
|
6
|
+
def initialize(skip_heredoc_literals: false, **rest)
|
|
7
|
+
super(**rest)
|
|
8
|
+
@skip_heredoc_literals = skip_heredoc_literals
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def visit_interpolated_string_node(node)
|
|
12
|
+
return super unless node.heredoc?
|
|
13
|
+
return if @skip_heredoc_literals
|
|
14
|
+
|
|
15
|
+
node.parts.each do |part|
|
|
16
|
+
next if part.is_a?(Prism::StringNode)
|
|
17
|
+
|
|
18
|
+
visit(part)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
6
22
|
def visit_string_node(node)
|
|
23
|
+
return super if node.heredoc?
|
|
24
|
+
|
|
7
25
|
replacement = node.content.empty? ? '"mutation"' : '""'
|
|
8
26
|
|
|
9
27
|
add_mutation(
|
|
@@ -65,7 +65,16 @@ class Evilution::Mutator::Registry
|
|
|
65
65
|
Evilution::Mutator::Operator::YieldStatement,
|
|
66
66
|
Evilution::Mutator::Operator::SplatOperator,
|
|
67
67
|
Evilution::Mutator::Operator::DefinedCheck,
|
|
68
|
-
Evilution::Mutator::Operator::RegexCapture
|
|
68
|
+
Evilution::Mutator::Operator::RegexCapture,
|
|
69
|
+
Evilution::Mutator::Operator::LoopFlip,
|
|
70
|
+
Evilution::Mutator::Operator::StringInterpolation,
|
|
71
|
+
Evilution::Mutator::Operator::RetryRemoval,
|
|
72
|
+
Evilution::Mutator::Operator::CaseWhen,
|
|
73
|
+
Evilution::Mutator::Operator::PredicateReplacement,
|
|
74
|
+
Evilution::Mutator::Operator::EqualityToIdentity,
|
|
75
|
+
Evilution::Mutator::Operator::LambdaBody,
|
|
76
|
+
Evilution::Mutator::Operator::BeginUnwrap,
|
|
77
|
+
Evilution::Mutator::Operator::BlockParamRemoval
|
|
69
78
|
].each { |op| registry.register(op) }
|
|
70
79
|
registry
|
|
71
80
|
end
|
|
@@ -79,9 +88,10 @@ class Evilution::Mutator::Registry
|
|
|
79
88
|
self
|
|
80
89
|
end
|
|
81
90
|
|
|
82
|
-
def mutations_for(subject, filter: nil)
|
|
91
|
+
def mutations_for(subject, filter: nil, operator_options: {})
|
|
83
92
|
@operators.flat_map do |operator_class|
|
|
84
|
-
operator_class
|
|
93
|
+
operator = build_operator(operator_class, operator_options)
|
|
94
|
+
operator.call(subject, filter: filter)
|
|
85
95
|
end
|
|
86
96
|
end
|
|
87
97
|
|
|
@@ -92,4 +102,10 @@ class Evilution::Mutator::Registry
|
|
|
92
102
|
def operators
|
|
93
103
|
@operators.dup
|
|
94
104
|
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def build_operator(operator_class, options)
|
|
109
|
+
operator_class.new(**options)
|
|
110
|
+
end
|
|
95
111
|
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Evilution::RelatedSpecHeuristic
|
|
4
|
+
RELATED_SPEC_DIRS = %w[
|
|
5
|
+
spec/requests
|
|
6
|
+
spec/integration
|
|
7
|
+
spec/features
|
|
8
|
+
spec/system
|
|
9
|
+
].freeze
|
|
10
|
+
|
|
11
|
+
INCLUDES_PATTERN = /\bincludes\(/
|
|
12
|
+
|
|
13
|
+
def call(mutation)
|
|
14
|
+
return [] unless includes_mutation?(mutation)
|
|
15
|
+
|
|
16
|
+
domain = extract_domain(mutation.file_path)
|
|
17
|
+
return [] unless domain
|
|
18
|
+
|
|
19
|
+
find_related_specs(domain)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def includes_mutation?(mutation)
|
|
25
|
+
diff = mutation.diff
|
|
26
|
+
return false unless diff
|
|
27
|
+
|
|
28
|
+
diff.split("\n").any? { |line| line.start_with?("- ") && line.match?(INCLUDES_PATTERN) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def extract_domain(file_path)
|
|
32
|
+
normalized = normalize_path(file_path)
|
|
33
|
+
|
|
34
|
+
# Strip common prefixes and get the relative path under app/ or lib/
|
|
35
|
+
relative = normalized
|
|
36
|
+
.delete_prefix("app/controllers/")
|
|
37
|
+
.delete_prefix("app/models/")
|
|
38
|
+
.delete_prefix("app/")
|
|
39
|
+
.delete_prefix("lib/")
|
|
40
|
+
|
|
41
|
+
# Remove .rb extension and _controller suffix
|
|
42
|
+
basename = relative.sub(/\.rb\z/, "")
|
|
43
|
+
basename = basename.sub(/_controller\z/, "")
|
|
44
|
+
|
|
45
|
+
basename.empty? ? nil : basename
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def normalize_path(path)
|
|
49
|
+
path = path.delete_prefix("./")
|
|
50
|
+
path = path.delete_prefix("#{Dir.pwd}/") if path.start_with?("/")
|
|
51
|
+
path
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def find_related_specs(domain)
|
|
55
|
+
RELATED_SPEC_DIRS.flat_map { |dir| find_specs_in_dir(dir, domain) }.sort
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def find_specs_in_dir(dir, domain)
|
|
59
|
+
return [] unless Dir.exist?(dir)
|
|
60
|
+
|
|
61
|
+
Dir.glob(File.join(dir, "**", "#{domain}_spec.rb"))
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -30,11 +30,12 @@ class Evilution::Reporter::CLI
|
|
|
30
30
|
private
|
|
31
31
|
|
|
32
32
|
def append_survived(lines, summary)
|
|
33
|
-
|
|
33
|
+
gaps = summary.coverage_gaps
|
|
34
|
+
return unless gaps.any?
|
|
34
35
|
|
|
35
36
|
lines << ""
|
|
36
|
-
lines << "Survived mutations:"
|
|
37
|
-
|
|
37
|
+
lines << "Survived mutations (#{gaps.length} coverage gap#{"s" unless gaps.length == 1}):"
|
|
38
|
+
gaps.each { |gap| lines << format_coverage_gap(gap) }
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
def append_neutral(lines, summary)
|
|
@@ -90,11 +91,16 @@ class Evilution::Reporter::CLI
|
|
|
90
91
|
"Efficiency: #{pct} killtime, #{rate} mutations/s"
|
|
91
92
|
end
|
|
92
93
|
|
|
93
|
-
def
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
def format_coverage_gap(gap)
|
|
95
|
+
location = "#{gap.file_path}:#{gap.line}"
|
|
96
|
+
header = if gap.single?
|
|
97
|
+
" #{gap.primary_operator}: #{location} (#{gap.subject_name})"
|
|
98
|
+
else
|
|
99
|
+
operators = gap.operator_names.join(", ")
|
|
100
|
+
" #{location} (#{gap.subject_name}) [#{gap.count} mutations: #{operators}]"
|
|
101
|
+
end
|
|
102
|
+
diff_lines = gap.primary_diff.split("\n").map { |l| " #{l}" }.join("\n")
|
|
103
|
+
"#{header}\n#{diff_lines}"
|
|
98
104
|
end
|
|
99
105
|
|
|
100
106
|
def format_neutral(result)
|
|
@@ -4,6 +4,7 @@ require "cgi"
|
|
|
4
4
|
require_relative "suggestion"
|
|
5
5
|
|
|
6
6
|
require_relative "../reporter"
|
|
7
|
+
require_relative "../result/coverage_gap_grouper"
|
|
7
8
|
|
|
8
9
|
class Evilution::Reporter::HTML
|
|
9
10
|
def initialize(baseline: nil)
|
|
@@ -156,15 +157,39 @@ class Evilution::Reporter::HTML
|
|
|
156
157
|
def build_survived_details(survived)
|
|
157
158
|
return "" if survived.empty?
|
|
158
159
|
|
|
159
|
-
|
|
160
|
+
gaps = Evilution::Result::CoverageGapGrouper.new.call(survived)
|
|
161
|
+
entries = gaps.map { |gap| build_gap_entry(gap) }.join("\n")
|
|
160
162
|
<<~HTML
|
|
161
163
|
<div class="survived-details">
|
|
162
|
-
<h3>
|
|
164
|
+
<h3>Coverage Gaps (#{gaps.length})</h3>
|
|
163
165
|
#{entries}
|
|
164
166
|
</div>
|
|
165
167
|
HTML
|
|
166
168
|
end
|
|
167
169
|
|
|
170
|
+
def build_gap_entry(gap)
|
|
171
|
+
if gap.single?
|
|
172
|
+
build_survived_entry(gap.mutation_results.first)
|
|
173
|
+
else
|
|
174
|
+
build_grouped_gap_entry(gap)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def build_grouped_gap_entry(gap)
|
|
179
|
+
operator_tags = gap.operator_names.map { |op| %(<span class="operator-tag">#{h(op)}</span>) }.join(" ")
|
|
180
|
+
entries_html = gap.mutation_results.map { |r| build_survived_entry(r) }.join("\n")
|
|
181
|
+
<<~HTML
|
|
182
|
+
<div class="coverage-gap">
|
|
183
|
+
<div class="gap-header">
|
|
184
|
+
<span class="location">#{h(gap.file_path)}:#{gap.line} (#{h(gap.subject_name)})</span>
|
|
185
|
+
<span class="gap-count">#{gap.count} mutations</span>
|
|
186
|
+
#{operator_tags}
|
|
187
|
+
</div>
|
|
188
|
+
#{entries_html}
|
|
189
|
+
</div>
|
|
190
|
+
HTML
|
|
191
|
+
end
|
|
192
|
+
|
|
168
193
|
def build_survived_entry(result)
|
|
169
194
|
mutation = result.mutation
|
|
170
195
|
suggestion_text = @suggestion.suggestion_for(mutation)
|
|
@@ -307,6 +332,11 @@ class Evilution::Reporter::HTML
|
|
|
307
332
|
.diff-removed { color: #f85149; display: block; }
|
|
308
333
|
.diff-added { color: #3fb950; display: block; }
|
|
309
334
|
.suggestion { color: #d29922; font-size: 0.8rem; margin-top: 0.5rem; font-style: italic; }
|
|
335
|
+
.coverage-gap { border: 1px solid #30363d; border-radius: 6px; padding: 0.75rem; margin-bottom: 0.75rem; background: #161b22; }
|
|
336
|
+
.gap-header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; font-size: 0.85rem; padding-bottom: 0.5rem; border-bottom: 1px solid #21262d; }
|
|
337
|
+
.gap-header .location { color: #58a6ff; font-family: monospace; }
|
|
338
|
+
.gap-count { background: #4a1a1a; color: #f85149; font-size: 0.7rem; padding: 0.1rem 0.5rem; border-radius: 10px; font-weight: bold; }
|
|
339
|
+
.operator-tag { background: #21262d; color: #8b949e; font-size: 0.7rem; padding: 0.1rem 0.4rem; border-radius: 4px; font-family: monospace; }
|
|
310
340
|
.empty { color: #8b949e; text-align: center; padding: 2rem; }
|
|
311
341
|
.baseline-comparison { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1rem; margin-bottom: 2rem; }
|
|
312
342
|
.baseline-comparison h2 { font-size: 1rem; color: #f0f6fc; margin-bottom: 0.75rem; }
|
|
@@ -24,6 +24,7 @@ class Evilution::Reporter::JSON
|
|
|
24
24
|
timestamp: Time.now.iso8601,
|
|
25
25
|
summary: build_summary(summary),
|
|
26
26
|
survived: summary.survived_results.map { |r| build_mutation_detail(r) },
|
|
27
|
+
coverage_gaps: build_coverage_gaps(summary),
|
|
27
28
|
killed: summary.killed_results.map { |r| build_mutation_detail(r) },
|
|
28
29
|
neutral: summary.neutral_results.map { |r| build_mutation_detail(r) },
|
|
29
30
|
timed_out: summary.results.select(&:timeout?).map { |r| build_mutation_detail(r) },
|
|
@@ -81,6 +82,19 @@ class Evilution::Reporter::JSON
|
|
|
81
82
|
detail
|
|
82
83
|
end
|
|
83
84
|
|
|
85
|
+
def build_coverage_gaps(summary)
|
|
86
|
+
summary.coverage_gaps.map do |gap|
|
|
87
|
+
{
|
|
88
|
+
file: gap.file_path,
|
|
89
|
+
subject: gap.subject_name,
|
|
90
|
+
line: gap.line,
|
|
91
|
+
operators: gap.operator_names,
|
|
92
|
+
count: gap.count,
|
|
93
|
+
mutations: gap.mutation_results.map { |r| build_mutation_detail(r) }
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
84
98
|
def build_disabled_detail(mutation)
|
|
85
99
|
{
|
|
86
100
|
operator: mutation.operator_name,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../result"
|
|
4
|
+
|
|
5
|
+
class Evilution::Result::CoverageGap
|
|
6
|
+
attr_reader :file_path, :subject_name, :line, :mutation_results
|
|
7
|
+
|
|
8
|
+
def initialize(file_path:, subject_name:, line:, mutation_results:)
|
|
9
|
+
@file_path = file_path
|
|
10
|
+
@subject_name = subject_name
|
|
11
|
+
@line = line
|
|
12
|
+
@mutation_results = mutation_results.dup.freeze
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def operator_names
|
|
17
|
+
mutation_results.map { |r| r.mutation.operator_name }.uniq
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def primary_operator
|
|
21
|
+
mutation_results.first.mutation.operator_name
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def primary_diff
|
|
25
|
+
mutation_results.first.mutation.diff
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def count
|
|
29
|
+
mutation_results.length
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def single?
|
|
33
|
+
count == 1
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "coverage_gap"
|
|
4
|
+
|
|
5
|
+
class Evilution::Result::CoverageGapGrouper
|
|
6
|
+
def call(survived_results)
|
|
7
|
+
grouped = survived_results.group_by do |r|
|
|
8
|
+
[r.mutation.file_path, r.mutation.subject.name, r.mutation.line]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
gaps = grouped.map do |(file_path, subject_name, line), results|
|
|
12
|
+
Evilution::Result::CoverageGap.new(
|
|
13
|
+
file_path: file_path,
|
|
14
|
+
subject_name: subject_name,
|
|
15
|
+
line: line,
|
|
16
|
+
mutation_results: results
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
gaps.sort_by { |gap| [gap.file_path, gap.line, gap.subject_name] }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../result"
|
|
4
|
+
require_relative "coverage_gap_grouper"
|
|
4
5
|
|
|
5
6
|
class Evilution::Result::Summary
|
|
6
7
|
attr_reader :results, :duration, :skipped, :disabled_mutations
|
|
@@ -73,6 +74,10 @@ class Evilution::Result::Summary
|
|
|
73
74
|
results.select(&:equivalent?)
|
|
74
75
|
end
|
|
75
76
|
|
|
77
|
+
def coverage_gaps
|
|
78
|
+
Evilution::Result::CoverageGapGrouper.new.call(survived_results)
|
|
79
|
+
end
|
|
80
|
+
|
|
76
81
|
def killtime
|
|
77
82
|
results.sum(0.0, &:duration)
|
|
78
83
|
end
|
data/lib/evilution/runner.rb
CHANGED
|
@@ -21,6 +21,7 @@ require_relative "cache"
|
|
|
21
21
|
require_relative "parallel/pool"
|
|
22
22
|
require_relative "session/store"
|
|
23
23
|
require_relative "ast/pattern/filter"
|
|
24
|
+
require_relative "temp_dir_tracker"
|
|
24
25
|
require_relative "disable_comment"
|
|
25
26
|
require_relative "ast/sorbet_sig_detector"
|
|
26
27
|
|
|
@@ -42,6 +43,7 @@ class Evilution::Runner
|
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
def call
|
|
46
|
+
install_signal_handlers
|
|
45
47
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
46
48
|
|
|
47
49
|
subjects = parse_and_filter_subjects
|
|
@@ -180,8 +182,9 @@ class Evilution::Runner
|
|
|
180
182
|
|
|
181
183
|
def generate_mutations(subjects)
|
|
182
184
|
filter = build_ignore_filter
|
|
185
|
+
operator_options = build_operator_options
|
|
183
186
|
mutations = subjects.flat_map do |subject|
|
|
184
|
-
registry.mutations_for(subject, filter: filter)
|
|
187
|
+
registry.mutations_for(subject, filter: filter, operator_options: operator_options)
|
|
185
188
|
end
|
|
186
189
|
skipped_count = filter ? filter.skipped_count : 0
|
|
187
190
|
|
|
@@ -252,6 +255,10 @@ class Evilution::Runner
|
|
|
252
255
|
end
|
|
253
256
|
end
|
|
254
257
|
|
|
258
|
+
def build_operator_options
|
|
259
|
+
{ skip_heredoc_literals: config.skip_heredoc_literals? }
|
|
260
|
+
end
|
|
261
|
+
|
|
255
262
|
def build_ignore_filter
|
|
256
263
|
patterns = config.ignore_patterns
|
|
257
264
|
return nil if patterns.nil? || patterns.empty?
|
|
@@ -432,6 +439,26 @@ class Evilution::Runner
|
|
|
432
439
|
config.fail_fast? && survived_count >= config.fail_fast
|
|
433
440
|
end
|
|
434
441
|
|
|
442
|
+
def install_signal_handlers
|
|
443
|
+
%w[INT TERM].each { |sig| install_signal_handler(sig) }
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def install_signal_handler(sig)
|
|
447
|
+
prev_handler = Signal.trap(sig) do
|
|
448
|
+
Evilution::TempDirTracker.cleanup_all
|
|
449
|
+
|
|
450
|
+
case prev_handler
|
|
451
|
+
when Proc, Method
|
|
452
|
+
prev_handler.call
|
|
453
|
+
when "IGNORE"
|
|
454
|
+
# Do nothing — signal is ignored
|
|
455
|
+
else
|
|
456
|
+
Signal.trap(sig, "DEFAULT")
|
|
457
|
+
Process.kill(sig, Process.pid)
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
435
462
|
def build_isolator
|
|
436
463
|
case resolve_isolation
|
|
437
464
|
when :fork then Evilution::Isolation::Fork.new(hooks: @hooks)
|
|
@@ -65,6 +65,7 @@ class Evilution::Session::Store
|
|
|
65
65
|
git: git_context,
|
|
66
66
|
summary: build_summary(summary),
|
|
67
67
|
survived: summary.survived_results.map { |r| build_mutation_detail(r) },
|
|
68
|
+
coverage_gaps: build_coverage_gaps(summary),
|
|
68
69
|
killed_count: summary.killed,
|
|
69
70
|
timed_out_count: summary.timed_out,
|
|
70
71
|
error_count: summary.errors,
|
|
@@ -101,6 +102,18 @@ class Evilution::Session::Store
|
|
|
101
102
|
}
|
|
102
103
|
end
|
|
103
104
|
|
|
105
|
+
def build_coverage_gaps(summary)
|
|
106
|
+
summary.coverage_gaps.map do |gap|
|
|
107
|
+
{
|
|
108
|
+
file: gap.file_path,
|
|
109
|
+
subject: gap.subject_name,
|
|
110
|
+
line: gap.line,
|
|
111
|
+
operators: gap.operator_names,
|
|
112
|
+
count: gap.count
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
104
117
|
def git_context
|
|
105
118
|
sha = `git rev-parse HEAD 2>/dev/null`.strip
|
|
106
119
|
branch = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "monitor"
|
|
5
|
+
require_relative "version"
|
|
6
|
+
|
|
7
|
+
module Evilution::TempDirTracker
|
|
8
|
+
@dirs = Set.new
|
|
9
|
+
@monitor = Monitor.new
|
|
10
|
+
@at_exit_registered = false
|
|
11
|
+
|
|
12
|
+
def self.register(dir)
|
|
13
|
+
@monitor.synchronize do
|
|
14
|
+
@dirs << dir
|
|
15
|
+
register_at_exit unless @at_exit_registered
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.unregister(dir)
|
|
20
|
+
@monitor.synchronize { @dirs.delete(dir) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.cleanup_all
|
|
24
|
+
@monitor.synchronize do
|
|
25
|
+
@dirs.each { |d| FileUtils.rm_rf(d) }
|
|
26
|
+
@dirs.clear
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.tracked_dirs
|
|
31
|
+
@monitor.synchronize { @dirs.dup }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.register_at_exit
|
|
35
|
+
at_exit { cleanup_all }
|
|
36
|
+
@at_exit_registered = true
|
|
37
|
+
end
|
|
38
|
+
private_class_method :register_at_exit
|
|
39
|
+
end
|
data/lib/evilution/version.rb
CHANGED
data/lib/evilution.rb
CHANGED
|
@@ -81,6 +81,15 @@ require_relative "evilution/mutator/operator/yield_statement"
|
|
|
81
81
|
require_relative "evilution/mutator/operator/splat_operator"
|
|
82
82
|
require_relative "evilution/mutator/operator/defined_check"
|
|
83
83
|
require_relative "evilution/mutator/operator/regex_capture"
|
|
84
|
+
require_relative "evilution/mutator/operator/loop_flip"
|
|
85
|
+
require_relative "evilution/mutator/operator/string_interpolation"
|
|
86
|
+
require_relative "evilution/mutator/operator/retry_removal"
|
|
87
|
+
require_relative "evilution/mutator/operator/case_when"
|
|
88
|
+
require_relative "evilution/mutator/operator/predicate_replacement"
|
|
89
|
+
require_relative "evilution/mutator/operator/equality_to_identity"
|
|
90
|
+
require_relative "evilution/mutator/operator/lambda_body"
|
|
91
|
+
require_relative "evilution/mutator/operator/begin_unwrap"
|
|
92
|
+
require_relative "evilution/mutator/operator/block_param_removal"
|
|
84
93
|
require_relative "evilution/mutator/registry"
|
|
85
94
|
require_relative "evilution/equivalent"
|
|
86
95
|
require_relative "evilution/equivalent/heuristic"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: evilution
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.21.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Denis Kiselev
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: diff-lcs
|
|
@@ -102,6 +102,7 @@ files:
|
|
|
102
102
|
- lib/evilution/equivalent/heuristic/dead_code.rb
|
|
103
103
|
- lib/evilution/equivalent/heuristic/method_body_nil.rb
|
|
104
104
|
- lib/evilution/equivalent/heuristic/noop_source.rb
|
|
105
|
+
- lib/evilution/equivalent/heuristic/void_context.rb
|
|
105
106
|
- lib/evilution/git.rb
|
|
106
107
|
- lib/evilution/git/changed_files.rb
|
|
107
108
|
- lib/evilution/hooks.rb
|
|
@@ -109,6 +110,7 @@ files:
|
|
|
109
110
|
- lib/evilution/hooks/registry.rb
|
|
110
111
|
- lib/evilution/integration.rb
|
|
111
112
|
- lib/evilution/integration/base.rb
|
|
113
|
+
- lib/evilution/integration/crash_detector.rb
|
|
112
114
|
- lib/evilution/integration/rspec.rb
|
|
113
115
|
- lib/evilution/isolation.rb
|
|
114
116
|
- lib/evilution/isolation/fork.rb
|
|
@@ -130,12 +132,15 @@ files:
|
|
|
130
132
|
- lib/evilution/mutator/operator/arithmetic_replacement.rb
|
|
131
133
|
- lib/evilution/mutator/operator/array_literal.rb
|
|
132
134
|
- lib/evilution/mutator/operator/bang_method.rb
|
|
135
|
+
- lib/evilution/mutator/operator/begin_unwrap.rb
|
|
133
136
|
- lib/evilution/mutator/operator/bitwise_complement.rb
|
|
134
137
|
- lib/evilution/mutator/operator/bitwise_replacement.rb
|
|
138
|
+
- lib/evilution/mutator/operator/block_param_removal.rb
|
|
135
139
|
- lib/evilution/mutator/operator/block_removal.rb
|
|
136
140
|
- lib/evilution/mutator/operator/boolean_literal_replacement.rb
|
|
137
141
|
- lib/evilution/mutator/operator/boolean_operator_replacement.rb
|
|
138
142
|
- lib/evilution/mutator/operator/break_statement.rb
|
|
143
|
+
- lib/evilution/mutator/operator/case_when.rb
|
|
139
144
|
- lib/evilution/mutator/operator/class_variable_write.rb
|
|
140
145
|
- lib/evilution/mutator/operator/collection_replacement.rb
|
|
141
146
|
- lib/evilution/mutator/operator/collection_return.rb
|
|
@@ -146,6 +151,7 @@ files:
|
|
|
146
151
|
- lib/evilution/mutator/operator/conditional_negation.rb
|
|
147
152
|
- lib/evilution/mutator/operator/defined_check.rb
|
|
148
153
|
- lib/evilution/mutator/operator/ensure_removal.rb
|
|
154
|
+
- lib/evilution/mutator/operator/equality_to_identity.rb
|
|
149
155
|
- lib/evilution/mutator/operator/explicit_super_mutation.rb
|
|
150
156
|
- lib/evilution/mutator/operator/float_literal.rb
|
|
151
157
|
- lib/evilution/mutator/operator/global_variable_write.rb
|
|
@@ -157,7 +163,9 @@ files:
|
|
|
157
163
|
- lib/evilution/mutator/operator/instance_variable_write.rb
|
|
158
164
|
- lib/evilution/mutator/operator/integer_literal.rb
|
|
159
165
|
- lib/evilution/mutator/operator/keyword_argument.rb
|
|
166
|
+
- lib/evilution/mutator/operator/lambda_body.rb
|
|
160
167
|
- lib/evilution/mutator/operator/local_variable_assignment.rb
|
|
168
|
+
- lib/evilution/mutator/operator/loop_flip.rb
|
|
161
169
|
- lib/evilution/mutator/operator/method_body_replacement.rb
|
|
162
170
|
- lib/evilution/mutator/operator/method_call_removal.rb
|
|
163
171
|
- lib/evilution/mutator/operator/mixin_removal.rb
|
|
@@ -168,6 +176,7 @@ files:
|
|
|
168
176
|
- lib/evilution/mutator/operator/pattern_matching_alternative.rb
|
|
169
177
|
- lib/evilution/mutator/operator/pattern_matching_array.rb
|
|
170
178
|
- lib/evilution/mutator/operator/pattern_matching_guard.rb
|
|
179
|
+
- lib/evilution/mutator/operator/predicate_replacement.rb
|
|
171
180
|
- lib/evilution/mutator/operator/range_replacement.rb
|
|
172
181
|
- lib/evilution/mutator/operator/receiver_replacement.rb
|
|
173
182
|
- lib/evilution/mutator/operator/redo_statement.rb
|
|
@@ -175,11 +184,13 @@ files:
|
|
|
175
184
|
- lib/evilution/mutator/operator/regexp_mutation.rb
|
|
176
185
|
- lib/evilution/mutator/operator/rescue_body_replacement.rb
|
|
177
186
|
- lib/evilution/mutator/operator/rescue_removal.rb
|
|
187
|
+
- lib/evilution/mutator/operator/retry_removal.rb
|
|
178
188
|
- lib/evilution/mutator/operator/return_value_removal.rb
|
|
179
189
|
- lib/evilution/mutator/operator/scalar_return.rb
|
|
180
190
|
- lib/evilution/mutator/operator/send_mutation.rb
|
|
181
191
|
- lib/evilution/mutator/operator/splat_operator.rb
|
|
182
192
|
- lib/evilution/mutator/operator/statement_deletion.rb
|
|
193
|
+
- lib/evilution/mutator/operator/string_interpolation.rb
|
|
183
194
|
- lib/evilution/mutator/operator/string_literal.rb
|
|
184
195
|
- lib/evilution/mutator/operator/superclass_removal.rb
|
|
185
196
|
- lib/evilution/mutator/operator/symbol_literal.rb
|
|
@@ -189,6 +200,7 @@ files:
|
|
|
189
200
|
- lib/evilution/parallel.rb
|
|
190
201
|
- lib/evilution/parallel/pool.rb
|
|
191
202
|
- lib/evilution/parallel/work_queue.rb
|
|
203
|
+
- lib/evilution/related_spec_heuristic.rb
|
|
192
204
|
- lib/evilution/reporter.rb
|
|
193
205
|
- lib/evilution/reporter/cli.rb
|
|
194
206
|
- lib/evilution/reporter/html.rb
|
|
@@ -196,6 +208,8 @@ files:
|
|
|
196
208
|
- lib/evilution/reporter/progress_bar.rb
|
|
197
209
|
- lib/evilution/reporter/suggestion.rb
|
|
198
210
|
- lib/evilution/result.rb
|
|
211
|
+
- lib/evilution/result/coverage_gap.rb
|
|
212
|
+
- lib/evilution/result/coverage_gap_grouper.rb
|
|
199
213
|
- lib/evilution/result/mutation_result.rb
|
|
200
214
|
- lib/evilution/result/summary.rb
|
|
201
215
|
- lib/evilution/runner.rb
|
|
@@ -204,6 +218,7 @@ files:
|
|
|
204
218
|
- lib/evilution/session/store.rb
|
|
205
219
|
- lib/evilution/spec_resolver.rb
|
|
206
220
|
- lib/evilution/subject.rb
|
|
221
|
+
- lib/evilution/temp_dir_tracker.rb
|
|
207
222
|
- lib/evilution/version.rb
|
|
208
223
|
- lib/tasks/memory_check.rake
|
|
209
224
|
- script/memory_check
|