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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/.migration-hint-ts +1 -1
  3. data/.beads/issues.jsonl +41 -41
  4. data/CHANGELOG.md +43 -0
  5. data/lib/evilution/ast/inheritance_scanner.rb +70 -0
  6. data/lib/evilution/ast/parser.rb +10 -6
  7. data/lib/evilution/cli.rb +6 -1
  8. data/lib/evilution/config.rb +7 -1
  9. data/lib/evilution/equivalent/detector.rb +5 -1
  10. data/lib/evilution/equivalent/heuristic/alias_swap.rb +5 -2
  11. data/lib/evilution/equivalent/heuristic/arithmetic_identity.rb +30 -0
  12. data/lib/evilution/equivalent/heuristic/comment_marking.rb +21 -0
  13. data/lib/evilution/mutator/base.rb +16 -0
  14. data/lib/evilution/mutator/operator/bang_method.rb +48 -0
  15. data/lib/evilution/mutator/operator/bitwise_complement.rb +31 -0
  16. data/lib/evilution/mutator/operator/bitwise_replacement.rb +30 -0
  17. data/lib/evilution/mutator/operator/break_statement.rb +50 -0
  18. data/lib/evilution/mutator/operator/class_variable_write.rb +25 -0
  19. data/lib/evilution/mutator/operator/collection_replacement.rb +25 -1
  20. data/lib/evilution/mutator/operator/ensure_removal.rb +27 -0
  21. data/lib/evilution/mutator/operator/explicit_super_mutation.rb +47 -0
  22. data/lib/evilution/mutator/operator/global_variable_write.rb +25 -0
  23. data/lib/evilution/mutator/operator/inline_rescue.rb +39 -0
  24. data/lib/evilution/mutator/operator/instance_variable_write.rb +25 -0
  25. data/lib/evilution/mutator/operator/local_variable_assignment.rb +16 -0
  26. data/lib/evilution/mutator/operator/mixin_removal.rb +80 -0
  27. data/lib/evilution/mutator/operator/next_statement.rb +50 -0
  28. data/lib/evilution/mutator/operator/redo_statement.rb +18 -0
  29. data/lib/evilution/mutator/operator/rescue_body_replacement.rb +94 -0
  30. data/lib/evilution/mutator/operator/rescue_removal.rb +37 -0
  31. data/lib/evilution/mutator/operator/send_mutation.rb +11 -2
  32. data/lib/evilution/mutator/operator/superclass_removal.rb +65 -0
  33. data/lib/evilution/mutator/operator/zsuper_removal.rb +16 -0
  34. data/lib/evilution/mutator/registry.rb +19 -1
  35. data/lib/evilution/reporter/progress_bar.rb +84 -0
  36. data/lib/evilution/reporter/suggestion.rb +253 -1
  37. data/lib/evilution/runner.rb +105 -19
  38. data/lib/evilution/version.rb +1 -1
  39. data/lib/evilution.rb +20 -0
  40. 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: [:each_with_object],
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