sixth_sense 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +125 -0
- data/exe/sixth_sense +7 -0
- data/lib/sixth_sense/adapters/minitest.rb +145 -0
- data/lib/sixth_sense/adapters/rspec.rb +373 -0
- data/lib/sixth_sense/adapters/test_unit.rb +142 -0
- data/lib/sixth_sense/analysis_context.rb +35 -0
- data/lib/sixth_sense/analysis_runner.rb +141 -0
- data/lib/sixth_sense/analyzer.rb +85 -0
- data/lib/sixth_sense/analyzers/adequacy_checked_coverage.rb +39 -0
- data/lib/sixth_sense/analyzers/adequacy_coverage.rb +63 -0
- data/lib/sixth_sense/analyzers/adequacy_mutation.rb +141 -0
- data/lib/sixth_sense/analyzers/quality_assertion_density.rb +41 -0
- data/lib/sixth_sense/analyzers/quality_flakiness.rb +53 -0
- data/lib/sixth_sense/analyzers/quality_test_smells.rb +253 -0
- data/lib/sixth_sense/analyzers/redundancy_clone.rb +54 -0
- data/lib/sixth_sense/analyzers/redundancy_coverage.rb +39 -0
- data/lib/sixth_sense/analyzers/redundancy_mutation.rb +39 -0
- data/lib/sixth_sense/analyzers/redundancy_requirement.rb +70 -0
- data/lib/sixth_sense/changed_files.rb +45 -0
- data/lib/sixth_sense/cli.rb +229 -0
- data/lib/sixth_sense/config.rb +91 -0
- data/lib/sixth_sense/engines/mutant.rb +258 -0
- data/lib/sixth_sense/framework_adapter.rb +55 -0
- data/lib/sixth_sense/guardrail/baseline.rb +135 -0
- data/lib/sixth_sense/guardrail/evaluator.rb +69 -0
- data/lib/sixth_sense/model.rb +264 -0
- data/lib/sixth_sense/mutation_cache.rb +93 -0
- data/lib/sixth_sense/mutation_engine.rb +52 -0
- data/lib/sixth_sense/mutation_matrix_mutant_generator.rb +462 -0
- data/lib/sixth_sense/mutation_matrix_producer.rb +308 -0
- data/lib/sixth_sense/mutation_score_cache_writer.rb +75 -0
- data/lib/sixth_sense/rake_task.rb +16 -0
- data/lib/sixth_sense/reporters/console.rb +31 -0
- data/lib/sixth_sense/reporters/html.rb +62 -0
- data/lib/sixth_sense/reporters/json.rb +18 -0
- data/lib/sixth_sense/reporters/markdown.rb +34 -0
- data/lib/sixth_sense/reporters/sarif.rb +77 -0
- data/lib/sixth_sense/result.rb +86 -0
- data/lib/sixth_sense/runners/checked_coverage_estimator.rb +62 -0
- data/lib/sixth_sense/runners/checked_coverage_runner.rb +130 -0
- data/lib/sixth_sense/runners/checked_coverage_trace.rb +110 -0
- data/lib/sixth_sense/runners/coverage_runner.rb +220 -0
- data/lib/sixth_sense/runners/coverage_snapshot.rb +64 -0
- data/lib/sixth_sense/runners/minitest_checked_coverage_probe.rb +42 -0
- data/lib/sixth_sense/runners/minitest_coverage_probe.rb +49 -0
- data/lib/sixth_sense/runners/rspec_checked_coverage_probe.rb +26 -0
- data/lib/sixth_sense/runners/rspec_coverage_probe.rb +98 -0
- data/lib/sixth_sense/runners/test_unit_checked_coverage_probe.rb +43 -0
- data/lib/sixth_sense/runners/test_unit_coverage_probe.rb +51 -0
- data/lib/sixth_sense/scoring/aggregator.rb +117 -0
- data/lib/sixth_sense/source_location.rb +19 -0
- data/lib/sixth_sense/version.rb +5 -0
- data/lib/sixth_sense.rb +74 -0
- metadata +113 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "prism"
|
|
5
|
+
|
|
6
|
+
module SixthSense
|
|
7
|
+
MatrixMutation = Struct.new(:id, :path, :line, :operator, :original, :replacement, :status, :metadata, keyword_init: true)
|
|
8
|
+
|
|
9
|
+
class MutationMatrixMutantGenerator
|
|
10
|
+
BUILTIN_OPERATORS = %w[
|
|
11
|
+
arithmetic_operator_replacement
|
|
12
|
+
boolean_flip
|
|
13
|
+
collection_literal_empty
|
|
14
|
+
collection_literal_replacement
|
|
15
|
+
comparison_operator_replacement
|
|
16
|
+
condition_negation
|
|
17
|
+
constant_replacement
|
|
18
|
+
logical_operator_replacement
|
|
19
|
+
loop_condition_negation
|
|
20
|
+
method_call_replacement
|
|
21
|
+
nil_replacement
|
|
22
|
+
numeric_literal_decrement
|
|
23
|
+
numeric_literal_increment
|
|
24
|
+
numeric_literal_negation
|
|
25
|
+
numeric_literal_one
|
|
26
|
+
numeric_literal_zero
|
|
27
|
+
predicate_method_replacement
|
|
28
|
+
range_operator_replacement
|
|
29
|
+
regexp_literal_empty
|
|
30
|
+
return_value_nil
|
|
31
|
+
string_literal_empty
|
|
32
|
+
string_literal_replacement
|
|
33
|
+
symbol_literal_replacement
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
METHOD_REPLACEMENTS = {
|
|
37
|
+
"__send__" => %w[public_send],
|
|
38
|
+
"all?" => %w[any? none?],
|
|
39
|
+
"any?" => %w[all? empty? none?],
|
|
40
|
+
"append" => %w[prepend],
|
|
41
|
+
"assoc" => %w[rassoc],
|
|
42
|
+
"at" => %w[fetch key?],
|
|
43
|
+
"bytes" => %w[chars],
|
|
44
|
+
"capitalize!" => %w[capitalize],
|
|
45
|
+
"chars" => %w[bytes],
|
|
46
|
+
"chomp!" => %w[chomp],
|
|
47
|
+
"chop!" => %w[chop],
|
|
48
|
+
"chunk" => %w[each],
|
|
49
|
+
"chunk_while" => %w[each],
|
|
50
|
+
"collect" => %w[each],
|
|
51
|
+
"collect!" => %w[collect],
|
|
52
|
+
"collect_concat" => %w[collect],
|
|
53
|
+
"compact!" => %w[compact],
|
|
54
|
+
"ceil" => %w[floor],
|
|
55
|
+
"count" => %w[size length],
|
|
56
|
+
"delete!" => %w[delete],
|
|
57
|
+
"delete_if" => %w[reject],
|
|
58
|
+
"detect" => %w[first last],
|
|
59
|
+
"downcase" => %w[upcase],
|
|
60
|
+
"downcase!" => %w[downcase],
|
|
61
|
+
"drop" => %w[take],
|
|
62
|
+
"each_cons" => %w[each],
|
|
63
|
+
"each_key" => %w[each_value],
|
|
64
|
+
"each_slice" => %w[each],
|
|
65
|
+
"each_value" => %w[each_key],
|
|
66
|
+
"each_with_index" => %w[each],
|
|
67
|
+
"each_with_object" => %w[each],
|
|
68
|
+
"empty?" => %w[any?],
|
|
69
|
+
"encode!" => %w[encode],
|
|
70
|
+
"end_with?" => %w[start_with?],
|
|
71
|
+
"even?" => %w[odd?],
|
|
72
|
+
"fetch" => %w[key?],
|
|
73
|
+
"filter" => %w[reject],
|
|
74
|
+
"filter!" => %w[filter],
|
|
75
|
+
"filter_map" => %w[map],
|
|
76
|
+
"find" => %w[first last],
|
|
77
|
+
"first" => %w[last],
|
|
78
|
+
"flat_map" => %w[map],
|
|
79
|
+
"flatten!" => %w[flatten],
|
|
80
|
+
"floor" => %w[ceil],
|
|
81
|
+
"grep" => %w[grep_v],
|
|
82
|
+
"grep_v" => %w[grep],
|
|
83
|
+
"gsub" => %w[sub],
|
|
84
|
+
"gsub!" => %w[gsub],
|
|
85
|
+
"include?" => %w[exclude?],
|
|
86
|
+
"is_a?" => %w[instance_of?],
|
|
87
|
+
"keep_if" => %w[select],
|
|
88
|
+
"key?" => %w[value?],
|
|
89
|
+
"keys" => %w[values],
|
|
90
|
+
"kind_of?" => %w[instance_of?],
|
|
91
|
+
"last" => %w[first],
|
|
92
|
+
"lstrip!" => %w[lstrip],
|
|
93
|
+
"map" => %w[each],
|
|
94
|
+
"map!" => %w[map],
|
|
95
|
+
"match" => %w[match?],
|
|
96
|
+
"max" => %w[first last min],
|
|
97
|
+
"max_by" => %w[first last min_by],
|
|
98
|
+
"merge!" => %w[merge],
|
|
99
|
+
"method" => %w[public_method],
|
|
100
|
+
"min" => %w[first last max],
|
|
101
|
+
"min_by" => %w[first last max_by],
|
|
102
|
+
"negative?" => %w[positive?],
|
|
103
|
+
"none?" => %w[any? all?],
|
|
104
|
+
"odd?" => %w[even?],
|
|
105
|
+
"pop" => %w[shift],
|
|
106
|
+
"positive?" => %w[negative?],
|
|
107
|
+
"pred" => %w[succ],
|
|
108
|
+
"prepend" => %w[append],
|
|
109
|
+
"push" => %w[unshift],
|
|
110
|
+
"rassoc" => %w[assoc],
|
|
111
|
+
"reject" => %w[select],
|
|
112
|
+
"reject!" => %w[reject],
|
|
113
|
+
"reverse!" => %w[reverse],
|
|
114
|
+
"reverse_each" => %w[each],
|
|
115
|
+
"reverse_map" => %w[map each],
|
|
116
|
+
"reverse_merge" => %w[merge],
|
|
117
|
+
"rotate!" => %w[rotate],
|
|
118
|
+
"rstrip!" => %w[rstrip],
|
|
119
|
+
"sample" => %w[first last],
|
|
120
|
+
"scrub!" => %w[scrub],
|
|
121
|
+
"select" => %w[reject],
|
|
122
|
+
"select!" => %w[select],
|
|
123
|
+
"send" => %w[public_send __send__],
|
|
124
|
+
"shift" => %w[pop],
|
|
125
|
+
"shuffle!" => %w[shuffle],
|
|
126
|
+
"slice_after" => %w[each],
|
|
127
|
+
"slice_before" => %w[each],
|
|
128
|
+
"slice_when" => %w[each],
|
|
129
|
+
"size" => %w[length count],
|
|
130
|
+
"sort!" => %w[sort],
|
|
131
|
+
"sort_by" => %w[sort],
|
|
132
|
+
"sort_by!" => %w[sort_by],
|
|
133
|
+
"squeeze!" => %w[squeeze],
|
|
134
|
+
"start_with?" => %w[end_with?],
|
|
135
|
+
"strip" => %w[lstrip rstrip],
|
|
136
|
+
"strip!" => %w[strip],
|
|
137
|
+
"sub!" => %w[sub],
|
|
138
|
+
"succ" => %w[pred],
|
|
139
|
+
"swapcase!" => %w[swapcase],
|
|
140
|
+
"take" => %w[drop],
|
|
141
|
+
"to_a" => %w[to_ary],
|
|
142
|
+
"to_f" => %w[to_i],
|
|
143
|
+
"to_h" => %w[to_hash],
|
|
144
|
+
"to_i" => %w[to_int],
|
|
145
|
+
"to_s" => %w[to_str],
|
|
146
|
+
"tr!" => %w[tr],
|
|
147
|
+
"tr_s!" => %w[tr_s],
|
|
148
|
+
"transform_keys" => %w[transform_values],
|
|
149
|
+
"transform_keys!" => %w[transform_keys],
|
|
150
|
+
"transform_values" => %w[transform_keys],
|
|
151
|
+
"transform_values!" => %w[transform_values],
|
|
152
|
+
"unicode_normalize!" => %w[unicode_normalize],
|
|
153
|
+
"uniq!" => %w[uniq],
|
|
154
|
+
"unshift" => %w[push],
|
|
155
|
+
"upcase" => %w[downcase],
|
|
156
|
+
"upcase!" => %w[upcase],
|
|
157
|
+
"values" => %w[keys],
|
|
158
|
+
"values_at" => %w[fetch_values],
|
|
159
|
+
"zero?" => %w[nonzero?]
|
|
160
|
+
}.freeze
|
|
161
|
+
|
|
162
|
+
def initialize(config: {})
|
|
163
|
+
@config = config
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def generate(plan)
|
|
167
|
+
generated = generate_mutants(plan)
|
|
168
|
+
skipped, executable = generated.partition { |mutant| operator_excluded?(mutant.operator) }
|
|
169
|
+
selected, sampling = select_mutants(executable)
|
|
170
|
+
{
|
|
171
|
+
mutants: skipped.map { |mutant| mark_skipped(mutant) } + selected,
|
|
172
|
+
sampling: sampling.merge("excluded_count" => skipped.length)
|
|
173
|
+
}
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
def generate_mutants(plan)
|
|
179
|
+
plan.source_units.flat_map do |unit|
|
|
180
|
+
path = unit.fetch(:path) { unit["path"] }
|
|
181
|
+
next [] unless File.file?(path)
|
|
182
|
+
|
|
183
|
+
mutants_for_file(path)
|
|
184
|
+
end.flatten
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def mutants_for_file(path)
|
|
188
|
+
source = File.read(path)
|
|
189
|
+
parsed = Prism.parse(source)
|
|
190
|
+
return [] unless parsed.success?
|
|
191
|
+
|
|
192
|
+
mutations = []
|
|
193
|
+
walk(parsed.value) do |node, parent|
|
|
194
|
+
mutations.concat(node_mutations(path, node, parent))
|
|
195
|
+
end
|
|
196
|
+
mutations.compact.sort_by { |mutant| [mutant.metadata.dig("range", "start"), mutant.operator, mutant.replacement] }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def walk(node, parent = nil, &block)
|
|
200
|
+
yield node, parent
|
|
201
|
+
node.each_child_node { |child| walk(child, node, &block) } if node.respond_to?(:each_child_node)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def node_mutations(path, node, parent)
|
|
205
|
+
case node
|
|
206
|
+
when Prism::StringNode then string_mutations(path, node)
|
|
207
|
+
when Prism::IntegerNode, Prism::FloatNode then numeric_mutations(path, node)
|
|
208
|
+
when Prism::TrueNode, Prism::FalseNode then boolean_mutations(path, node)
|
|
209
|
+
when Prism::IfNode, Prism::UnlessNode then conditional_mutations(path, node)
|
|
210
|
+
when Prism::WhileNode, Prism::UntilNode then loop_mutations(path, node)
|
|
211
|
+
when Prism::AndNode, Prism::OrNode then logical_mutations(path, node)
|
|
212
|
+
when Prism::NilNode then [range_mutation(path, "nil_replacement", node.location, "false")]
|
|
213
|
+
when Prism::ReturnNode then return_mutations(path, node)
|
|
214
|
+
when Prism::DefNode then implicit_return_mutations(path, node)
|
|
215
|
+
when Prism::SymbolNode then symbol_mutations(path, node)
|
|
216
|
+
when Prism::ArrayNode, Prism::HashNode then collection_mutations(path, node)
|
|
217
|
+
when Prism::RangeNode then range_operator_mutations(path, node)
|
|
218
|
+
when Prism::RegularExpressionNode then [range_mutation(path, "regexp_literal_empty", node.location, "//")]
|
|
219
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode then constant_mutations(path, node, parent)
|
|
220
|
+
when Prism::CallNode then call_mutations(path, node)
|
|
221
|
+
else []
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def string_mutations(path, node)
|
|
226
|
+
delimiter = node.opening == "'" ? "'" : '"'
|
|
227
|
+
[
|
|
228
|
+
range_mutation(path, "string_literal_replacement", node.location, "#{delimiter}#{node.unescaped}__mutant__#{delimiter}"),
|
|
229
|
+
range_mutation(path, "string_literal_empty", node.location, "#{delimiter}#{delimiter}")
|
|
230
|
+
]
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def numeric_mutations(path, node)
|
|
234
|
+
value = numeric_value(node.slice)
|
|
235
|
+
[
|
|
236
|
+
range_mutation(path, "numeric_literal_increment", node.location, numeric_literal(value + 1)),
|
|
237
|
+
range_mutation(path, "numeric_literal_decrement", node.location, numeric_literal(value - 1)),
|
|
238
|
+
range_mutation(path, "numeric_literal_zero", node.location, "0"),
|
|
239
|
+
range_mutation(path, "numeric_literal_one", node.location, "1"),
|
|
240
|
+
range_mutation(path, "numeric_literal_negation", node.location, numeric_literal(-value))
|
|
241
|
+
]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def boolean_mutations(path, node)
|
|
245
|
+
replacement = node.is_a?(Prism::TrueNode) ? "false" : "true"
|
|
246
|
+
[range_mutation(path, "boolean_flip", node.location, replacement)]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def conditional_mutations(path, node)
|
|
250
|
+
location = node.respond_to?(:if_keyword_loc) ? node.if_keyword_loc : node.keyword_loc
|
|
251
|
+
replacement = node.is_a?(Prism::UnlessNode) || location.slice == "unless" ? "if" : "unless"
|
|
252
|
+
[range_mutation(path, "condition_negation", location, replacement)]
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def loop_mutations(path, node)
|
|
256
|
+
replacement = node.is_a?(Prism::WhileNode) ? "until" : "while"
|
|
257
|
+
[range_mutation(path, "loop_condition_negation", node.keyword_loc, replacement)]
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def logical_mutations(path, node)
|
|
261
|
+
replacement = node.operator == "&&" || node.operator == "and" ? (node.operator == "and" ? "or" : "||") : (node.operator == "or" ? "and" : "&&")
|
|
262
|
+
[range_mutation(path, "logical_operator_replacement", node.operator_loc, replacement)]
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def return_mutations(path, node)
|
|
266
|
+
return [] unless node.location.slice.match?(/\Areturn\b/)
|
|
267
|
+
|
|
268
|
+
[range_mutation(path, "return_value_nil", node.location, "return nil")]
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def implicit_return_mutations(path, node)
|
|
272
|
+
statements = node.body&.body
|
|
273
|
+
return [] if statements.nil? || statements.empty?
|
|
274
|
+
|
|
275
|
+
last_statement = statements.last
|
|
276
|
+
return [] if last_statement.is_a?(Prism::NilNode) || last_statement.is_a?(Prism::ReturnNode)
|
|
277
|
+
|
|
278
|
+
[range_mutation(path, "return_value_nil", last_statement.location, "nil")]
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def symbol_mutations(path, node)
|
|
282
|
+
return [] unless node.location.slice.start_with?(":")
|
|
283
|
+
|
|
284
|
+
[range_mutation(path, "symbol_literal_replacement", node.location, ":#{node.value}__mutant__")]
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def collection_mutations(path, node)
|
|
288
|
+
literal = node.location.slice
|
|
289
|
+
replacement = node.is_a?(Prism::ArrayNode) ? "[]" : "{}"
|
|
290
|
+
mutations = [range_mutation(path, "collection_literal_replacement", node.location, "nil")]
|
|
291
|
+
mutations << range_mutation(path, "collection_literal_empty", node.location, replacement) unless literal == replacement
|
|
292
|
+
mutations
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def range_operator_mutations(path, node)
|
|
296
|
+
replacement = node.operator == "..." ? ".." : "..."
|
|
297
|
+
[range_mutation(path, "range_operator_replacement", node.operator_loc, replacement)]
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def constant_mutations(path, node, parent)
|
|
301
|
+
return [] if parent.is_a?(Prism::ClassNode) || parent.is_a?(Prism::ModuleNode)
|
|
302
|
+
return [] if node.location.slice == "Object"
|
|
303
|
+
|
|
304
|
+
[range_mutation(path, "constant_replacement", node.location, "Object")]
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def call_mutations(path, node)
|
|
308
|
+
[
|
|
309
|
+
comparison_mutations(path, node),
|
|
310
|
+
arithmetic_mutations(path, node),
|
|
311
|
+
method_call_mutations(path, node),
|
|
312
|
+
predicate_method_mutations(path, node)
|
|
313
|
+
].flatten.compact
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def comparison_mutations(path, node)
|
|
317
|
+
replacements = {
|
|
318
|
+
"===" => ["is_a?"],
|
|
319
|
+
"=~" => ["match?"],
|
|
320
|
+
"==" => %w[!= eql? equal?],
|
|
321
|
+
"!=" => %w[== eql? equal?],
|
|
322
|
+
">=" => %w[> == eql? equal?],
|
|
323
|
+
"<=" => %w[< == eql? equal?],
|
|
324
|
+
">" => %w[== eql? equal?],
|
|
325
|
+
"<" => %w[== eql? equal?]
|
|
326
|
+
}[node.name.to_s]
|
|
327
|
+
return [] unless replacements
|
|
328
|
+
|
|
329
|
+
replacements.filter_map do |replacement|
|
|
330
|
+
call_operator_mutation(path, node, "comparison_operator_replacement", replacement)
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def arithmetic_mutations(path, node)
|
|
335
|
+
replacement = {
|
|
336
|
+
"**" => "*",
|
|
337
|
+
"+" => "-",
|
|
338
|
+
"-" => "+",
|
|
339
|
+
"*" => "/",
|
|
340
|
+
"/" => "*",
|
|
341
|
+
"%" => "/",
|
|
342
|
+
"<<" => ">>",
|
|
343
|
+
">>" => "<<",
|
|
344
|
+
"&" => %w[| ^],
|
|
345
|
+
"|" => %w[& ^],
|
|
346
|
+
"^" => %w[& |]
|
|
347
|
+
}[node.name.to_s]
|
|
348
|
+
return [] unless replacement
|
|
349
|
+
|
|
350
|
+
Array(replacement).map { |item| range_mutation(path, "arithmetic_operator_replacement", node.message_loc, item) }
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def method_call_mutations(path, node)
|
|
354
|
+
return [] unless node.receiver
|
|
355
|
+
|
|
356
|
+
replacements = METHOD_REPLACEMENTS[node.name.to_s]
|
|
357
|
+
return [] unless replacements
|
|
358
|
+
|
|
359
|
+
replacements.filter_map do |replacement|
|
|
360
|
+
range_mutation(path, "method_call_replacement", node.message_loc, replacement)
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def predicate_method_mutations(path, node)
|
|
365
|
+
return [] unless node.receiver && node.name.to_s.end_with?("?")
|
|
366
|
+
|
|
367
|
+
[
|
|
368
|
+
range_mutation(path, "predicate_method_replacement", node.location, "true"),
|
|
369
|
+
range_mutation(path, "predicate_method_replacement", node.location, "false")
|
|
370
|
+
]
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def call_operator_mutation(path, node, operator, replacement)
|
|
374
|
+
return range_mutation(path, operator, node.message_loc, replacement) if replacement.match?(/\A[^\w]+\z/)
|
|
375
|
+
|
|
376
|
+
source = method_call_source(node, replacement)
|
|
377
|
+
range_mutation(path, operator, node.location, source) if source
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def method_call_source(node, method_name)
|
|
381
|
+
receiver = node.receiver&.slice
|
|
382
|
+
return unless receiver
|
|
383
|
+
|
|
384
|
+
args = Array(node.arguments&.arguments).map(&:slice).join(", ")
|
|
385
|
+
args.empty? ? "#{receiver}.#{method_name}" : "#{receiver}.#{method_name}(#{args})"
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def numeric_value(source)
|
|
389
|
+
normalized = source.delete("_")
|
|
390
|
+
normalized.include?(".") ? normalized.to_f : normalized.to_i
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def numeric_literal(value)
|
|
394
|
+
value.is_a?(Float) && value.to_i != value ? value.to_s : value.to_i.to_s
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def range_mutation(path, operator, location, replacement)
|
|
398
|
+
return if location.nil?
|
|
399
|
+
return if location.slice == replacement
|
|
400
|
+
|
|
401
|
+
digest = Digest::SHA256.hexdigest([path, location.start_offset, location.end_offset, operator, replacement].join(":"))[0, 12]
|
|
402
|
+
MatrixMutation.new(
|
|
403
|
+
id: "m-#{digest}",
|
|
404
|
+
path: path,
|
|
405
|
+
line: location.start_line,
|
|
406
|
+
operator: operator,
|
|
407
|
+
original: location.slice,
|
|
408
|
+
replacement: replacement,
|
|
409
|
+
status: "active",
|
|
410
|
+
metadata: {
|
|
411
|
+
"rewrite" => "range",
|
|
412
|
+
"range" => {
|
|
413
|
+
"start" => location.start_offset,
|
|
414
|
+
"end" => location.end_offset
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def operator_excluded?(operator)
|
|
421
|
+
operator_excludes.include?(operator.to_s)
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def operator_excludes
|
|
425
|
+
operators = @config["operators"] || @config[:operators] || {}
|
|
426
|
+
Array(operators["exclude"] || operators[:exclude]).map(&:to_s)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def select_mutants(mutants)
|
|
430
|
+
return [mutants, sampling_metadata(mutants, mutants, sampled: false)] if mutants.length <= max_mutants
|
|
431
|
+
|
|
432
|
+
selected = []
|
|
433
|
+
grouped = mutants.group_by(&:operator).sort_by { |operator, _items| operator.to_s }
|
|
434
|
+
until selected.length >= max_mutants || grouped.empty?
|
|
435
|
+
grouped.each do |_operator, items|
|
|
436
|
+
selected << items.shift if items.any? && selected.length < max_mutants
|
|
437
|
+
end
|
|
438
|
+
grouped.reject! { |_operator, items| items.empty? }
|
|
439
|
+
end
|
|
440
|
+
[selected, sampling_metadata(mutants, selected, sampled: true)]
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def sampling_metadata(generated, selected, sampled:)
|
|
444
|
+
{
|
|
445
|
+
"sampled" => sampled,
|
|
446
|
+
"strategy" => sampled ? "operator_round_robin" : "none",
|
|
447
|
+
"generated_count" => generated.length,
|
|
448
|
+
"selected_count" => selected.length
|
|
449
|
+
}
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def max_mutants
|
|
453
|
+
[@config.fetch("max_mutants", 500).to_i, 1].max
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def mark_skipped(mutant)
|
|
457
|
+
mutant.status = "skipped"
|
|
458
|
+
mutant.metadata = mutant.metadata.merge("skip_reason" => "operator_excluded")
|
|
459
|
+
mutant
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
end
|