evilution 0.13.0 → 0.14.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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/.migration-hint-ts +1 -1
  3. data/.beads/issues.jsonl +8 -8
  4. data/CHANGELOG.md +17 -0
  5. data/lib/evilution/ast/parser.rb +69 -68
  6. data/lib/evilution/ast/source_surgeon.rb +7 -9
  7. data/lib/evilution/ast.rb +4 -0
  8. data/lib/evilution/baseline.rb +73 -75
  9. data/lib/evilution/cache.rb +75 -77
  10. data/lib/evilution/cli.rb +408 -173
  11. data/lib/evilution/config.rb +141 -136
  12. data/lib/evilution/equivalent/detector.rb +25 -27
  13. data/lib/evilution/equivalent/heuristic/alias_swap.rb +29 -33
  14. data/lib/evilution/equivalent/heuristic/dead_code.rb +41 -45
  15. data/lib/evilution/equivalent/heuristic/method_body_nil.rb +11 -15
  16. data/lib/evilution/equivalent/heuristic/noop_source.rb +5 -9
  17. data/lib/evilution/equivalent/heuristic.rb +6 -0
  18. data/lib/evilution/equivalent.rb +4 -0
  19. data/lib/evilution/git/changed_files.rb +35 -37
  20. data/lib/evilution/git.rb +4 -0
  21. data/lib/evilution/integration/base.rb +5 -7
  22. data/lib/evilution/integration/rspec.rb +114 -116
  23. data/lib/evilution/integration.rb +4 -0
  24. data/lib/evilution/isolation/fork.rb +98 -100
  25. data/lib/evilution/isolation/in_process.rb +59 -61
  26. data/lib/evilution/isolation.rb +4 -0
  27. data/lib/evilution/mcp/mutate_tool.rb +172 -143
  28. data/lib/evilution/mcp/server.rb +12 -11
  29. data/lib/evilution/mcp/session_diff_tool.rb +89 -0
  30. data/lib/evilution/mcp/session_list_tool.rb +46 -0
  31. data/lib/evilution/mcp/session_show_tool.rb +53 -0
  32. data/lib/evilution/mcp.rb +4 -0
  33. data/lib/evilution/memory/leak_check.rb +80 -84
  34. data/lib/evilution/memory.rb +34 -36
  35. data/lib/evilution/mutation.rb +40 -42
  36. data/lib/evilution/mutator/base.rb +46 -48
  37. data/lib/evilution/mutator/operator/argument_nil_substitution.rb +32 -36
  38. data/lib/evilution/mutator/operator/argument_removal.rb +32 -36
  39. data/lib/evilution/mutator/operator/arithmetic_replacement.rb +26 -30
  40. data/lib/evilution/mutator/operator/array_literal.rb +18 -22
  41. data/lib/evilution/mutator/operator/block_removal.rb +16 -20
  42. data/lib/evilution/mutator/operator/boolean_literal_replacement.rb +38 -42
  43. data/lib/evilution/mutator/operator/boolean_operator_replacement.rb +41 -45
  44. data/lib/evilution/mutator/operator/collection_replacement.rb +32 -36
  45. data/lib/evilution/mutator/operator/comparison_replacement.rb +24 -28
  46. data/lib/evilution/mutator/operator/compound_assignment.rb +114 -118
  47. data/lib/evilution/mutator/operator/conditional_branch.rb +25 -29
  48. data/lib/evilution/mutator/operator/conditional_flip.rb +26 -30
  49. data/lib/evilution/mutator/operator/conditional_negation.rb +25 -29
  50. data/lib/evilution/mutator/operator/float_literal.rb +22 -26
  51. data/lib/evilution/mutator/operator/hash_literal.rb +18 -22
  52. data/lib/evilution/mutator/operator/integer_literal.rb +2 -0
  53. data/lib/evilution/mutator/operator/method_body_replacement.rb +12 -16
  54. data/lib/evilution/mutator/operator/method_call_removal.rb +12 -16
  55. data/lib/evilution/mutator/operator/negation_insertion.rb +12 -16
  56. data/lib/evilution/mutator/operator/nil_replacement.rb +13 -17
  57. data/lib/evilution/mutator/operator/range_replacement.rb +12 -16
  58. data/lib/evilution/mutator/operator/receiver_replacement.rb +16 -20
  59. data/lib/evilution/mutator/operator/regexp_mutation.rb +15 -19
  60. data/lib/evilution/mutator/operator/return_value_removal.rb +12 -16
  61. data/lib/evilution/mutator/operator/send_mutation.rb +36 -40
  62. data/lib/evilution/mutator/operator/statement_deletion.rb +13 -17
  63. data/lib/evilution/mutator/operator/string_literal.rb +18 -22
  64. data/lib/evilution/mutator/operator/symbol_literal.rb +17 -21
  65. data/lib/evilution/mutator/operator.rb +6 -0
  66. data/lib/evilution/mutator/registry.rb +54 -56
  67. data/lib/evilution/mutator.rb +4 -0
  68. data/lib/evilution/parallel/pool.rb +56 -58
  69. data/lib/evilution/parallel.rb +4 -0
  70. data/lib/evilution/reporter/cli.rb +99 -101
  71. data/lib/evilution/reporter/html.rb +242 -244
  72. data/lib/evilution/reporter/json.rb +57 -59
  73. data/lib/evilution/reporter/suggestion.rb +326 -328
  74. data/lib/evilution/reporter.rb +4 -0
  75. data/lib/evilution/result/mutation_result.rb +43 -46
  76. data/lib/evilution/result/summary.rb +80 -81
  77. data/lib/evilution/result.rb +4 -0
  78. data/lib/evilution/runner.rb +334 -323
  79. data/lib/evilution/session/store.rb +147 -0
  80. data/lib/evilution/session.rb +4 -0
  81. data/lib/evilution/spec_resolver.rb +49 -47
  82. data/lib/evilution/subject.rb +14 -16
  83. data/lib/evilution/version.rb +1 -1
  84. data/lib/evilution.rb +13 -0
  85. metadata +19 -2
@@ -1,341 +1,339 @@
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
+ }.freeze
294
29
 
295
- DEFAULT_SUGGESTION = "Add a more specific test that detects this mutation"
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
296
294
 
297
- def initialize(suggest_tests: false)
298
- @suggest_tests = suggest_tests
299
- end
295
+ DEFAULT_SUGGESTION = "Add a more specific test that detects this mutation"
300
296
 
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
297
+ def initialize(suggest_tests: false)
298
+ @suggest_tests = suggest_tests
299
+ end
313
300
 
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
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
323
313
 
324
- TEMPLATES.fetch(mutation.operator_name, DEFAULT_SUGGESTION)
325
- end
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
326
323
 
327
- class << self
328
- def parse_method_name(subject_name)
329
- subject_name.split(/[#.]/).last
330
- end
324
+ TEMPLATES.fetch(mutation.operator_name, DEFAULT_SUGGESTION)
325
+ end
331
326
 
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
327
+ class << self
328
+ def parse_method_name(subject_name)
329
+ subject_name.split(/[#.]/).last
330
+ end
331
+
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]
339
337
  end
340
338
  end
341
339
  end