evilution 0.13.0 → 0.15.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/.migration-hint-ts +1 -1
  3. data/.beads/issues.jsonl +17 -17
  4. data/CHANGELOG.md +39 -0
  5. data/lib/evilution/ast/inheritance_scanner.rb +70 -0
  6. data/lib/evilution/ast/parser.rb +73 -68
  7. data/lib/evilution/ast/source_surgeon.rb +7 -9
  8. data/lib/evilution/ast.rb +4 -0
  9. data/lib/evilution/baseline.rb +73 -75
  10. data/lib/evilution/cache.rb +75 -77
  11. data/lib/evilution/cli.rb +412 -173
  12. data/lib/evilution/config.rb +141 -136
  13. data/lib/evilution/equivalent/detector.rb +29 -27
  14. data/lib/evilution/equivalent/heuristic/alias_swap.rb +32 -33
  15. data/lib/evilution/equivalent/heuristic/arithmetic_identity.rb +30 -0
  16. data/lib/evilution/equivalent/heuristic/comment_marking.rb +21 -0
  17. data/lib/evilution/equivalent/heuristic/dead_code.rb +41 -45
  18. data/lib/evilution/equivalent/heuristic/method_body_nil.rb +11 -15
  19. data/lib/evilution/equivalent/heuristic/noop_source.rb +5 -9
  20. data/lib/evilution/equivalent/heuristic.rb +6 -0
  21. data/lib/evilution/equivalent.rb +4 -0
  22. data/lib/evilution/git/changed_files.rb +35 -37
  23. data/lib/evilution/git.rb +4 -0
  24. data/lib/evilution/integration/base.rb +5 -7
  25. data/lib/evilution/integration/rspec.rb +114 -116
  26. data/lib/evilution/integration.rb +4 -0
  27. data/lib/evilution/isolation/fork.rb +98 -100
  28. data/lib/evilution/isolation/in_process.rb +59 -61
  29. data/lib/evilution/isolation.rb +4 -0
  30. data/lib/evilution/mcp/mutate_tool.rb +172 -143
  31. data/lib/evilution/mcp/server.rb +12 -11
  32. data/lib/evilution/mcp/session_diff_tool.rb +89 -0
  33. data/lib/evilution/mcp/session_list_tool.rb +46 -0
  34. data/lib/evilution/mcp/session_show_tool.rb +53 -0
  35. data/lib/evilution/mcp.rb +4 -0
  36. data/lib/evilution/memory/leak_check.rb +80 -84
  37. data/lib/evilution/memory.rb +34 -36
  38. data/lib/evilution/mutation.rb +40 -42
  39. data/lib/evilution/mutator/base.rb +62 -48
  40. data/lib/evilution/mutator/operator/argument_nil_substitution.rb +32 -36
  41. data/lib/evilution/mutator/operator/argument_removal.rb +32 -36
  42. data/lib/evilution/mutator/operator/arithmetic_replacement.rb +26 -30
  43. data/lib/evilution/mutator/operator/array_literal.rb +18 -22
  44. data/lib/evilution/mutator/operator/block_removal.rb +16 -20
  45. data/lib/evilution/mutator/operator/boolean_literal_replacement.rb +38 -42
  46. data/lib/evilution/mutator/operator/boolean_operator_replacement.rb +41 -45
  47. data/lib/evilution/mutator/operator/collection_replacement.rb +32 -36
  48. data/lib/evilution/mutator/operator/comparison_replacement.rb +24 -28
  49. data/lib/evilution/mutator/operator/compound_assignment.rb +114 -118
  50. data/lib/evilution/mutator/operator/conditional_branch.rb +25 -29
  51. data/lib/evilution/mutator/operator/conditional_flip.rb +26 -30
  52. data/lib/evilution/mutator/operator/conditional_negation.rb +25 -29
  53. data/lib/evilution/mutator/operator/float_literal.rb +22 -26
  54. data/lib/evilution/mutator/operator/hash_literal.rb +18 -22
  55. data/lib/evilution/mutator/operator/integer_literal.rb +2 -0
  56. data/lib/evilution/mutator/operator/method_body_replacement.rb +12 -16
  57. data/lib/evilution/mutator/operator/method_call_removal.rb +12 -16
  58. data/lib/evilution/mutator/operator/mixin_removal.rb +80 -0
  59. data/lib/evilution/mutator/operator/negation_insertion.rb +12 -16
  60. data/lib/evilution/mutator/operator/nil_replacement.rb +13 -17
  61. data/lib/evilution/mutator/operator/range_replacement.rb +12 -16
  62. data/lib/evilution/mutator/operator/receiver_replacement.rb +16 -20
  63. data/lib/evilution/mutator/operator/regexp_mutation.rb +15 -19
  64. data/lib/evilution/mutator/operator/return_value_removal.rb +12 -16
  65. data/lib/evilution/mutator/operator/send_mutation.rb +36 -40
  66. data/lib/evilution/mutator/operator/statement_deletion.rb +13 -17
  67. data/lib/evilution/mutator/operator/string_literal.rb +18 -22
  68. data/lib/evilution/mutator/operator/superclass_removal.rb +65 -0
  69. data/lib/evilution/mutator/operator/symbol_literal.rb +17 -21
  70. data/lib/evilution/mutator/operator.rb +6 -0
  71. data/lib/evilution/mutator/registry.rb +56 -56
  72. data/lib/evilution/mutator.rb +4 -0
  73. data/lib/evilution/parallel/pool.rb +56 -58
  74. data/lib/evilution/parallel.rb +4 -0
  75. data/lib/evilution/reporter/cli.rb +99 -101
  76. data/lib/evilution/reporter/html.rb +242 -244
  77. data/lib/evilution/reporter/json.rb +57 -59
  78. data/lib/evilution/reporter/suggestion.rb +354 -328
  79. data/lib/evilution/reporter.rb +4 -0
  80. data/lib/evilution/result/mutation_result.rb +43 -46
  81. data/lib/evilution/result/summary.rb +80 -81
  82. data/lib/evilution/result.rb +4 -0
  83. data/lib/evilution/runner.rb +401 -316
  84. data/lib/evilution/session/store.rb +147 -0
  85. data/lib/evilution/session.rb +4 -0
  86. data/lib/evilution/spec_resolver.rb +49 -47
  87. data/lib/evilution/subject.rb +14 -16
  88. data/lib/evilution/version.rb +1 -1
  89. data/lib/evilution.rb +16 -0
  90. metadata +24 -2
@@ -1,341 +1,367 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Evilution
4
- module Reporter
5
- class Suggestion
6
- TEMPLATES = {
7
- "comparison_replacement" => "Add a test for the boundary condition where the comparison operand equals the threshold exactly",
8
- "arithmetic_replacement" => "Add a test that verifies the arithmetic result, not just truthiness of the outcome",
9
- "boolean_operator_replacement" => "Add a test where only one of the boolean conditions is true to distinguish && from ||",
10
- "boolean_literal_replacement" => "Add a test that exercises the false/true branch explicitly",
11
- "nil_replacement" => "Add a test that asserts the return value is not nil",
12
- "integer_literal" => "Add a test that checks the exact numeric value, not just > 0 or truthy",
13
- "float_literal" => "Add a test that checks the exact floating-point value returned",
14
- "string_literal" => "Add a test that asserts the string content, not just its presence",
15
- "array_literal" => "Add a test that verifies the array contents or length",
16
- "hash_literal" => "Add a test that verifies the hash keys and values",
17
- "symbol_literal" => "Add a test that checks the exact symbol returned",
18
- "conditional_negation" => "Add tests for both the true and false branches of this conditional",
19
- "conditional_branch" => "Add a test that exercises the removed branch of this conditional",
20
- "statement_deletion" => "Add a test that depends on the side effect of this statement",
21
- "method_body_replacement" => "Add a test that checks the method's return value or side effects",
22
- "negation_insertion" => "Add a test where the predicate result matters (not just truthiness)",
23
- "return_value_removal" => "Add a test that uses the return value of this method",
24
- "collection_replacement" => "Add a test that checks the return value of the collection operation, not just side effects",
25
- "method_call_removal" => "Add a test that depends on the return value or side effect of this method call",
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)"
28
- }.freeze
3
+ require_relative "../reporter"
29
4
 
30
- CONCRETE_TEMPLATES = {
31
- "comparison_replacement" => lambda { |mutation|
32
- method_name = parse_method_name(mutation.subject.name)
33
- original_line, mutated_line = extract_diff_lines(mutation.diff)
34
- <<~RSPEC.strip
35
- # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
36
- # #{mutation.file_path}:#{mutation.line}
37
- it 'returns the correct result at the comparison boundary in ##{method_name}' do
38
- # Test with values where the original operator and mutated operator
39
- # produce different results (e.g., equal values for > vs >=)
40
- result = subject.#{method_name}(boundary_value)
41
- expect(result).to eq(expected)
42
- end
43
- RSPEC
44
- },
45
- "arithmetic_replacement" => lambda { |mutation|
46
- method_name = parse_method_name(mutation.subject.name)
47
- original_line, mutated_line = extract_diff_lines(mutation.diff)
48
- <<~RSPEC.strip
49
- # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
50
- # #{mutation.file_path}:#{mutation.line}
51
- it 'computes the correct arithmetic result in ##{method_name}' do
52
- # Assert the exact numeric result, not just truthiness or sign
53
- result = subject.#{method_name}(input_value)
54
- expect(result).to eq(expected)
55
- end
56
- RSPEC
57
- },
58
- "boolean_operator_replacement" => lambda { |mutation|
59
- method_name = parse_method_name(mutation.subject.name)
60
- original_line, mutated_line = extract_diff_lines(mutation.diff)
61
- <<~RSPEC.strip
62
- # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
63
- # #{mutation.file_path}:#{mutation.line}
64
- it 'returns the correct result when one condition is true and one is false in ##{method_name}' do
65
- # Use inputs where only one operand is truthy to distinguish && from ||
66
- result = subject.#{method_name}(input_value)
67
- expect(result).to eq(expected)
68
- end
69
- RSPEC
70
- },
71
- "boolean_literal_replacement" => lambda { |mutation|
72
- method_name = parse_method_name(mutation.subject.name)
73
- original_line, mutated_line = extract_diff_lines(mutation.diff)
74
- <<~RSPEC.strip
75
- # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
76
- # #{mutation.file_path}:#{mutation.line}
77
- it 'returns the expected boolean value from ##{method_name}' do
78
- # Assert the exact true/false/nil value, not just truthiness
79
- result = subject.#{method_name}(input_value)
80
- expect(result).to eq(expected)
81
- end
82
- RSPEC
83
- },
84
- "negation_insertion" => lambda { |mutation|
85
- method_name = parse_method_name(mutation.subject.name)
86
- original_line, mutated_line = extract_diff_lines(mutation.diff)
87
- <<~RSPEC.strip
88
- # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
89
- # #{mutation.file_path}:#{mutation.line}
90
- it 'returns the correct boolean from the predicate in ##{method_name}' do
91
- # Assert the exact true/false result, not just truthiness
92
- result = subject.#{method_name}(input_value)
93
- expect(result).to eq(true).or eq(false)
94
- end
95
- RSPEC
96
- },
97
- "integer_literal" => lambda { |mutation|
98
- method_name = parse_method_name(mutation.subject.name)
99
- original_line, mutated_line = extract_diff_lines(mutation.diff)
100
- <<~RSPEC.strip
101
- # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
102
- # #{mutation.file_path}:#{mutation.line}
103
- it 'returns the exact integer value from ##{method_name}' do
104
- # Assert the exact numeric value, not just > 0 or truthy
105
- result = subject.#{method_name}(input_value)
106
- expect(result).to eq(expected)
107
- end
108
- RSPEC
109
- },
110
- "float_literal" => lambda { |mutation|
111
- method_name = parse_method_name(mutation.subject.name)
112
- original_line, mutated_line = extract_diff_lines(mutation.diff)
113
- <<~RSPEC.strip
114
- # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
115
- # #{mutation.file_path}:#{mutation.line}
116
- it 'returns the exact float value from ##{method_name}' do
117
- # Assert the exact floating-point result
118
- result = subject.#{method_name}(input_value)
119
- expect(result).to eq(expected)
120
- end
121
- RSPEC
122
- },
123
- "string_literal" => lambda { |mutation|
124
- method_name = parse_method_name(mutation.subject.name)
125
- original_line, mutated_line = extract_diff_lines(mutation.diff)
126
- <<~RSPEC.strip
127
- # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
128
- # #{mutation.file_path}:#{mutation.line}
129
- it 'returns the exact string content from ##{method_name}' do
130
- # Assert the exact string value, not just presence or non-empty
131
- result = subject.#{method_name}(input_value)
132
- expect(result).to eq(expected)
133
- end
134
- RSPEC
135
- },
136
- "symbol_literal" => lambda { |mutation|
137
- method_name = parse_method_name(mutation.subject.name)
138
- original_line, mutated_line = extract_diff_lines(mutation.diff)
139
- <<~RSPEC.strip
140
- # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
141
- # #{mutation.file_path}:#{mutation.line}
142
- it 'returns the exact symbol from ##{method_name}' do
143
- # Assert the exact symbol value, not just that it is a Symbol
144
- result = subject.#{method_name}(input_value)
145
- expect(result).to eq(expected)
146
- end
147
- RSPEC
148
- },
149
- "array_literal" => lambda { |mutation|
150
- method_name = parse_method_name(mutation.subject.name)
151
- original_line, mutated_line = extract_diff_lines(mutation.diff)
152
- <<~RSPEC.strip
153
- # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
154
- # #{mutation.file_path}:#{mutation.line}
155
- it 'returns the expected array contents from ##{method_name}' do
156
- # Assert the exact array elements, not just non-empty or truthy
157
- result = subject.#{method_name}(input_value)
158
- expect(result).to eq(expected)
159
- end
160
- RSPEC
161
- },
162
- "hash_literal" => lambda { |mutation|
163
- method_name = parse_method_name(mutation.subject.name)
164
- original_line, mutated_line = extract_diff_lines(mutation.diff)
165
- <<~RSPEC.strip
166
- # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
167
- # #{mutation.file_path}:#{mutation.line}
168
- it 'returns the expected hash contents from ##{method_name}' do
169
- # Assert the exact keys and values, not just non-empty or truthy
170
- result = subject.#{method_name}(input_value)
171
- expect(result).to eq(expected)
172
- end
173
- RSPEC
174
- },
175
- "collection_replacement" => lambda { |mutation|
176
- method_name = parse_method_name(mutation.subject.name)
177
- original_line, mutated_line = extract_diff_lines(mutation.diff)
178
- <<~RSPEC.strip
179
- # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
180
- # #{mutation.file_path}:#{mutation.line}
181
- it 'uses the return value of the collection operation in ##{method_name}' do
182
- # Assert the return value of the collection method, not just side effects
183
- result = subject.#{method_name}(input_value)
184
- expect(result).to eq(expected)
185
- end
186
- RSPEC
187
- },
188
- "conditional_negation" => lambda { |mutation|
189
- method_name = parse_method_name(mutation.subject.name)
190
- original_line, mutated_line = extract_diff_lines(mutation.diff)
191
- <<~RSPEC.strip
192
- # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
193
- # #{mutation.file_path}:#{mutation.line}
194
- it 'exercises both branches of the conditional in ##{method_name}' do
195
- # Test with inputs that make the condition true AND false
196
- result = subject.#{method_name}(input_value)
197
- expect(result).to eq(expected)
198
- end
199
- RSPEC
200
- },
201
- "conditional_branch" => lambda { |mutation|
202
- method_name = parse_method_name(mutation.subject.name)
203
- original_line, mutated_line = extract_diff_lines(mutation.diff)
204
- <<~RSPEC.strip
205
- # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
206
- # #{mutation.file_path}:#{mutation.line}
207
- it 'exercises the removed branch of the conditional in ##{method_name}' do
208
- # Test with inputs that trigger the branch removed by this mutation
209
- result = subject.#{method_name}(input_value)
210
- expect(result).to eq(expected)
211
- end
212
- RSPEC
213
- },
214
- "statement_deletion" => lambda { |mutation|
215
- method_name = parse_method_name(mutation.subject.name)
216
- original_line, _mutated_line = extract_diff_lines(mutation.diff)
217
- <<~RSPEC.strip
218
- # Mutation: deleted `#{original_line}` in #{mutation.subject.name}
219
- # #{mutation.file_path}:#{mutation.line}
220
- it 'depends on the side effect of the deleted statement in ##{method_name}' do
221
- # Assert a side effect or return value that changes when this statement is removed
222
- subject.#{method_name}(input_value)
223
- expect(observable_side_effect).to eq(expected)
224
- end
225
- RSPEC
226
- },
227
- "method_body_replacement" => lambda { |mutation|
228
- method_name = parse_method_name(mutation.subject.name)
229
- original_line, mutated_line = extract_diff_lines(mutation.diff)
230
- <<~RSPEC.strip
231
- # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
232
- # #{mutation.file_path}:#{mutation.line}
233
- it 'verifies the return value or side effects of ##{method_name}' do
234
- # Assert the method produces a meaningful result, not just nil
235
- result = subject.#{method_name}(input_value)
236
- expect(result).to eq(expected)
237
- end
238
- RSPEC
239
- },
240
- "return_value_removal" => lambda { |mutation|
241
- method_name = parse_method_name(mutation.subject.name)
242
- original_line, mutated_line = extract_diff_lines(mutation.diff)
243
- <<~RSPEC.strip
244
- # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
245
- # #{mutation.file_path}:#{mutation.line}
246
- it 'uses the return value of ##{method_name}' do
247
- # Assert the caller depends on the return value, not just side effects
248
- result = subject.#{method_name}(input_value)
249
- expect(result).to eq(expected)
250
- end
251
- RSPEC
252
- },
253
- "method_call_removal" => lambda { |mutation|
254
- method_name = parse_method_name(mutation.subject.name)
255
- original_line, mutated_line = extract_diff_lines(mutation.diff)
256
- <<~RSPEC.strip
257
- # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
258
- # #{mutation.file_path}:#{mutation.line}
259
- it 'depends on the return value or side effect of the call in ##{method_name}' do
260
- # Assert the method call's effect is observable
261
- result = subject.#{method_name}(input_value)
262
- expect(result).to eq(expected)
263
- end
264
- RSPEC
265
- },
266
- "compound_assignment" => lambda { |mutation|
267
- method_name = parse_method_name(mutation.subject.name)
268
- original_line, mutated_line = extract_diff_lines(mutation.diff)
269
- <<~RSPEC.strip
270
- # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
271
- # #{mutation.file_path}:#{mutation.line}
272
- it 'verifies the compound assignment side effect in ##{method_name}' do
273
- # Assert the accumulated value after the compound assignment
274
- # The mutation changes the operator, so the final value will differ
275
- subject.#{method_name}(input_value)
276
- expect(observable_side_effect).to eq(expected)
277
- end
278
- RSPEC
279
- },
280
- "nil_replacement" => lambda { |mutation|
281
- method_name = parse_method_name(mutation.subject.name)
282
- original_line, mutated_line = extract_diff_lines(mutation.diff)
283
- <<~RSPEC.strip
284
- # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
285
- # #{mutation.file_path}:#{mutation.line}
286
- it 'asserts the nil return value from ##{method_name}' do
287
- # Assert the method returns nil, not a substituted value
288
- result = subject.#{method_name}(input_value)
289
- expect(result).to be_nil
290
- end
291
- RSPEC
292
- }
293
- }.freeze
5
+ class Evilution::Reporter::Suggestion
6
+ TEMPLATES = {
7
+ "comparison_replacement" => "Add a test for the boundary condition where the comparison operand equals the threshold exactly",
8
+ "arithmetic_replacement" => "Add a test that verifies the arithmetic result, not just truthiness of the outcome",
9
+ "boolean_operator_replacement" => "Add a test where only one of the boolean conditions is true to distinguish && from ||",
10
+ "boolean_literal_replacement" => "Add a test that exercises the false/true branch explicitly",
11
+ "nil_replacement" => "Add a test that asserts the return value is not nil",
12
+ "integer_literal" => "Add a test that checks the exact numeric value, not just > 0 or truthy",
13
+ "float_literal" => "Add a test that checks the exact floating-point value returned",
14
+ "string_literal" => "Add a test that asserts the string content, not just its presence",
15
+ "array_literal" => "Add a test that verifies the array contents or length",
16
+ "hash_literal" => "Add a test that verifies the hash keys and values",
17
+ "symbol_literal" => "Add a test that checks the exact symbol returned",
18
+ "conditional_negation" => "Add tests for both the true and false branches of this conditional",
19
+ "conditional_branch" => "Add a test that exercises the removed branch of this conditional",
20
+ "statement_deletion" => "Add a test that depends on the side effect of this statement",
21
+ "method_body_replacement" => "Add a test that checks the method's return value or side effects",
22
+ "negation_insertion" => "Add a test where the predicate result matters (not just truthiness)",
23
+ "return_value_removal" => "Add a test that uses the return value of this method",
24
+ "collection_replacement" => "Add a test that checks the return value of the collection operation, not just side effects",
25
+ "method_call_removal" => "Add a test that depends on the return value or side effect of this method call",
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)",
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
+ }.freeze
294
31
 
295
- DEFAULT_SUGGESTION = "Add a more specific test that detects this mutation"
32
+ CONCRETE_TEMPLATES = {
33
+ "comparison_replacement" => lambda { |mutation|
34
+ method_name = parse_method_name(mutation.subject.name)
35
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
36
+ <<~RSPEC.strip
37
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
38
+ # #{mutation.file_path}:#{mutation.line}
39
+ it 'returns the correct result at the comparison boundary in ##{method_name}' do
40
+ # Test with values where the original operator and mutated operator
41
+ # produce different results (e.g., equal values for > vs >=)
42
+ result = subject.#{method_name}(boundary_value)
43
+ expect(result).to eq(expected)
44
+ end
45
+ RSPEC
46
+ },
47
+ "arithmetic_replacement" => lambda { |mutation|
48
+ method_name = parse_method_name(mutation.subject.name)
49
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
50
+ <<~RSPEC.strip
51
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
52
+ # #{mutation.file_path}:#{mutation.line}
53
+ it 'computes the correct arithmetic result in ##{method_name}' do
54
+ # Assert the exact numeric result, not just truthiness or sign
55
+ result = subject.#{method_name}(input_value)
56
+ expect(result).to eq(expected)
57
+ end
58
+ RSPEC
59
+ },
60
+ "boolean_operator_replacement" => lambda { |mutation|
61
+ method_name = parse_method_name(mutation.subject.name)
62
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
63
+ <<~RSPEC.strip
64
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
65
+ # #{mutation.file_path}:#{mutation.line}
66
+ it 'returns the correct result when one condition is true and one is false in ##{method_name}' do
67
+ # Use inputs where only one operand is truthy to distinguish && from ||
68
+ result = subject.#{method_name}(input_value)
69
+ expect(result).to eq(expected)
70
+ end
71
+ RSPEC
72
+ },
73
+ "boolean_literal_replacement" => lambda { |mutation|
74
+ method_name = parse_method_name(mutation.subject.name)
75
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
76
+ <<~RSPEC.strip
77
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
78
+ # #{mutation.file_path}:#{mutation.line}
79
+ it 'returns the expected boolean value from ##{method_name}' do
80
+ # Assert the exact true/false/nil value, not just truthiness
81
+ result = subject.#{method_name}(input_value)
82
+ expect(result).to eq(expected)
83
+ end
84
+ RSPEC
85
+ },
86
+ "negation_insertion" => lambda { |mutation|
87
+ method_name = parse_method_name(mutation.subject.name)
88
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
89
+ <<~RSPEC.strip
90
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
91
+ # #{mutation.file_path}:#{mutation.line}
92
+ it 'returns the correct boolean from the predicate in ##{method_name}' do
93
+ # Assert the exact true/false result, not just truthiness
94
+ result = subject.#{method_name}(input_value)
95
+ expect(result).to eq(true).or eq(false)
96
+ end
97
+ RSPEC
98
+ },
99
+ "integer_literal" => lambda { |mutation|
100
+ method_name = parse_method_name(mutation.subject.name)
101
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
102
+ <<~RSPEC.strip
103
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
104
+ # #{mutation.file_path}:#{mutation.line}
105
+ it 'returns the exact integer value from ##{method_name}' do
106
+ # Assert the exact numeric value, not just > 0 or truthy
107
+ result = subject.#{method_name}(input_value)
108
+ expect(result).to eq(expected)
109
+ end
110
+ RSPEC
111
+ },
112
+ "float_literal" => lambda { |mutation|
113
+ method_name = parse_method_name(mutation.subject.name)
114
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
115
+ <<~RSPEC.strip
116
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
117
+ # #{mutation.file_path}:#{mutation.line}
118
+ it 'returns the exact float value from ##{method_name}' do
119
+ # Assert the exact floating-point result
120
+ result = subject.#{method_name}(input_value)
121
+ expect(result).to eq(expected)
122
+ end
123
+ RSPEC
124
+ },
125
+ "string_literal" => lambda { |mutation|
126
+ method_name = parse_method_name(mutation.subject.name)
127
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
128
+ <<~RSPEC.strip
129
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
130
+ # #{mutation.file_path}:#{mutation.line}
131
+ it 'returns the exact string content from ##{method_name}' do
132
+ # Assert the exact string value, not just presence or non-empty
133
+ result = subject.#{method_name}(input_value)
134
+ expect(result).to eq(expected)
135
+ end
136
+ RSPEC
137
+ },
138
+ "symbol_literal" => lambda { |mutation|
139
+ method_name = parse_method_name(mutation.subject.name)
140
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
141
+ <<~RSPEC.strip
142
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
143
+ # #{mutation.file_path}:#{mutation.line}
144
+ it 'returns the exact symbol from ##{method_name}' do
145
+ # Assert the exact symbol value, not just that it is a Symbol
146
+ result = subject.#{method_name}(input_value)
147
+ expect(result).to eq(expected)
148
+ end
149
+ RSPEC
150
+ },
151
+ "array_literal" => lambda { |mutation|
152
+ method_name = parse_method_name(mutation.subject.name)
153
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
154
+ <<~RSPEC.strip
155
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
156
+ # #{mutation.file_path}:#{mutation.line}
157
+ it 'returns the expected array contents from ##{method_name}' do
158
+ # Assert the exact array elements, not just non-empty or truthy
159
+ result = subject.#{method_name}(input_value)
160
+ expect(result).to eq(expected)
161
+ end
162
+ RSPEC
163
+ },
164
+ "hash_literal" => lambda { |mutation|
165
+ method_name = parse_method_name(mutation.subject.name)
166
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
167
+ <<~RSPEC.strip
168
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
169
+ # #{mutation.file_path}:#{mutation.line}
170
+ it 'returns the expected hash contents from ##{method_name}' do
171
+ # Assert the exact keys and values, not just non-empty or truthy
172
+ result = subject.#{method_name}(input_value)
173
+ expect(result).to eq(expected)
174
+ end
175
+ RSPEC
176
+ },
177
+ "collection_replacement" => lambda { |mutation|
178
+ method_name = parse_method_name(mutation.subject.name)
179
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
180
+ <<~RSPEC.strip
181
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
182
+ # #{mutation.file_path}:#{mutation.line}
183
+ it 'uses the return value of the collection operation in ##{method_name}' do
184
+ # Assert the return value of the collection method, not just side effects
185
+ result = subject.#{method_name}(input_value)
186
+ expect(result).to eq(expected)
187
+ end
188
+ RSPEC
189
+ },
190
+ "conditional_negation" => lambda { |mutation|
191
+ method_name = parse_method_name(mutation.subject.name)
192
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
193
+ <<~RSPEC.strip
194
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
195
+ # #{mutation.file_path}:#{mutation.line}
196
+ it 'exercises both branches of the conditional in ##{method_name}' do
197
+ # Test with inputs that make the condition true AND false
198
+ result = subject.#{method_name}(input_value)
199
+ expect(result).to eq(expected)
200
+ end
201
+ RSPEC
202
+ },
203
+ "conditional_branch" => lambda { |mutation|
204
+ method_name = parse_method_name(mutation.subject.name)
205
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
206
+ <<~RSPEC.strip
207
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
208
+ # #{mutation.file_path}:#{mutation.line}
209
+ it 'exercises the removed branch of the conditional in ##{method_name}' do
210
+ # Test with inputs that trigger the branch removed by this mutation
211
+ result = subject.#{method_name}(input_value)
212
+ expect(result).to eq(expected)
213
+ end
214
+ RSPEC
215
+ },
216
+ "statement_deletion" => lambda { |mutation|
217
+ method_name = parse_method_name(mutation.subject.name)
218
+ original_line, _mutated_line = extract_diff_lines(mutation.diff)
219
+ <<~RSPEC.strip
220
+ # Mutation: deleted `#{original_line}` in #{mutation.subject.name}
221
+ # #{mutation.file_path}:#{mutation.line}
222
+ it 'depends on the side effect of the deleted statement in ##{method_name}' do
223
+ # Assert a side effect or return value that changes when this statement is removed
224
+ subject.#{method_name}(input_value)
225
+ expect(observable_side_effect).to eq(expected)
226
+ end
227
+ RSPEC
228
+ },
229
+ "method_body_replacement" => lambda { |mutation|
230
+ method_name = parse_method_name(mutation.subject.name)
231
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
232
+ <<~RSPEC.strip
233
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
234
+ # #{mutation.file_path}:#{mutation.line}
235
+ it 'verifies the return value or side effects of ##{method_name}' do
236
+ # Assert the method produces a meaningful result, not just nil
237
+ result = subject.#{method_name}(input_value)
238
+ expect(result).to eq(expected)
239
+ end
240
+ RSPEC
241
+ },
242
+ "return_value_removal" => lambda { |mutation|
243
+ method_name = parse_method_name(mutation.subject.name)
244
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
245
+ <<~RSPEC.strip
246
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
247
+ # #{mutation.file_path}:#{mutation.line}
248
+ it 'uses the return value of ##{method_name}' do
249
+ # Assert the caller depends on the return value, not just side effects
250
+ result = subject.#{method_name}(input_value)
251
+ expect(result).to eq(expected)
252
+ end
253
+ RSPEC
254
+ },
255
+ "method_call_removal" => lambda { |mutation|
256
+ method_name = parse_method_name(mutation.subject.name)
257
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
258
+ <<~RSPEC.strip
259
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
260
+ # #{mutation.file_path}:#{mutation.line}
261
+ it 'depends on the return value or side effect of the call in ##{method_name}' do
262
+ # Assert the method call's effect is observable
263
+ result = subject.#{method_name}(input_value)
264
+ expect(result).to eq(expected)
265
+ end
266
+ RSPEC
267
+ },
268
+ "compound_assignment" => lambda { |mutation|
269
+ method_name = parse_method_name(mutation.subject.name)
270
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
271
+ <<~RSPEC.strip
272
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
273
+ # #{mutation.file_path}:#{mutation.line}
274
+ it 'verifies the compound assignment side effect in ##{method_name}' do
275
+ # Assert the accumulated value after the compound assignment
276
+ # The mutation changes the operator, so the final value will differ
277
+ subject.#{method_name}(input_value)
278
+ expect(observable_side_effect).to eq(expected)
279
+ end
280
+ RSPEC
281
+ },
282
+ "nil_replacement" => lambda { |mutation|
283
+ method_name = parse_method_name(mutation.subject.name)
284
+ original_line, mutated_line = extract_diff_lines(mutation.diff)
285
+ <<~RSPEC.strip
286
+ # Mutation: changed `#{original_line}` to `#{mutated_line}` in #{mutation.subject.name}
287
+ # #{mutation.file_path}:#{mutation.line}
288
+ it 'asserts the nil return value from ##{method_name}' do
289
+ # Assert the method returns nil, not a substituted value
290
+ result = subject.#{method_name}(input_value)
291
+ expect(result).to be_nil
292
+ end
293
+ RSPEC
294
+ },
295
+ "superclass_removal" => lambda { |mutation|
296
+ method_name = parse_method_name(mutation.subject.name)
297
+ original_line, _mutated_line = extract_diff_lines(mutation.diff)
298
+ <<~RSPEC.strip
299
+ # Mutation: removed superclass from `#{original_line}` in #{mutation.subject.name}
300
+ # #{mutation.file_path}:#{mutation.line}
301
+ it 'depends on inherited behavior in ##{method_name}' do
302
+ # Assert behavior that comes from the superclass
303
+ result = subject.#{method_name}(input_value)
304
+ expect(result).to eq(expected)
305
+ end
306
+ RSPEC
307
+ },
308
+ "mixin_removal" => lambda { |mutation|
309
+ method_name = parse_method_name(mutation.subject.name)
310
+ original_line, _mutated_line = extract_diff_lines(mutation.diff)
311
+ <<~RSPEC.strip
312
+ # Mutation: removed `#{original_line}` in #{mutation.subject.name}
313
+ # #{mutation.file_path}:#{mutation.line}
314
+ it 'depends on behavior from the included module in ##{method_name}' do
315
+ # Assert behavior provided by the mixin
316
+ result = subject.#{method_name}(input_value)
317
+ expect(result).to eq(expected)
318
+ end
319
+ RSPEC
320
+ }
321
+ }.freeze
296
322
 
297
- def initialize(suggest_tests: false)
298
- @suggest_tests = suggest_tests
299
- end
323
+ DEFAULT_SUGGESTION = "Add a more specific test that detects this mutation"
300
324
 
301
- # Generate suggestions for survived mutations.
302
- #
303
- # @param summary [Result::Summary]
304
- # @return [Array<Hash>] Array of { mutation:, suggestion: }
305
- def call(summary)
306
- summary.survived_results.map do |result|
307
- {
308
- mutation: result.mutation,
309
- suggestion: suggestion_for(result.mutation)
310
- }
311
- end
312
- end
325
+ def initialize(suggest_tests: false)
326
+ @suggest_tests = suggest_tests
327
+ end
313
328
 
314
- # Generate a suggestion for a single mutation.
315
- #
316
- # @param mutation [Mutation]
317
- # @return [String]
318
- def suggestion_for(mutation)
319
- if @suggest_tests
320
- concrete = CONCRETE_TEMPLATES[mutation.operator_name]
321
- return concrete.call(mutation) if concrete
322
- end
329
+ # Generate suggestions for survived mutations.
330
+ #
331
+ # @param summary [Result::Summary]
332
+ # @return [Array<Hash>] Array of { mutation:, suggestion: }
333
+ def call(summary)
334
+ summary.survived_results.map do |result|
335
+ {
336
+ mutation: result.mutation,
337
+ suggestion: suggestion_for(result.mutation)
338
+ }
339
+ end
340
+ end
323
341
 
324
- TEMPLATES.fetch(mutation.operator_name, DEFAULT_SUGGESTION)
325
- end
342
+ # Generate a suggestion for a single mutation.
343
+ #
344
+ # @param mutation [Mutation]
345
+ # @return [String]
346
+ def suggestion_for(mutation)
347
+ if @suggest_tests
348
+ concrete = CONCRETE_TEMPLATES[mutation.operator_name]
349
+ return concrete.call(mutation) if concrete
350
+ end
326
351
 
327
- class << self
328
- def parse_method_name(subject_name)
329
- subject_name.split(/[#.]/).last
330
- end
352
+ TEMPLATES.fetch(mutation.operator_name, DEFAULT_SUGGESTION)
353
+ end
331
354
 
332
- def extract_diff_lines(diff)
333
- lines = diff.split("\n")
334
- original = lines.find { |l| l.start_with?("- ") }
335
- mutated = lines.find { |l| l.start_with?("+ ") }
336
- [original&.sub(/^- /, "")&.strip, mutated&.sub(/^\+ /, "")&.strip]
337
- end
338
- end
355
+ class << self
356
+ def parse_method_name(subject_name)
357
+ subject_name.split(/[#.]/).last
358
+ end
359
+
360
+ def extract_diff_lines(diff)
361
+ lines = diff.split("\n")
362
+ original = lines.find { |l| l.start_with?("- ") }
363
+ mutated = lines.find { |l| l.start_with?("+ ") }
364
+ [original&.sub(/^- /, "")&.strip, mutated&.sub(/^\+ /, "")&.strip]
339
365
  end
340
366
  end
341
367
  end