evilution 0.16.0 → 0.17.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 +19 -18
- data/CHANGELOG.md +23 -0
- data/docs/ast_pattern_syntax.md +210 -0
- data/lib/evilution/ast/pattern/filter.rb +25 -0
- data/lib/evilution/ast/pattern/matcher.rb +107 -0
- data/lib/evilution/ast/pattern/parser.rb +185 -0
- data/lib/evilution/ast/pattern.rb +4 -0
- data/lib/evilution/ast/source_surgeon.rb +3 -3
- data/lib/evilution/cli.rb +13 -1
- data/lib/evilution/config.rb +35 -2
- data/lib/evilution/hooks/loader.rb +35 -0
- data/lib/evilution/hooks/registry.rb +60 -0
- data/lib/evilution/hooks.rb +58 -0
- data/lib/evilution/integration/base.rb +4 -0
- data/lib/evilution/integration/rspec.rb +6 -2
- data/lib/evilution/isolation/fork.rb +5 -0
- data/lib/evilution/mutator/base.rb +4 -1
- data/lib/evilution/mutator/operator/index_assignment_removal.rb +18 -0
- data/lib/evilution/mutator/operator/index_to_dig.rb +58 -0
- data/lib/evilution/mutator/operator/index_to_fetch.rb +30 -0
- data/lib/evilution/mutator/operator/mixin_removal.rb +2 -1
- data/lib/evilution/mutator/operator/pattern_matching_alternative.rb +46 -0
- data/lib/evilution/mutator/operator/pattern_matching_array.rb +97 -0
- data/lib/evilution/mutator/operator/pattern_matching_guard.rb +44 -0
- data/lib/evilution/mutator/operator/superclass_removal.rb +2 -1
- data/lib/evilution/mutator/registry.rb +9 -3
- data/lib/evilution/parallel/pool.rb +3 -1
- data/lib/evilution/reporter/cli.rb +1 -0
- data/lib/evilution/reporter/html.rb +7 -0
- data/lib/evilution/reporter/json.rb +1 -0
- data/lib/evilution/reporter/suggestion.rb +87 -1
- data/lib/evilution/result/summary.rb +3 -2
- data/lib/evilution/runner.rb +21 -9
- data/lib/evilution/session/store.rb +5 -2
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +12 -0
- metadata +16 -2
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::PatternMatchingAlternative < Evilution::Mutator::Base
|
|
6
|
+
def visit_alternation_pattern_node(node)
|
|
7
|
+
remove_left(node)
|
|
8
|
+
remove_right(node)
|
|
9
|
+
swap_order(node)
|
|
10
|
+
super
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def remove_left(node)
|
|
16
|
+
add_mutation(
|
|
17
|
+
offset: node.location.start_offset,
|
|
18
|
+
length: node.location.length,
|
|
19
|
+
replacement: source_for(node.right),
|
|
20
|
+
node: node
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def remove_right(node)
|
|
25
|
+
add_mutation(
|
|
26
|
+
offset: node.location.start_offset,
|
|
27
|
+
length: node.location.length,
|
|
28
|
+
replacement: source_for(node.left),
|
|
29
|
+
node: node
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def swap_order(node)
|
|
34
|
+
operator = @file_source.byteslice(node.operator_loc.start_offset, node.operator_loc.length)
|
|
35
|
+
add_mutation(
|
|
36
|
+
offset: node.location.start_offset,
|
|
37
|
+
length: node.location.length,
|
|
38
|
+
replacement: "#{source_for(node.right)} #{operator} #{source_for(node.left)}",
|
|
39
|
+
node: node
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def source_for(node)
|
|
44
|
+
@file_source.byteslice(node.location.start_offset, node.location.length)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::PatternMatchingArray < Evilution::Mutator::Base
|
|
6
|
+
def visit_array_pattern_node(node)
|
|
7
|
+
mutate_array_elements(node)
|
|
8
|
+
super
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def visit_find_pattern_node(node)
|
|
12
|
+
mutate_find_elements(node)
|
|
13
|
+
super
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def mutate_array_elements(node)
|
|
19
|
+
requireds = node.requireds
|
|
20
|
+
posts = node.posts
|
|
21
|
+
rest = node.rest
|
|
22
|
+
elements = requireds + posts
|
|
23
|
+
return if elements.empty?
|
|
24
|
+
|
|
25
|
+
elements.each_with_index do |_element, index|
|
|
26
|
+
remove_array_element(node, requireds, posts, rest, index) if elements.length > 1
|
|
27
|
+
wildcard_array_element(node, requireds, posts, rest, index)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def remove_array_element(node, requireds, posts, rest, skip_index)
|
|
32
|
+
parts = build_array_parts(requireds, posts, rest, skip_index: skip_index)
|
|
33
|
+
replace_pattern(node, parts)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def wildcard_array_element(node, requireds, posts, rest, wildcard_index)
|
|
37
|
+
parts = build_array_parts(requireds, posts, rest, wildcard_index: wildcard_index)
|
|
38
|
+
replace_pattern(node, parts)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def build_array_parts(requireds, posts, rest, skip_index: nil, wildcard_index: nil)
|
|
42
|
+
parts = []
|
|
43
|
+
requireds.each_with_index do |req, i|
|
|
44
|
+
next if i == skip_index
|
|
45
|
+
|
|
46
|
+
parts << (i == wildcard_index ? "_" : source_for(req))
|
|
47
|
+
end
|
|
48
|
+
parts << source_for(rest) if rest
|
|
49
|
+
posts.each_with_index do |post, i|
|
|
50
|
+
adjusted = requireds.length + i
|
|
51
|
+
next if adjusted == skip_index
|
|
52
|
+
|
|
53
|
+
parts << (adjusted == wildcard_index ? "_" : source_for(post))
|
|
54
|
+
end
|
|
55
|
+
parts
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def mutate_find_elements(node)
|
|
59
|
+
return if node.requireds.empty?
|
|
60
|
+
|
|
61
|
+
node.requireds.each_with_index do |_element, index|
|
|
62
|
+
remove_find_element(node, index) if node.requireds.length > 1
|
|
63
|
+
wildcard_find_element(node, index)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def remove_find_element(node, skip_index)
|
|
68
|
+
parts = [source_for(node.left)]
|
|
69
|
+
node.requireds.each_with_index do |req, i|
|
|
70
|
+
parts << source_for(req) unless i == skip_index
|
|
71
|
+
end
|
|
72
|
+
parts << source_for(node.right)
|
|
73
|
+
replace_pattern(node, parts)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def wildcard_find_element(node, wildcard_index)
|
|
77
|
+
parts = [source_for(node.left)]
|
|
78
|
+
node.requireds.each_with_index do |req, i|
|
|
79
|
+
parts << (i == wildcard_index ? "_" : source_for(req))
|
|
80
|
+
end
|
|
81
|
+
parts << source_for(node.right)
|
|
82
|
+
replace_pattern(node, parts)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def replace_pattern(node, parts)
|
|
86
|
+
add_mutation(
|
|
87
|
+
offset: node.location.start_offset,
|
|
88
|
+
length: node.location.length,
|
|
89
|
+
replacement: "[#{parts.join(", ")}]",
|
|
90
|
+
node: node
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def source_for(node)
|
|
95
|
+
@file_source.byteslice(node.location.start_offset, node.location.length)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::PatternMatchingGuard < Evilution::Mutator::Base
|
|
6
|
+
def visit_in_node(node)
|
|
7
|
+
pattern = node.pattern
|
|
8
|
+
mutate_guard(pattern, node) if guarded?(pattern)
|
|
9
|
+
super
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def guarded?(pattern)
|
|
15
|
+
pattern.is_a?(Prism::IfNode) || pattern.is_a?(Prism::UnlessNode)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def mutate_guard(pattern, in_node)
|
|
19
|
+
guard_start = pattern.statements.location.start_offset + pattern.statements.location.length
|
|
20
|
+
guard_end = pattern.predicate.location.start_offset + pattern.predicate.location.length
|
|
21
|
+
|
|
22
|
+
remove_guard(guard_start, guard_end, in_node)
|
|
23
|
+
negate_guard(pattern, in_node)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def remove_guard(guard_start, guard_end, in_node)
|
|
27
|
+
add_mutation(
|
|
28
|
+
offset: guard_start,
|
|
29
|
+
length: guard_end - guard_start,
|
|
30
|
+
replacement: "",
|
|
31
|
+
node: in_node
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def negate_guard(pattern, in_node)
|
|
36
|
+
pred_loc = pattern.predicate.location
|
|
37
|
+
add_mutation(
|
|
38
|
+
offset: pred_loc.start_offset,
|
|
39
|
+
length: pred_loc.length,
|
|
40
|
+
replacement: "!(#{@file_source.byteslice(pred_loc.start_offset, pred_loc.length)})",
|
|
41
|
+
node: in_node
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -5,10 +5,11 @@ require "prism"
|
|
|
5
5
|
require_relative "../operator"
|
|
6
6
|
|
|
7
7
|
class Evilution::Mutator::Operator::SuperclassRemoval < Evilution::Mutator::Base
|
|
8
|
-
def call(subject)
|
|
8
|
+
def call(subject, filter: nil)
|
|
9
9
|
@subject = subject
|
|
10
10
|
@file_source = File.read(subject.file_path)
|
|
11
11
|
@mutations = []
|
|
12
|
+
@filter = filter
|
|
12
13
|
|
|
13
14
|
tree = self.class.parsed_tree_for(subject.file_path, @file_source)
|
|
14
15
|
enclosing = find_enclosing_class(tree, subject.line_number)
|
|
@@ -51,7 +51,13 @@ class Evilution::Mutator::Registry
|
|
|
51
51
|
Evilution::Mutator::Operator::BitwiseReplacement,
|
|
52
52
|
Evilution::Mutator::Operator::BitwiseComplement,
|
|
53
53
|
Evilution::Mutator::Operator::ZsuperRemoval,
|
|
54
|
-
Evilution::Mutator::Operator::ExplicitSuperMutation
|
|
54
|
+
Evilution::Mutator::Operator::ExplicitSuperMutation,
|
|
55
|
+
Evilution::Mutator::Operator::IndexToFetch,
|
|
56
|
+
Evilution::Mutator::Operator::IndexToDig,
|
|
57
|
+
Evilution::Mutator::Operator::IndexAssignmentRemoval,
|
|
58
|
+
Evilution::Mutator::Operator::PatternMatchingGuard,
|
|
59
|
+
Evilution::Mutator::Operator::PatternMatchingAlternative,
|
|
60
|
+
Evilution::Mutator::Operator::PatternMatchingArray
|
|
55
61
|
].each { |op| registry.register(op) }
|
|
56
62
|
registry
|
|
57
63
|
end
|
|
@@ -65,9 +71,9 @@ class Evilution::Mutator::Registry
|
|
|
65
71
|
self
|
|
66
72
|
end
|
|
67
73
|
|
|
68
|
-
def mutations_for(subject)
|
|
74
|
+
def mutations_for(subject, filter: nil)
|
|
69
75
|
@operators.flat_map do |operator_class|
|
|
70
|
-
operator_class.new.call(subject)
|
|
76
|
+
operator_class.new.call(subject, filter: filter)
|
|
71
77
|
end
|
|
72
78
|
end
|
|
73
79
|
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
require_relative "../parallel"
|
|
4
4
|
|
|
5
5
|
class Evilution::Parallel::Pool
|
|
6
|
-
def initialize(size:)
|
|
6
|
+
def initialize(size:, hooks: nil)
|
|
7
7
|
raise ArgumentError, "pool size must be a positive integer, got #{size.inspect}" unless size.is_a?(Integer) && size >= 1
|
|
8
8
|
|
|
9
9
|
@size = size
|
|
10
|
+
@hooks = hooks
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
def map(items, &block)
|
|
@@ -35,6 +36,7 @@ class Evilution::Parallel::Pool
|
|
|
35
36
|
def fork_worker(item, read_io, write_io, &block)
|
|
36
37
|
Process.fork do
|
|
37
38
|
read_io.close
|
|
39
|
+
@hooks.fire(:worker_process_start) if @hooks
|
|
38
40
|
result = block.call(item)
|
|
39
41
|
Marshal.dump(result, write_io)
|
|
40
42
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
@@ -60,6 +60,7 @@ class Evilution::Reporter::CLI
|
|
|
60
60
|
"#{summary.survived} survived, #{summary.timed_out} timed out"
|
|
61
61
|
parts += ", #{summary.neutral} neutral" if summary.neutral.positive?
|
|
62
62
|
parts += ", #{summary.equivalent} equivalent" if summary.equivalent.positive?
|
|
63
|
+
parts += ", #{summary.skipped} skipped" if summary.skipped.positive?
|
|
63
64
|
parts
|
|
64
65
|
end
|
|
65
66
|
|
|
@@ -77,12 +77,19 @@ class Evilution::Reporter::HTML
|
|
|
77
77
|
<div class="card"><span class="card-value">#{summary.errors}</span><span class="card-label">Errors</span></div>
|
|
78
78
|
<div class="card"><span class="card-value">#{summary.neutral}</span><span class="card-label">Neutral</span></div>
|
|
79
79
|
<div class="card"><span class="card-value">#{summary.equivalent}</span><span class="card-label">Equivalent</span></div>
|
|
80
|
+
#{build_skipped_card(summary)}
|
|
80
81
|
<div class="card"><span class="card-value">#{format("%.2f", summary.duration)}s</span><span class="card-label">Duration</span></div>
|
|
81
82
|
#{peak_html}
|
|
82
83
|
</section>
|
|
83
84
|
HTML
|
|
84
85
|
end
|
|
85
86
|
|
|
87
|
+
def build_skipped_card(summary)
|
|
88
|
+
return "" unless summary.skipped.positive?
|
|
89
|
+
|
|
90
|
+
%(<div class="card"><span class="card-value">#{summary.skipped}</span><span class="card-label">Skipped</span></div>)
|
|
91
|
+
end
|
|
92
|
+
|
|
86
93
|
def build_truncation_notice(summary)
|
|
87
94
|
return "" unless summary.truncated?
|
|
88
95
|
|
|
@@ -46,6 +46,7 @@ class Evilution::Reporter::JSON
|
|
|
46
46
|
duration: summary.duration.round(4)
|
|
47
47
|
}
|
|
48
48
|
data[:truncated] = true if summary.truncated?
|
|
49
|
+
data[:skipped] = summary.skipped if summary.skipped.positive?
|
|
49
50
|
peak = summary.peak_memory_mb
|
|
50
51
|
data[:peak_memory_mb] = peak.round(1) if peak
|
|
51
52
|
data
|
|
@@ -42,7 +42,13 @@ class Evilution::Reporter::Suggestion
|
|
|
42
42
|
"bitwise_replacement" => "Add a test that checks the exact bitwise result to distinguish &, |, and ^ operators",
|
|
43
43
|
"bitwise_complement" => "Add a test that verifies the bitwise complement (~) result, not just the sign or magnitude",
|
|
44
44
|
"zsuper_removal" => "Add a test that verifies inherited behavior from super is needed, not just the subclass logic",
|
|
45
|
-
"explicit_super_mutation" => "Add a test that verifies the correct arguments are passed to super and the inherited result matters"
|
|
45
|
+
"explicit_super_mutation" => "Add a test that verifies the correct arguments are passed to super and the inherited result matters",
|
|
46
|
+
"index_to_fetch" => "Add a test that distinguishes [] (returns nil for missing keys) from .fetch (raises KeyError)",
|
|
47
|
+
"index_to_dig" => "Add a test that verifies chained [] access returns the correct nested value",
|
|
48
|
+
"index_assignment_removal" => "Add a test that verifies the []= assignment side effect is observable (the collection is modified)",
|
|
49
|
+
"pattern_matching_guard" => "Add a test with input that matches the pattern but fails the guard to verify filtering",
|
|
50
|
+
"pattern_matching_alternative" => "Add a test with input that matches only one specific alternative to verify each branch is reachable",
|
|
51
|
+
"pattern_matching_array" => "Add a test that verifies each element position in the array pattern matches the expected type or value"
|
|
46
52
|
}.freeze
|
|
47
53
|
|
|
48
54
|
CONCRETE_TEMPLATES = {
|
|
@@ -541,6 +547,86 @@ class Evilution::Reporter::Suggestion
|
|
|
541
547
|
expect(result).to eq(expected)
|
|
542
548
|
end
|
|
543
549
|
RSPEC
|
|
550
|
+
},
|
|
551
|
+
"index_to_fetch" => lambda { |mutation|
|
|
552
|
+
method_name = parse_method_name(mutation.subject.name)
|
|
553
|
+
original_line, mutated_line = extract_diff_lines(mutation.diff)
|
|
554
|
+
<<~RSPEC.strip
|
|
555
|
+
# Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
|
|
556
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
557
|
+
it 'distinguishes [] from .fetch for missing keys in ##{method_name}' do
|
|
558
|
+
# Access a missing key: [] returns nil, .fetch raises KeyError
|
|
559
|
+
expect { subject.#{method_name}(collection_with_missing_key) }.to raise_error(KeyError)
|
|
560
|
+
end
|
|
561
|
+
RSPEC
|
|
562
|
+
},
|
|
563
|
+
"index_to_dig" => lambda { |mutation|
|
|
564
|
+
method_name = parse_method_name(mutation.subject.name)
|
|
565
|
+
original_line, mutated_line = extract_diff_lines(mutation.diff)
|
|
566
|
+
<<~RSPEC.strip
|
|
567
|
+
# Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
|
|
568
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
569
|
+
it 'verifies the chained [] access returns the correct nested value in ##{method_name}' do
|
|
570
|
+
# Assert the nested lookup produces the expected value
|
|
571
|
+
result = subject.#{method_name}(nested_collection)
|
|
572
|
+
expect(result).to eq(expected)
|
|
573
|
+
end
|
|
574
|
+
RSPEC
|
|
575
|
+
},
|
|
576
|
+
"index_assignment_removal" => lambda { |mutation|
|
|
577
|
+
method_name = parse_method_name(mutation.subject.name)
|
|
578
|
+
original_line, mutated_line = extract_diff_lines(mutation.diff)
|
|
579
|
+
<<~RSPEC.strip
|
|
580
|
+
# Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
|
|
581
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
582
|
+
it 'verifies the []= assignment modifies the collection in ##{method_name}' do
|
|
583
|
+
# Assert the collection contains the assigned value after the method runs
|
|
584
|
+
result = subject.#{method_name}(collection)
|
|
585
|
+
expect(result).to include(expected_key => expected_value)
|
|
586
|
+
end
|
|
587
|
+
RSPEC
|
|
588
|
+
},
|
|
589
|
+
"pattern_matching_guard" => lambda { |mutation|
|
|
590
|
+
method_name = parse_method_name(mutation.subject.name)
|
|
591
|
+
original_line, mutated_line = extract_diff_lines(mutation.diff)
|
|
592
|
+
<<~RSPEC.strip
|
|
593
|
+
# Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
|
|
594
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
595
|
+
it 'verifies the pattern guard filters correctly in ##{method_name}' do
|
|
596
|
+
# Test with input that matches the pattern but fails the guard condition
|
|
597
|
+
# The guard should prevent matching, routing to a different branch
|
|
598
|
+
result = subject.#{method_name}(input_matching_pattern_but_failing_guard)
|
|
599
|
+
expect(result).to eq(expected)
|
|
600
|
+
end
|
|
601
|
+
RSPEC
|
|
602
|
+
},
|
|
603
|
+
"pattern_matching_alternative" => lambda { |mutation|
|
|
604
|
+
method_name = parse_method_name(mutation.subject.name)
|
|
605
|
+
original_line, mutated_line = extract_diff_lines(mutation.diff)
|
|
606
|
+
<<~RSPEC.strip
|
|
607
|
+
# Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
|
|
608
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
609
|
+
it 'verifies each pattern alternative is reachable in ##{method_name}' do
|
|
610
|
+
# Test with input that matches only one specific alternative
|
|
611
|
+
# Each alternative should have a dedicated test case
|
|
612
|
+
result = subject.#{method_name}(input_for_specific_alternative)
|
|
613
|
+
expect(result).to eq(expected)
|
|
614
|
+
end
|
|
615
|
+
RSPEC
|
|
616
|
+
},
|
|
617
|
+
"pattern_matching_array" => lambda { |mutation|
|
|
618
|
+
method_name = parse_method_name(mutation.subject.name)
|
|
619
|
+
original_line, mutated_line = extract_diff_lines(mutation.diff)
|
|
620
|
+
<<~RSPEC.strip
|
|
621
|
+
# Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
|
|
622
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
623
|
+
it 'verifies each array pattern element matters in ##{method_name}' do
|
|
624
|
+
# Test with input where changing one element type causes a different match
|
|
625
|
+
# Each position in the array pattern should be validated
|
|
626
|
+
result = subject.#{method_name}(input_with_wrong_element_type)
|
|
627
|
+
expect(result).to eq(expected)
|
|
628
|
+
end
|
|
629
|
+
RSPEC
|
|
544
630
|
}
|
|
545
631
|
}.freeze
|
|
546
632
|
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
require_relative "../result"
|
|
4
4
|
|
|
5
5
|
class Evilution::Result::Summary
|
|
6
|
-
attr_reader :results, :duration
|
|
6
|
+
attr_reader :results, :duration, :skipped
|
|
7
7
|
|
|
8
|
-
def initialize(results:, duration: 0.0, truncated: false)
|
|
8
|
+
def initialize(results:, duration: 0.0, truncated: false, skipped: 0)
|
|
9
9
|
@results = results
|
|
10
10
|
@duration = duration
|
|
11
11
|
@truncated = truncated
|
|
12
|
+
@skipped = skipped
|
|
12
13
|
freeze
|
|
13
14
|
end
|
|
14
15
|
|
data/lib/evilution/runner.rb
CHANGED
|
@@ -20,13 +20,15 @@ require_relative "baseline"
|
|
|
20
20
|
require_relative "cache"
|
|
21
21
|
require_relative "parallel/pool"
|
|
22
22
|
require_relative "session/store"
|
|
23
|
+
require_relative "ast/pattern/filter"
|
|
23
24
|
|
|
24
25
|
class Evilution::Runner
|
|
25
26
|
attr_reader :config
|
|
26
27
|
|
|
27
|
-
def initialize(config: Evilution::Config.new, on_result: nil)
|
|
28
|
+
def initialize(config: Evilution::Config.new, on_result: nil, hooks: nil)
|
|
28
29
|
@config = config
|
|
29
30
|
@on_result = on_result
|
|
31
|
+
@hooks = hooks
|
|
30
32
|
@parser = Evilution::AST::Parser.new
|
|
31
33
|
@registry = Evilution::Mutator::Registry.default
|
|
32
34
|
@isolator = build_isolator
|
|
@@ -41,7 +43,7 @@ class Evilution::Runner
|
|
|
41
43
|
|
|
42
44
|
baseline_result = run_baseline(subjects)
|
|
43
45
|
|
|
44
|
-
mutations = generate_mutations(subjects)
|
|
46
|
+
mutations, skipped_count = generate_mutations(subjects)
|
|
45
47
|
equivalent_mutations, mutations = filter_equivalent(mutations)
|
|
46
48
|
release_subject_nodes(subjects)
|
|
47
49
|
clear_operator_caches
|
|
@@ -54,7 +56,8 @@ class Evilution::Runner
|
|
|
54
56
|
|
|
55
57
|
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
56
58
|
|
|
57
|
-
summary = Evilution::Result::Summary.new(results: results, duration: duration, truncated: truncated
|
|
59
|
+
summary = Evilution::Result::Summary.new(results: results, duration: duration, truncated: truncated,
|
|
60
|
+
skipped: skipped_count)
|
|
58
61
|
output_report(summary)
|
|
59
62
|
save_session(summary)
|
|
60
63
|
|
|
@@ -63,7 +66,7 @@ class Evilution::Runner
|
|
|
63
66
|
|
|
64
67
|
private
|
|
65
68
|
|
|
66
|
-
attr_reader :parser, :registry, :isolator, :cache, :on_result
|
|
69
|
+
attr_reader :parser, :registry, :isolator, :cache, :on_result, :hooks
|
|
67
70
|
|
|
68
71
|
def parse_and_filter_subjects
|
|
69
72
|
subjects = parse_subjects
|
|
@@ -169,9 +172,18 @@ class Evilution::Runner
|
|
|
169
172
|
end
|
|
170
173
|
|
|
171
174
|
def generate_mutations(subjects)
|
|
172
|
-
|
|
173
|
-
|
|
175
|
+
filter = build_ignore_filter
|
|
176
|
+
mutations = subjects.flat_map do |subject|
|
|
177
|
+
registry.mutations_for(subject, filter: filter)
|
|
174
178
|
end
|
|
179
|
+
[mutations, filter ? filter.skipped_count : 0]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def build_ignore_filter
|
|
183
|
+
patterns = config.ignore_patterns
|
|
184
|
+
return nil if patterns.nil? || patterns.empty?
|
|
185
|
+
|
|
186
|
+
Evilution::AST::Pattern::Filter.new(patterns)
|
|
175
187
|
end
|
|
176
188
|
|
|
177
189
|
def filter_equivalent(mutations)
|
|
@@ -240,7 +252,7 @@ class Evilution::Runner
|
|
|
240
252
|
|
|
241
253
|
def run_mutations_parallel(mutations, baseline_result = nil)
|
|
242
254
|
integration = build_integration
|
|
243
|
-
pool = Evilution::Parallel::Pool.new(size: config.jobs)
|
|
255
|
+
pool = Evilution::Parallel::Pool.new(size: config.jobs, hooks: @hooks)
|
|
244
256
|
worker_isolator = Evilution::Isolation::InProcess.new
|
|
245
257
|
spec_resolver = baseline_result&.failed? ? Evilution::SpecResolver.new : nil
|
|
246
258
|
state = { results: [], survived_count: 0, truncated: false, completed: 0 }
|
|
@@ -341,7 +353,7 @@ class Evilution::Runner
|
|
|
341
353
|
|
|
342
354
|
def build_isolator
|
|
343
355
|
case resolve_isolation
|
|
344
|
-
when :fork then Evilution::Isolation::Fork.new
|
|
356
|
+
when :fork then Evilution::Isolation::Fork.new(hooks: @hooks)
|
|
345
357
|
when :in_process then Evilution::Isolation::InProcess.new
|
|
346
358
|
end
|
|
347
359
|
end
|
|
@@ -356,7 +368,7 @@ class Evilution::Runner
|
|
|
356
368
|
case config.integration
|
|
357
369
|
when :rspec
|
|
358
370
|
test_files = config.spec_files.empty? ? nil : config.spec_files
|
|
359
|
-
Evilution::Integration::RSpec.new(test_files: test_files)
|
|
371
|
+
Evilution::Integration::RSpec.new(test_files: test_files, hooks: @hooks)
|
|
360
372
|
else
|
|
361
373
|
raise Evilution::Error, "unknown integration: #{config.integration}"
|
|
362
374
|
end
|
|
@@ -69,12 +69,13 @@ class Evilution::Session::Store
|
|
|
69
69
|
timed_out_count: summary.timed_out,
|
|
70
70
|
error_count: summary.errors,
|
|
71
71
|
neutral_count: summary.neutral,
|
|
72
|
-
equivalent_count: summary.equivalent
|
|
72
|
+
equivalent_count: summary.equivalent,
|
|
73
|
+
skipped_count: summary.skipped
|
|
73
74
|
}
|
|
74
75
|
end
|
|
75
76
|
|
|
76
77
|
def build_summary(summary)
|
|
77
|
-
{
|
|
78
|
+
data = {
|
|
78
79
|
total: summary.total,
|
|
79
80
|
killed: summary.killed,
|
|
80
81
|
survived: summary.survived,
|
|
@@ -85,6 +86,8 @@ class Evilution::Session::Store
|
|
|
85
86
|
score: summary.score.round(4),
|
|
86
87
|
duration: summary.duration.round(4)
|
|
87
88
|
}
|
|
89
|
+
data[:skipped] = summary.skipped if summary.skipped.positive?
|
|
90
|
+
data
|
|
88
91
|
end
|
|
89
92
|
|
|
90
93
|
def build_mutation_detail(result)
|
data/lib/evilution/version.rb
CHANGED
data/lib/evilution.rb
CHANGED
|
@@ -11,6 +11,12 @@ require_relative "evilution/parallel"
|
|
|
11
11
|
require_relative "evilution/ast/source_surgeon"
|
|
12
12
|
require_relative "evilution/ast/parser"
|
|
13
13
|
require_relative "evilution/ast/inheritance_scanner"
|
|
14
|
+
require_relative "evilution/ast/pattern"
|
|
15
|
+
require_relative "evilution/ast/pattern/matcher"
|
|
16
|
+
require_relative "evilution/ast/pattern/parser"
|
|
17
|
+
require_relative "evilution/hooks"
|
|
18
|
+
require_relative "evilution/hooks/registry"
|
|
19
|
+
require_relative "evilution/hooks/loader"
|
|
14
20
|
require_relative "evilution/mutator"
|
|
15
21
|
require_relative "evilution/mutator/base"
|
|
16
22
|
require_relative "evilution/mutator/operator"
|
|
@@ -60,6 +66,12 @@ require_relative "evilution/mutator/operator/bitwise_replacement"
|
|
|
60
66
|
require_relative "evilution/mutator/operator/bitwise_complement"
|
|
61
67
|
require_relative "evilution/mutator/operator/zsuper_removal"
|
|
62
68
|
require_relative "evilution/mutator/operator/explicit_super_mutation"
|
|
69
|
+
require_relative "evilution/mutator/operator/index_to_fetch"
|
|
70
|
+
require_relative "evilution/mutator/operator/index_to_dig"
|
|
71
|
+
require_relative "evilution/mutator/operator/index_assignment_removal"
|
|
72
|
+
require_relative "evilution/mutator/operator/pattern_matching_guard"
|
|
73
|
+
require_relative "evilution/mutator/operator/pattern_matching_alternative"
|
|
74
|
+
require_relative "evilution/mutator/operator/pattern_matching_array"
|
|
63
75
|
require_relative "evilution/mutator/registry"
|
|
64
76
|
require_relative "evilution/equivalent"
|
|
65
77
|
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.17.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-03-
|
|
11
|
+
date: 2026-03-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: diff-lcs
|
|
@@ -76,11 +76,16 @@ files:
|
|
|
76
76
|
- README.md
|
|
77
77
|
- Rakefile
|
|
78
78
|
- claude-swarm.yml
|
|
79
|
+
- docs/ast_pattern_syntax.md
|
|
79
80
|
- exe/evilution
|
|
80
81
|
- lib/evilution.rb
|
|
81
82
|
- lib/evilution/ast.rb
|
|
82
83
|
- lib/evilution/ast/inheritance_scanner.rb
|
|
83
84
|
- lib/evilution/ast/parser.rb
|
|
85
|
+
- lib/evilution/ast/pattern.rb
|
|
86
|
+
- lib/evilution/ast/pattern/filter.rb
|
|
87
|
+
- lib/evilution/ast/pattern/matcher.rb
|
|
88
|
+
- lib/evilution/ast/pattern/parser.rb
|
|
84
89
|
- lib/evilution/ast/source_surgeon.rb
|
|
85
90
|
- lib/evilution/baseline.rb
|
|
86
91
|
- lib/evilution/cache.rb
|
|
@@ -97,6 +102,9 @@ files:
|
|
|
97
102
|
- lib/evilution/equivalent/heuristic/noop_source.rb
|
|
98
103
|
- lib/evilution/git.rb
|
|
99
104
|
- lib/evilution/git/changed_files.rb
|
|
105
|
+
- lib/evilution/hooks.rb
|
|
106
|
+
- lib/evilution/hooks/loader.rb
|
|
107
|
+
- lib/evilution/hooks/registry.rb
|
|
100
108
|
- lib/evilution/integration.rb
|
|
101
109
|
- lib/evilution/integration/base.rb
|
|
102
110
|
- lib/evilution/integration/rspec.rb
|
|
@@ -138,6 +146,9 @@ files:
|
|
|
138
146
|
- lib/evilution/mutator/operator/float_literal.rb
|
|
139
147
|
- lib/evilution/mutator/operator/global_variable_write.rb
|
|
140
148
|
- lib/evilution/mutator/operator/hash_literal.rb
|
|
149
|
+
- lib/evilution/mutator/operator/index_assignment_removal.rb
|
|
150
|
+
- lib/evilution/mutator/operator/index_to_dig.rb
|
|
151
|
+
- lib/evilution/mutator/operator/index_to_fetch.rb
|
|
141
152
|
- lib/evilution/mutator/operator/inline_rescue.rb
|
|
142
153
|
- lib/evilution/mutator/operator/instance_variable_write.rb
|
|
143
154
|
- lib/evilution/mutator/operator/integer_literal.rb
|
|
@@ -148,6 +159,9 @@ files:
|
|
|
148
159
|
- lib/evilution/mutator/operator/negation_insertion.rb
|
|
149
160
|
- lib/evilution/mutator/operator/next_statement.rb
|
|
150
161
|
- lib/evilution/mutator/operator/nil_replacement.rb
|
|
162
|
+
- lib/evilution/mutator/operator/pattern_matching_alternative.rb
|
|
163
|
+
- lib/evilution/mutator/operator/pattern_matching_array.rb
|
|
164
|
+
- lib/evilution/mutator/operator/pattern_matching_guard.rb
|
|
151
165
|
- lib/evilution/mutator/operator/range_replacement.rb
|
|
152
166
|
- lib/evilution/mutator/operator/receiver_replacement.rb
|
|
153
167
|
- lib/evilution/mutator/operator/redo_statement.rb
|