evilution 0.14.0 → 0.16.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 +41 -41
- data/CHANGELOG.md +43 -0
- data/lib/evilution/ast/inheritance_scanner.rb +70 -0
- data/lib/evilution/ast/parser.rb +10 -6
- data/lib/evilution/cli.rb +6 -1
- data/lib/evilution/config.rb +7 -1
- data/lib/evilution/equivalent/detector.rb +5 -1
- data/lib/evilution/equivalent/heuristic/alias_swap.rb +5 -2
- data/lib/evilution/equivalent/heuristic/arithmetic_identity.rb +30 -0
- data/lib/evilution/equivalent/heuristic/comment_marking.rb +21 -0
- data/lib/evilution/mutator/base.rb +16 -0
- data/lib/evilution/mutator/operator/bang_method.rb +48 -0
- data/lib/evilution/mutator/operator/bitwise_complement.rb +31 -0
- data/lib/evilution/mutator/operator/bitwise_replacement.rb +30 -0
- data/lib/evilution/mutator/operator/break_statement.rb +50 -0
- data/lib/evilution/mutator/operator/class_variable_write.rb +25 -0
- data/lib/evilution/mutator/operator/collection_replacement.rb +25 -1
- data/lib/evilution/mutator/operator/ensure_removal.rb +27 -0
- data/lib/evilution/mutator/operator/explicit_super_mutation.rb +47 -0
- data/lib/evilution/mutator/operator/global_variable_write.rb +25 -0
- data/lib/evilution/mutator/operator/inline_rescue.rb +39 -0
- data/lib/evilution/mutator/operator/instance_variable_write.rb +25 -0
- data/lib/evilution/mutator/operator/local_variable_assignment.rb +16 -0
- data/lib/evilution/mutator/operator/mixin_removal.rb +80 -0
- data/lib/evilution/mutator/operator/next_statement.rb +50 -0
- data/lib/evilution/mutator/operator/redo_statement.rb +18 -0
- data/lib/evilution/mutator/operator/rescue_body_replacement.rb +94 -0
- data/lib/evilution/mutator/operator/rescue_removal.rb +37 -0
- data/lib/evilution/mutator/operator/send_mutation.rb +11 -2
- data/lib/evilution/mutator/operator/superclass_removal.rb +65 -0
- data/lib/evilution/mutator/operator/zsuper_removal.rb +16 -0
- data/lib/evilution/mutator/registry.rb +19 -1
- data/lib/evilution/reporter/progress_bar.rb +84 -0
- data/lib/evilution/reporter/suggestion.rb +253 -1
- data/lib/evilution/runner.rb +105 -19
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +20 -0
- metadata +24 -2
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::RescueBodyReplacement < Evilution::Mutator::Base
|
|
6
|
+
def visit_rescue_node(node)
|
|
7
|
+
generate_nil_replacement(node)
|
|
8
|
+
generate_raise_replacement(node)
|
|
9
|
+
|
|
10
|
+
super
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def generate_nil_replacement(node)
|
|
16
|
+
return if node.statements.nil?
|
|
17
|
+
|
|
18
|
+
body_loc = node.statements.location
|
|
19
|
+
indent = " " * indentation_of(body_loc.start_offset)
|
|
20
|
+
|
|
21
|
+
add_mutation(
|
|
22
|
+
offset: body_loc.start_offset,
|
|
23
|
+
length: body_loc.length,
|
|
24
|
+
replacement: "#{indent}nil".lstrip,
|
|
25
|
+
node: node
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def generate_raise_replacement(node)
|
|
30
|
+
return if bare_raise?(node)
|
|
31
|
+
|
|
32
|
+
if node.statements.nil?
|
|
33
|
+
insert_raise_into_empty(node)
|
|
34
|
+
else
|
|
35
|
+
replace_body_with_raise(node)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def replace_body_with_raise(node)
|
|
40
|
+
body_loc = node.statements.location
|
|
41
|
+
indent = " " * indentation_of(body_loc.start_offset)
|
|
42
|
+
|
|
43
|
+
add_mutation(
|
|
44
|
+
offset: body_loc.start_offset,
|
|
45
|
+
length: body_loc.length,
|
|
46
|
+
replacement: "#{indent}raise".lstrip,
|
|
47
|
+
node: node
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def insert_raise_into_empty(node)
|
|
52
|
+
insert_offset = rescue_line_end(node)
|
|
53
|
+
indent = " " * (indentation_of(node.keyword_loc.start_offset) + 2)
|
|
54
|
+
|
|
55
|
+
add_mutation(
|
|
56
|
+
offset: insert_offset,
|
|
57
|
+
length: 0,
|
|
58
|
+
replacement: "\n#{indent}raise",
|
|
59
|
+
node: node
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def bare_raise?(node)
|
|
64
|
+
return false if node.statements.nil?
|
|
65
|
+
|
|
66
|
+
body = node.statements.body
|
|
67
|
+
body.length == 1 &&
|
|
68
|
+
body.first.is_a?(Prism::CallNode) &&
|
|
69
|
+
body.first.name == :raise &&
|
|
70
|
+
body.first.arguments.nil? &&
|
|
71
|
+
body.first.receiver.nil?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def rescue_line_end(node)
|
|
75
|
+
if node.reference
|
|
76
|
+
node.reference.location.start_offset + node.reference.location.length
|
|
77
|
+
elsif node.exceptions.any?
|
|
78
|
+
last_exc = node.exceptions.last
|
|
79
|
+
last_exc.location.start_offset + last_exc.location.length
|
|
80
|
+
else
|
|
81
|
+
node.keyword_loc.start_offset + node.keyword_loc.length
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def indentation_of(offset)
|
|
86
|
+
pos = offset - 1
|
|
87
|
+
col = 0
|
|
88
|
+
while pos >= 0 && @file_source[pos] != "\n"
|
|
89
|
+
col += 1
|
|
90
|
+
pos -= 1
|
|
91
|
+
end
|
|
92
|
+
col
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::RescueRemoval < Evilution::Mutator::Base
|
|
6
|
+
def visit_rescue_node(node)
|
|
7
|
+
remove_start = line_start_before(node.keyword_loc.start_offset)
|
|
8
|
+
remove_end = rescue_end_offset(node)
|
|
9
|
+
|
|
10
|
+
add_mutation(
|
|
11
|
+
offset: remove_start,
|
|
12
|
+
length: remove_end - remove_start,
|
|
13
|
+
replacement: "",
|
|
14
|
+
node: node
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def rescue_end_offset(node)
|
|
23
|
+
if node.subsequent
|
|
24
|
+
line_start_before(node.subsequent.keyword_loc.start_offset)
|
|
25
|
+
elsif node.statements
|
|
26
|
+
node.statements.location.start_offset + node.statements.location.length
|
|
27
|
+
else
|
|
28
|
+
node.keyword_loc.start_offset + node.keyword_loc.length
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def line_start_before(offset)
|
|
33
|
+
pos = offset - 1
|
|
34
|
+
pos -= 1 while pos.positive? && @file_source[pos] != "\n"
|
|
35
|
+
pos
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -14,13 +14,22 @@ class Evilution::Mutator::Operator::SendMutation < Evilution::Mutator::Base
|
|
|
14
14
|
detect: [:find],
|
|
15
15
|
find: [:detect],
|
|
16
16
|
each_with_object: [:inject],
|
|
17
|
-
inject: [
|
|
17
|
+
inject: %i[each_with_object sum],
|
|
18
18
|
reverse_each: [:each],
|
|
19
19
|
each: [:reverse_each],
|
|
20
20
|
length: [:size],
|
|
21
21
|
size: [:length],
|
|
22
22
|
values_at: [:fetch_values],
|
|
23
|
-
fetch_values: [:values_at]
|
|
23
|
+
fetch_values: [:values_at],
|
|
24
|
+
sum: [:inject],
|
|
25
|
+
count: [:size],
|
|
26
|
+
select: [:filter],
|
|
27
|
+
filter: [:select],
|
|
28
|
+
to_s: [:to_i],
|
|
29
|
+
to_i: [:to_s],
|
|
30
|
+
to_f: [:to_i],
|
|
31
|
+
to_a: [:to_h],
|
|
32
|
+
to_h: [:to_a]
|
|
24
33
|
}.freeze
|
|
25
34
|
|
|
26
35
|
def visit_call_node(node)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../operator"
|
|
6
|
+
|
|
7
|
+
class Evilution::Mutator::Operator::SuperclassRemoval < Evilution::Mutator::Base
|
|
8
|
+
def call(subject)
|
|
9
|
+
@subject = subject
|
|
10
|
+
@file_source = File.read(subject.file_path)
|
|
11
|
+
@mutations = []
|
|
12
|
+
|
|
13
|
+
tree = self.class.parsed_tree_for(subject.file_path, @file_source)
|
|
14
|
+
enclosing = find_enclosing_class(tree, subject.line_number)
|
|
15
|
+
return @mutations unless enclosing
|
|
16
|
+
return @mutations unless enclosing.superclass
|
|
17
|
+
|
|
18
|
+
first_method_line = find_first_method_line(enclosing)
|
|
19
|
+
return @mutations unless first_method_line == subject.line_number
|
|
20
|
+
|
|
21
|
+
name_end = enclosing.constant_path.location.start_offset + enclosing.constant_path.location.length
|
|
22
|
+
superclass_end = enclosing.superclass.location.start_offset + enclosing.superclass.location.length
|
|
23
|
+
|
|
24
|
+
add_mutation(
|
|
25
|
+
offset: name_end,
|
|
26
|
+
length: superclass_end - name_end,
|
|
27
|
+
replacement: "",
|
|
28
|
+
node: enclosing
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
@mutations
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def find_enclosing_class(tree, target_line)
|
|
37
|
+
finder = ClassFinder.new(target_line)
|
|
38
|
+
finder.visit(tree)
|
|
39
|
+
finder.result
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def find_first_method_line(class_node)
|
|
43
|
+
return nil unless class_node.body
|
|
44
|
+
|
|
45
|
+
class_node.body.body.each do |node|
|
|
46
|
+
return node.location.start_line if node.is_a?(Prism::DefNode)
|
|
47
|
+
end
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Visitor to find the ClassNode enclosing a given line number.
|
|
52
|
+
class ClassFinder < Prism::Visitor
|
|
53
|
+
attr_reader :result
|
|
54
|
+
|
|
55
|
+
def initialize(target_line)
|
|
56
|
+
@target_line = target_line
|
|
57
|
+
@result = nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def visit_class_node(node)
|
|
61
|
+
@result = node if @target_line.between?(node.location.start_line, node.location.end_line)
|
|
62
|
+
super
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::ZsuperRemoval < Evilution::Mutator::Base
|
|
6
|
+
def visit_forwarding_super_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
|
|
@@ -33,7 +33,25 @@ class Evilution::Mutator::Registry
|
|
|
33
33
|
Evilution::Mutator::Operator::ReceiverReplacement,
|
|
34
34
|
Evilution::Mutator::Operator::SendMutation,
|
|
35
35
|
Evilution::Mutator::Operator::ArgumentNilSubstitution,
|
|
36
|
-
Evilution::Mutator::Operator::CompoundAssignment
|
|
36
|
+
Evilution::Mutator::Operator::CompoundAssignment,
|
|
37
|
+
Evilution::Mutator::Operator::MixinRemoval,
|
|
38
|
+
Evilution::Mutator::Operator::SuperclassRemoval,
|
|
39
|
+
Evilution::Mutator::Operator::LocalVariableAssignment,
|
|
40
|
+
Evilution::Mutator::Operator::InstanceVariableWrite,
|
|
41
|
+
Evilution::Mutator::Operator::ClassVariableWrite,
|
|
42
|
+
Evilution::Mutator::Operator::GlobalVariableWrite,
|
|
43
|
+
Evilution::Mutator::Operator::RescueRemoval,
|
|
44
|
+
Evilution::Mutator::Operator::RescueBodyReplacement,
|
|
45
|
+
Evilution::Mutator::Operator::InlineRescue,
|
|
46
|
+
Evilution::Mutator::Operator::EnsureRemoval,
|
|
47
|
+
Evilution::Mutator::Operator::BreakStatement,
|
|
48
|
+
Evilution::Mutator::Operator::NextStatement,
|
|
49
|
+
Evilution::Mutator::Operator::RedoStatement,
|
|
50
|
+
Evilution::Mutator::Operator::BangMethod,
|
|
51
|
+
Evilution::Mutator::Operator::BitwiseReplacement,
|
|
52
|
+
Evilution::Mutator::Operator::BitwiseComplement,
|
|
53
|
+
Evilution::Mutator::Operator::ZsuperRemoval,
|
|
54
|
+
Evilution::Mutator::Operator::ExplicitSuperMutation
|
|
37
55
|
].each { |op| registry.register(op) }
|
|
38
56
|
registry
|
|
39
57
|
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../reporter"
|
|
4
|
+
|
|
5
|
+
class Evilution::Reporter::ProgressBar
|
|
6
|
+
attr_reader :total, :width, :completed, :killed, :survived
|
|
7
|
+
|
|
8
|
+
def initialize(total:, output: $stdout, width: 30)
|
|
9
|
+
@total = total
|
|
10
|
+
@output = output
|
|
11
|
+
@width = width
|
|
12
|
+
@completed = 0
|
|
13
|
+
@killed = 0
|
|
14
|
+
@survived = 0
|
|
15
|
+
@tty = self.class.tty?(output)
|
|
16
|
+
@start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def tick(status:)
|
|
20
|
+
@completed += 1
|
|
21
|
+
@killed += 1 if status == :killed
|
|
22
|
+
@survived += 1 if status == :survived
|
|
23
|
+
render
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render
|
|
27
|
+
line = "#{bar_string} #{stats_string} | #{time_string}"
|
|
28
|
+
if @tty
|
|
29
|
+
@output.print("\r#{line}")
|
|
30
|
+
else
|
|
31
|
+
@output.puts(line)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def finish
|
|
36
|
+
render
|
|
37
|
+
@output.print("\n") if @tty
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.tty?(io)
|
|
41
|
+
io.respond_to?(:tty?) && io.tty?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def bar_string
|
|
47
|
+
fraction = @total.positive? ? @completed.to_f / @total : 0
|
|
48
|
+
raw_filled = (fraction * @width).to_i
|
|
49
|
+
filled = raw_filled.clamp(0, @width)
|
|
50
|
+
|
|
51
|
+
interior = if filled <= 0
|
|
52
|
+
" " * @width
|
|
53
|
+
elsif filled >= @width
|
|
54
|
+
"#{"=" * (@width - 1)}>"
|
|
55
|
+
else
|
|
56
|
+
"#{"=" * (filled - 1)}>#{" " * (@width - filled)}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
"[#{interior}]"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def stats_string
|
|
63
|
+
"#{@completed}/#{@total} mutations | #{@killed} killed | #{@survived} survived"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def time_string
|
|
67
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start_time
|
|
68
|
+
remaining = estimate_remaining(elapsed)
|
|
69
|
+
"#{format_time(elapsed)} elapsed | ~#{format_time(remaining)} remaining"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def estimate_remaining(elapsed)
|
|
73
|
+
return 0 unless @completed.positive?
|
|
74
|
+
|
|
75
|
+
remaining = (@total - @completed) * (elapsed / @completed)
|
|
76
|
+
[remaining, 0].max
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def format_time(seconds)
|
|
80
|
+
mins = (seconds / 60).to_i
|
|
81
|
+
secs = (seconds % 60).to_i
|
|
82
|
+
format("%<mins>02d:%<secs>02d", mins: mins, secs: secs)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -24,7 +24,25 @@ class Evilution::Reporter::Suggestion
|
|
|
24
24
|
"collection_replacement" => "Add a test that checks the return value of the collection operation, not just side effects",
|
|
25
25
|
"method_call_removal" => "Add a test that depends on the return value or side effect of this method call",
|
|
26
26
|
"argument_removal" => "Add a test that verifies the correct arguments are passed to this method call",
|
|
27
|
-
"compound_assignment" => "Add a test that verifies the side effect of this compound assignment (the accumulated value matters)"
|
|
27
|
+
"compound_assignment" => "Add a test that verifies the side effect of this compound assignment (the accumulated value matters)",
|
|
28
|
+
"superclass_removal" => "Add a test that exercises inherited behavior from the superclass",
|
|
29
|
+
"mixin_removal" => "Add a test that exercises behavior provided by the included/extended module",
|
|
30
|
+
"local_variable_assignment" => "Add a test that depends on the assigned variable being stored, not just the value expression",
|
|
31
|
+
"instance_variable_write" => "Add a test that verifies the instance variable is set correctly, not just the return value",
|
|
32
|
+
"class_variable_write" => "Add a test that verifies the class variable is set correctly and affects shared state",
|
|
33
|
+
"global_variable_write" => "Add a test that verifies the global variable is set correctly, not just the value expression",
|
|
34
|
+
"rescue_removal" => "Add a test that triggers the rescued exception and verifies the rescue handler behavior",
|
|
35
|
+
"rescue_body_replacement" => "Add a test that triggers the rescued exception and verifies the rescue body produces the correct result",
|
|
36
|
+
"inline_rescue" => "Add a test that triggers the inline rescue and verifies the fallback value is used correctly",
|
|
37
|
+
"ensure_removal" => "Add a test that verifies the ensure cleanup code runs and its side effects are observable",
|
|
38
|
+
"break_statement" => "Add a test that verifies the break condition and the value returned when the loop exits early",
|
|
39
|
+
"next_statement" => "Add a test that verifies the next condition and the value yielded when the iteration skips",
|
|
40
|
+
"redo_statement" => "Add a test that verifies the redo restarts the iteration and the retry logic is necessary",
|
|
41
|
+
"bang_method" => "Add a test that distinguishes in-place mutation from copy semantics (bang vs non-bang)",
|
|
42
|
+
"bitwise_replacement" => "Add a test that checks the exact bitwise result to distinguish &, |, and ^ operators",
|
|
43
|
+
"bitwise_complement" => "Add a test that verifies the bitwise complement (~) result, not just the sign or magnitude",
|
|
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"
|
|
28
46
|
}.freeze
|
|
29
47
|
|
|
30
48
|
CONCRETE_TEMPLATES = {
|
|
@@ -289,6 +307,240 @@ class Evilution::Reporter::Suggestion
|
|
|
289
307
|
expect(result).to be_nil
|
|
290
308
|
end
|
|
291
309
|
RSPEC
|
|
310
|
+
},
|
|
311
|
+
"superclass_removal" => lambda { |mutation|
|
|
312
|
+
method_name = parse_method_name(mutation.subject.name)
|
|
313
|
+
original_line, _mutated_line = extract_diff_lines(mutation.diff)
|
|
314
|
+
<<~RSPEC.strip
|
|
315
|
+
# Mutation: removed superclass from `#{original_line}` in #{mutation.subject.name}
|
|
316
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
317
|
+
it 'depends on inherited behavior in ##{method_name}' do
|
|
318
|
+
# Assert behavior that comes from the superclass
|
|
319
|
+
result = subject.#{method_name}(input_value)
|
|
320
|
+
expect(result).to eq(expected)
|
|
321
|
+
end
|
|
322
|
+
RSPEC
|
|
323
|
+
},
|
|
324
|
+
"local_variable_assignment" => lambda { |mutation|
|
|
325
|
+
method_name = parse_method_name(mutation.subject.name)
|
|
326
|
+
original_line, mutated_line = extract_diff_lines(mutation.diff)
|
|
327
|
+
<<~RSPEC.strip
|
|
328
|
+
# Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
|
|
329
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
330
|
+
it 'verifies the local variable assignment is used in ##{method_name}' do
|
|
331
|
+
# Assert that the assigned variable is read later, not just the value expression
|
|
332
|
+
result = subject.#{method_name}(input_value)
|
|
333
|
+
expect(result).to eq(expected)
|
|
334
|
+
end
|
|
335
|
+
RSPEC
|
|
336
|
+
},
|
|
337
|
+
"instance_variable_write" => lambda { |mutation|
|
|
338
|
+
method_name = parse_method_name(mutation.subject.name)
|
|
339
|
+
original_line, mutated_line = extract_diff_lines(mutation.diff)
|
|
340
|
+
<<~RSPEC.strip
|
|
341
|
+
# Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
|
|
342
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
343
|
+
it 'verifies the instance variable @state is set correctly in ##{method_name}' do
|
|
344
|
+
# Assert that the instance variable holds the expected value after the method runs
|
|
345
|
+
subject.#{method_name}(input_value)
|
|
346
|
+
expect(subject.instance_variable_get(:@variable)).to eq(expected)
|
|
347
|
+
end
|
|
348
|
+
RSPEC
|
|
349
|
+
},
|
|
350
|
+
"class_variable_write" => lambda { |mutation|
|
|
351
|
+
method_name = parse_method_name(mutation.subject.name)
|
|
352
|
+
original_line, mutated_line = extract_diff_lines(mutation.diff)
|
|
353
|
+
<<~RSPEC.strip
|
|
354
|
+
# Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
|
|
355
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
356
|
+
it 'verifies the class variable @@shared state is set correctly in ##{method_name}' do
|
|
357
|
+
# Assert that the class variable holds the expected value and affects shared state
|
|
358
|
+
subject.#{method_name}(input_value)
|
|
359
|
+
expect(described_class.class_variable_get(:@@variable)).to eq(expected)
|
|
360
|
+
end
|
|
361
|
+
RSPEC
|
|
362
|
+
},
|
|
363
|
+
"global_variable_write" => lambda { |mutation|
|
|
364
|
+
method_name = parse_method_name(mutation.subject.name)
|
|
365
|
+
original_line, mutated_line = extract_diff_lines(mutation.diff)
|
|
366
|
+
<<~RSPEC.strip
|
|
367
|
+
# Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
|
|
368
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
369
|
+
it 'verifies the global variable $state is set correctly in ##{method_name}' do
|
|
370
|
+
# Assert that the global variable holds the expected value after the method runs
|
|
371
|
+
subject.#{method_name}(input_value)
|
|
372
|
+
expect($variable).to eq(expected)
|
|
373
|
+
end
|
|
374
|
+
RSPEC
|
|
375
|
+
},
|
|
376
|
+
"mixin_removal" => lambda { |mutation|
|
|
377
|
+
method_name = parse_method_name(mutation.subject.name)
|
|
378
|
+
original_line, _mutated_line = extract_diff_lines(mutation.diff)
|
|
379
|
+
<<~RSPEC.strip
|
|
380
|
+
# Mutation: removed `#{original_line}` in #{mutation.subject.name}
|
|
381
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
382
|
+
it 'depends on behavior from the included module in ##{method_name}' do
|
|
383
|
+
# Assert behavior provided by the mixin
|
|
384
|
+
result = subject.#{method_name}(input_value)
|
|
385
|
+
expect(result).to eq(expected)
|
|
386
|
+
end
|
|
387
|
+
RSPEC
|
|
388
|
+
},
|
|
389
|
+
"rescue_removal" => lambda { |mutation|
|
|
390
|
+
method_name = parse_method_name(mutation.subject.name)
|
|
391
|
+
original_line, _mutated_line = extract_diff_lines(mutation.diff)
|
|
392
|
+
<<~RSPEC.strip
|
|
393
|
+
# Mutation: removed `#{original_line}` in #{mutation.subject.name}
|
|
394
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
395
|
+
it 'verifies the rescue handler is needed in ##{method_name}' do
|
|
396
|
+
# Trigger the rescued exception and assert the handler's effect
|
|
397
|
+
result = subject.#{method_name}(input_that_raises)
|
|
398
|
+
expect(result).to eq(expected)
|
|
399
|
+
end
|
|
400
|
+
RSPEC
|
|
401
|
+
},
|
|
402
|
+
"rescue_body_replacement" => lambda { |mutation|
|
|
403
|
+
method_name = parse_method_name(mutation.subject.name)
|
|
404
|
+
original_line, mutated_line = extract_diff_lines(mutation.diff)
|
|
405
|
+
<<~RSPEC.strip
|
|
406
|
+
# Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
|
|
407
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
408
|
+
it 'verifies the rescue handler produces the correct result in ##{method_name}' do
|
|
409
|
+
# Trigger the exception and assert the rescue body's return value or side effect
|
|
410
|
+
result = subject.#{method_name}(input_that_raises)
|
|
411
|
+
expect(result).to eq(expected)
|
|
412
|
+
end
|
|
413
|
+
RSPEC
|
|
414
|
+
},
|
|
415
|
+
"inline_rescue" => lambda { |mutation|
|
|
416
|
+
method_name = parse_method_name(mutation.subject.name)
|
|
417
|
+
original_line, mutated_line = extract_diff_lines(mutation.diff)
|
|
418
|
+
<<~RSPEC.strip
|
|
419
|
+
# Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
|
|
420
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
421
|
+
it 'verifies the inline rescue fallback value in ##{method_name}' do
|
|
422
|
+
# Trigger the exception and assert the fallback value is correct
|
|
423
|
+
result = subject.#{method_name}(input_that_raises)
|
|
424
|
+
expect(result).to eq(expected)
|
|
425
|
+
end
|
|
426
|
+
RSPEC
|
|
427
|
+
},
|
|
428
|
+
"ensure_removal" => lambda { |mutation|
|
|
429
|
+
method_name = parse_method_name(mutation.subject.name)
|
|
430
|
+
original_line, _mutated_line = extract_diff_lines(mutation.diff)
|
|
431
|
+
<<~RSPEC.strip
|
|
432
|
+
# Mutation: removed ensure block `#{original_line}` in #{mutation.subject.name}
|
|
433
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
434
|
+
it 'verifies the ensure cleanup runs in ##{method_name}' do
|
|
435
|
+
# Assert that the cleanup side effect is observable after the method runs
|
|
436
|
+
subject.#{method_name}(input_value)
|
|
437
|
+
expect(observable_cleanup_effect).to eq(expected)
|
|
438
|
+
end
|
|
439
|
+
RSPEC
|
|
440
|
+
},
|
|
441
|
+
"break_statement" => lambda { |mutation|
|
|
442
|
+
method_name = parse_method_name(mutation.subject.name)
|
|
443
|
+
original_line, mutated_line = extract_diff_lines(mutation.diff)
|
|
444
|
+
<<~RSPEC.strip
|
|
445
|
+
# Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
|
|
446
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
447
|
+
it 'verifies the break exits the loop correctly in ##{method_name}' do
|
|
448
|
+
# Assert the loop exits early and returns the expected value
|
|
449
|
+
result = subject.#{method_name}(input_value)
|
|
450
|
+
expect(result).to eq(expected)
|
|
451
|
+
end
|
|
452
|
+
RSPEC
|
|
453
|
+
},
|
|
454
|
+
"next_statement" => lambda { |mutation|
|
|
455
|
+
method_name = parse_method_name(mutation.subject.name)
|
|
456
|
+
original_line, mutated_line = extract_diff_lines(mutation.diff)
|
|
457
|
+
<<~RSPEC.strip
|
|
458
|
+
# Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
|
|
459
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
460
|
+
it 'verifies the next skips the iteration correctly in ##{method_name}' do
|
|
461
|
+
# Assert the iteration is skipped and the expected value is yielded
|
|
462
|
+
result = subject.#{method_name}(input_value)
|
|
463
|
+
expect(result).to eq(expected)
|
|
464
|
+
end
|
|
465
|
+
RSPEC
|
|
466
|
+
},
|
|
467
|
+
"redo_statement" => lambda { |mutation|
|
|
468
|
+
method_name = parse_method_name(mutation.subject.name)
|
|
469
|
+
original_line, mutated_line = extract_diff_lines(mutation.diff)
|
|
470
|
+
<<~RSPEC.strip
|
|
471
|
+
# Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
|
|
472
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
473
|
+
it 'verifies the redo retry logic is necessary in ##{method_name}' do
|
|
474
|
+
# Assert the iteration restart changes the outcome
|
|
475
|
+
result = subject.#{method_name}(input_value)
|
|
476
|
+
expect(result).to eq(expected)
|
|
477
|
+
end
|
|
478
|
+
RSPEC
|
|
479
|
+
},
|
|
480
|
+
"bitwise_replacement" => lambda { |mutation|
|
|
481
|
+
method_name = parse_method_name(mutation.subject.name)
|
|
482
|
+
original_line, mutated_line = extract_diff_lines(mutation.diff)
|
|
483
|
+
<<~RSPEC.strip
|
|
484
|
+
# Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
|
|
485
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
486
|
+
it 'verifies the exact bitwise result in ##{method_name}' do
|
|
487
|
+
# Assert the exact bit-level result to distinguish &, |, and ^ operators
|
|
488
|
+
result = subject.#{method_name}(input_value)
|
|
489
|
+
expect(result).to eq(expected)
|
|
490
|
+
end
|
|
491
|
+
RSPEC
|
|
492
|
+
},
|
|
493
|
+
"bitwise_complement" => lambda { |mutation|
|
|
494
|
+
method_name = parse_method_name(mutation.subject.name)
|
|
495
|
+
original_line, mutated_line = extract_diff_lines(mutation.diff)
|
|
496
|
+
<<~RSPEC.strip
|
|
497
|
+
# Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
|
|
498
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
499
|
+
it 'verifies the bitwise complement result in ##{method_name}' do
|
|
500
|
+
# Assert the exact complement (~) value, not just sign or magnitude
|
|
501
|
+
result = subject.#{method_name}(input_value)
|
|
502
|
+
expect(result).to eq(expected)
|
|
503
|
+
end
|
|
504
|
+
RSPEC
|
|
505
|
+
},
|
|
506
|
+
"bang_method" => lambda { |mutation|
|
|
507
|
+
method_name = parse_method_name(mutation.subject.name)
|
|
508
|
+
original_line, mutated_line = extract_diff_lines(mutation.diff)
|
|
509
|
+
<<~RSPEC.strip
|
|
510
|
+
# Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
|
|
511
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
512
|
+
it 'verifies in-place vs copy semantics matter in ##{method_name}' do
|
|
513
|
+
# Assert that the original object is or is not modified
|
|
514
|
+
result = subject.#{method_name}(input_value)
|
|
515
|
+
expect(result).to eq(expected)
|
|
516
|
+
end
|
|
517
|
+
RSPEC
|
|
518
|
+
},
|
|
519
|
+
"zsuper_removal" => lambda { |mutation|
|
|
520
|
+
method_name = parse_method_name(mutation.subject.name)
|
|
521
|
+
original_line, mutated_line = extract_diff_lines(mutation.diff)
|
|
522
|
+
<<~RSPEC.strip
|
|
523
|
+
# Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
|
|
524
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
525
|
+
it 'verifies inherited behavior from super is needed in ##{method_name}' do
|
|
526
|
+
# Assert that the result depends on the superclass implementation
|
|
527
|
+
result = subject.#{method_name}(input_value)
|
|
528
|
+
expect(result).to eq(expected)
|
|
529
|
+
end
|
|
530
|
+
RSPEC
|
|
531
|
+
},
|
|
532
|
+
"explicit_super_mutation" => lambda { |mutation|
|
|
533
|
+
method_name = parse_method_name(mutation.subject.name)
|
|
534
|
+
original_line, mutated_line = extract_diff_lines(mutation.diff)
|
|
535
|
+
<<~RSPEC.strip
|
|
536
|
+
# Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
|
|
537
|
+
# #{mutation.file_path}:#{mutation.line}
|
|
538
|
+
it 'verifies the correct arguments are passed to super in ##{method_name}' do
|
|
539
|
+
# Assert the inherited method receives the expected arguments
|
|
540
|
+
result = subject.#{method_name}(input_value)
|
|
541
|
+
expect(result).to eq(expected)
|
|
542
|
+
end
|
|
543
|
+
RSPEC
|
|
292
544
|
}
|
|
293
545
|
}.freeze
|
|
294
546
|
|