mutant 0.16.3 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/lib/mutant/ast/structure.rb +71 -0
- data/lib/mutant/bootstrap.rb +56 -7
- data/lib/mutant/cli/command/environment.rb +20 -2
- data/lib/mutant/cli/command/session.rb +19 -11
- data/lib/mutant/config/selection.rb +92 -0
- data/lib/mutant/config.rb +6 -0
- data/lib/mutant/context_map/loader.rb +246 -0
- data/lib/mutant/context_map.rb +109 -0
- data/lib/mutant/integration.rb +1 -1
- data/lib/mutant/matcher/method/instance.rb +26 -0
- data/lib/mutant/meta.rb +1 -1
- data/lib/mutant/mutation/runner/sink.rb +27 -1
- data/lib/mutant/mutation/runner.rb +1 -0
- data/lib/mutant/mutator/node/itblock.rb +36 -0
- data/lib/mutant/reporter/cli/printer/env.rb +6 -0
- data/lib/mutant/result/json_writer.rb +38 -6
- data/lib/mutant/result/session_files.rb +51 -0
- data/lib/mutant/result.rb +1 -1
- data/lib/mutant/selector/context_map.rb +94 -0
- data/lib/mutant/selector/expression.rb +5 -0
- data/lib/mutant/selector/null.rb +5 -0
- data/lib/mutant/selector.rb +5 -0
- data/lib/mutant/subject/method/instance.rb +22 -0
- data/lib/mutant/test.rb +2 -1
- data/lib/mutant/transform.rb +69 -0
- data/lib/mutant.rb +8 -0
- metadata +8 -2
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mutant
|
|
4
|
+
class Selector
|
|
5
|
+
# Test selector backed by a per test coverage recording
|
|
6
|
+
#
|
|
7
|
+
# Selects the tests that executed the subject's own source lines, which
|
|
8
|
+
# finds the tests that cover a subject regardless of where they live or how
|
|
9
|
+
# they are named. Subjects the recording says nothing about fall back to the
|
|
10
|
+
# expression selector: code that only runs while the suite loads (a constant
|
|
11
|
+
# body, a class level DSL call) executes under no test, and reporting its
|
|
12
|
+
# mutations alive without running a single test would be a lie.
|
|
13
|
+
class ContextMap < self
|
|
14
|
+
include Anima.new(:context_map, :fallback, :integration)
|
|
15
|
+
|
|
16
|
+
# Name of the strategy this selector implements
|
|
17
|
+
#
|
|
18
|
+
# @return [String]
|
|
19
|
+
def name = 'context_map'
|
|
20
|
+
|
|
21
|
+
# Tests for subject, likeliest killer first
|
|
22
|
+
#
|
|
23
|
+
# @param [Subject] subject
|
|
24
|
+
#
|
|
25
|
+
# @return [Enumerable<Test>]
|
|
26
|
+
def call(subject)
|
|
27
|
+
reach = context_map.reach(path: subject.source_path, lines: subject.source_lines)
|
|
28
|
+
|
|
29
|
+
covering = reach.flat_map do |location, lines|
|
|
30
|
+
index.fetch(location, EMPTY_ARRAY).map { |test| [test, lines, context_map.footprint(location)] }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
return fallback.call(subject) if covering.empty?
|
|
34
|
+
|
|
35
|
+
order(covering, subject)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Whether the recording names no test this suite runs
|
|
39
|
+
#
|
|
40
|
+
# A recording taken from another project root, or from a suite that has
|
|
41
|
+
# since been renamed away, covers everything and matches nothing. Left
|
|
42
|
+
# unsaid it looks exactly like a suite in which no test covers anything.
|
|
43
|
+
#
|
|
44
|
+
# @return [Boolean]
|
|
45
|
+
def unmatched?
|
|
46
|
+
index.each_key.none? { |location| context_map.context_ids.include?(location) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# A run against a mutation stops at the first failing test, so this order
|
|
52
|
+
# is the difference between running one test and running all of them.
|
|
53
|
+
#
|
|
54
|
+
# A test the expressions would also have picked is the subject's own unit
|
|
55
|
+
# test, and no measurement beats that prior, so those go first as a block.
|
|
56
|
+
# Within each block comes the test spending the largest share of its own
|
|
57
|
+
# footprint inside this subject, since a test that exists for this code
|
|
58
|
+
# asserts on it where a feature spec passing through may swallow the
|
|
59
|
+
# difference. Reach breaks the remaining ties, because a test that ran
|
|
60
|
+
# more of the subject's lines is likelier to have run the mutated one.
|
|
61
|
+
def order(covering, subject)
|
|
62
|
+
named, rest = covering.partition { |test, _lines, _footprint| expression_match?(subject, test) }
|
|
63
|
+
|
|
64
|
+
(by_focus(named) + by_focus(rest)).map(&:first)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The test id breaks a full tie, so the order depends on the recording's
|
|
68
|
+
# content rather than on the order a coverage tool happened to write it.
|
|
69
|
+
def by_focus(covering)
|
|
70
|
+
covering.sort_by { |test, lines, footprint| [-Rational(lines, footprint), -lines, test.id] }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The question the expression selector asks of one test, rather than the
|
|
74
|
+
# set it builds by asking it of every test in the suite.
|
|
75
|
+
def expression_match?(subject, test)
|
|
76
|
+
subject.match_expressions.any? do |match_expression|
|
|
77
|
+
test.expressions.any? { |test_expression| match_expression.prefix?(test_expression) }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Tests by the location the recording would name them under. A location
|
|
82
|
+
# holds more than one test whenever a framework generates examples from
|
|
83
|
+
# one line, so the value is a list.
|
|
84
|
+
def index
|
|
85
|
+
integration.available_tests.each_with_object({}) do |test, index|
|
|
86
|
+
location = test.location or next
|
|
87
|
+
|
|
88
|
+
(index[context_map.normalize(location)] ||= []) << test
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
memoize :index
|
|
92
|
+
end # ContextMap
|
|
93
|
+
end # Selector
|
|
94
|
+
end # Mutant
|
data/lib/mutant/selector/null.rb
CHANGED
data/lib/mutant/selector.rb
CHANGED
|
@@ -38,6 +38,28 @@ module Mutant
|
|
|
38
38
|
s(:begin, mutant, s(:send, nil, :memoize, s(:sym, name)))
|
|
39
39
|
end
|
|
40
40
|
end # Memoized
|
|
41
|
+
|
|
42
|
+
# Subject for instance methods exposed via module_function
|
|
43
|
+
#
|
|
44
|
+
# module_function copies the method onto the module's singleton
|
|
45
|
+
# class. Redefining only the instance method would leave that copy,
|
|
46
|
+
# which callers use, unmutated.
|
|
47
|
+
class ModuleFunction < self
|
|
48
|
+
|
|
49
|
+
# Prepare subject for mutation insertion
|
|
50
|
+
#
|
|
51
|
+
# @return [self]
|
|
52
|
+
def prepare
|
|
53
|
+
scope.raw.singleton_class.undef_method(name)
|
|
54
|
+
|
|
55
|
+
super
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Copy the inserted method onto the singleton class again
|
|
59
|
+
#
|
|
60
|
+
# @return [self]
|
|
61
|
+
def post_insert = tap { scope.raw.__send__(:module_function, name) }
|
|
62
|
+
end # ModuleFunction
|
|
41
63
|
end # Instance
|
|
42
64
|
end # Method
|
|
43
65
|
end # Subject
|
data/lib/mutant/test.rb
CHANGED
data/lib/mutant/transform.rb
CHANGED
|
@@ -295,6 +295,75 @@ module Mutant
|
|
|
295
295
|
end
|
|
296
296
|
end # Key
|
|
297
297
|
|
|
298
|
+
# Transform reducing a hash to the keys it names
|
|
299
|
+
#
|
|
300
|
+
# A document another tool writes carries keys mutant has no use for,
|
|
301
|
+
# and gains more between versions. Slicing ahead of a Hash transform
|
|
302
|
+
# lets that transform stay strict about the keys it does know.
|
|
303
|
+
class Slice < Transform
|
|
304
|
+
include Anima.new(:keys)
|
|
305
|
+
|
|
306
|
+
# Apply transformation to input
|
|
307
|
+
#
|
|
308
|
+
# @param [Object] input
|
|
309
|
+
#
|
|
310
|
+
# @return [Either<Error, Hash>]
|
|
311
|
+
def call(input)
|
|
312
|
+
PRIMITIVE
|
|
313
|
+
.call(input)
|
|
314
|
+
.lmap(&method(:lift_error))
|
|
315
|
+
.fmap { |hash| hash.slice(*keys) }
|
|
316
|
+
end
|
|
317
|
+
end # Slice
|
|
318
|
+
|
|
319
|
+
# Transform a hash keyed by data via mapping its pairs over key and
|
|
320
|
+
# value transforms
|
|
321
|
+
#
|
|
322
|
+
# Where Hash names the keys it accepts, this accepts every key the key
|
|
323
|
+
# transform does.
|
|
324
|
+
class Map < Transform
|
|
325
|
+
include Anima.new(:key, :value)
|
|
326
|
+
|
|
327
|
+
DUPLICATE_MESSAGE = 'Key transform maps distinct keys onto one'
|
|
328
|
+
|
|
329
|
+
private_constant(*constants(false))
|
|
330
|
+
|
|
331
|
+
# Apply transformation to input
|
|
332
|
+
#
|
|
333
|
+
# @param [Object] input
|
|
334
|
+
#
|
|
335
|
+
# @return [Either<Error, Hash>]
|
|
336
|
+
def call(input)
|
|
337
|
+
PRIMITIVE
|
|
338
|
+
.call(input)
|
|
339
|
+
.lmap(&method(:lift_error))
|
|
340
|
+
.bind(&method(:run))
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
private
|
|
344
|
+
|
|
345
|
+
# rubocop:disable Metrics/MethodLength
|
|
346
|
+
def run(input)
|
|
347
|
+
output = input.to_h do |key_input, value_input|
|
|
348
|
+
[
|
|
349
|
+
coerce(key, key_input, key_input).from_right { |error| return failure(error(cause: error, input:)) },
|
|
350
|
+
coerce(value, key_input, value_input).from_right { |error| return failure(error(cause: error, input:)) }
|
|
351
|
+
]
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
if output.size.equal?(input.size)
|
|
355
|
+
success(output)
|
|
356
|
+
else
|
|
357
|
+
failure(error(input:, message: DUPLICATE_MESSAGE))
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
# rubocop:enable Metrics/MethodLength
|
|
361
|
+
|
|
362
|
+
def coerce(transform, key, input)
|
|
363
|
+
Key.new(value: key, transform:).call(input)
|
|
364
|
+
end
|
|
365
|
+
end # Map
|
|
366
|
+
|
|
298
367
|
# Apply transformation to input
|
|
299
368
|
#
|
|
300
369
|
# @param [Object] input
|
data/lib/mutant.rb
CHANGED
|
@@ -42,6 +42,7 @@ module Mutant
|
|
|
42
42
|
stringio
|
|
43
43
|
unparser
|
|
44
44
|
yaml
|
|
45
|
+
zlib
|
|
45
46
|
].each { |name| require(name) }
|
|
46
47
|
end
|
|
47
48
|
|
|
@@ -187,6 +188,7 @@ module Mutant
|
|
|
187
188
|
require 'mutant/mutator/node/procarg_zero'
|
|
188
189
|
require 'mutant/mutator/node/kwargs'
|
|
189
190
|
require 'mutant/mutator/node/numblock'
|
|
191
|
+
require 'mutant/mutator/node/itblock' if RUBY_VERSION > '3.4.0'
|
|
190
192
|
require 'mutant/mutator/node/const_pattern'
|
|
191
193
|
require 'mutant/mutator/regexp'
|
|
192
194
|
require 'mutant/loader'
|
|
@@ -225,13 +227,17 @@ module Mutant
|
|
|
225
227
|
require 'mutant/timer'
|
|
226
228
|
require 'mutant/integration'
|
|
227
229
|
require 'mutant/integration/null'
|
|
230
|
+
require 'mutant/context_map'
|
|
231
|
+
require 'mutant/context_map/loader'
|
|
228
232
|
require 'mutant/selector'
|
|
233
|
+
require 'mutant/selector/context_map'
|
|
229
234
|
require 'mutant/selector/expression'
|
|
230
235
|
require 'mutant/selector/null'
|
|
231
236
|
require 'mutant/world'
|
|
232
237
|
require 'mutant/hooks'
|
|
233
238
|
require 'mutant/config'
|
|
234
239
|
require 'mutant/config/coverage_criteria'
|
|
240
|
+
require 'mutant/config/selection'
|
|
235
241
|
require 'mutant/cli'
|
|
236
242
|
require 'mutant/cli/command'
|
|
237
243
|
require 'mutant/cli/command/environment'
|
|
@@ -245,6 +251,7 @@ module Mutant
|
|
|
245
251
|
require 'mutant/mutation/runner/sink'
|
|
246
252
|
require 'mutant/result'
|
|
247
253
|
require 'mutant/result/session'
|
|
254
|
+
require 'mutant/result/session_files'
|
|
248
255
|
require 'mutant/result/json_writer'
|
|
249
256
|
require 'mutant/reporter'
|
|
250
257
|
require 'mutant/reporter/null'
|
|
@@ -378,6 +385,7 @@ module Mutant
|
|
|
378
385
|
mutation: Mutation::Config::EMPTY,
|
|
379
386
|
reporter: Reporter::CLI.build(WORLD.stdout),
|
|
380
387
|
requires: EMPTY_ARRAY,
|
|
388
|
+
selection: Config::Selection::DEFAULT,
|
|
381
389
|
usage: Usage::Unknown.new
|
|
382
390
|
)
|
|
383
391
|
end # Config
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mutant
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.17.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Markus Schirp
|
|
@@ -235,7 +235,10 @@ files:
|
|
|
235
235
|
- lib/mutant/cli/command/util.rb
|
|
236
236
|
- lib/mutant/config.rb
|
|
237
237
|
- lib/mutant/config/coverage_criteria.rb
|
|
238
|
+
- lib/mutant/config/selection.rb
|
|
238
239
|
- lib/mutant/context.rb
|
|
240
|
+
- lib/mutant/context_map.rb
|
|
241
|
+
- lib/mutant/context_map/loader.rb
|
|
239
242
|
- lib/mutant/env.rb
|
|
240
243
|
- lib/mutant/expression.rb
|
|
241
244
|
- lib/mutant/expression/descendants.rb
|
|
@@ -301,6 +304,7 @@ files:
|
|
|
301
304
|
- lib/mutant/mutator/node/if.rb
|
|
302
305
|
- lib/mutant/mutator/node/in_pattern.rb
|
|
303
306
|
- lib/mutant/mutator/node/index.rb
|
|
307
|
+
- lib/mutant/mutator/node/itblock.rb
|
|
304
308
|
- lib/mutant/mutator/node/kwargs.rb
|
|
305
309
|
- lib/mutant/mutator/node/kwbegin.rb
|
|
306
310
|
- lib/mutant/mutator/node/literal.rb
|
|
@@ -386,11 +390,13 @@ files:
|
|
|
386
390
|
- lib/mutant/result/json_writer.rb
|
|
387
391
|
- lib/mutant/result/process_status.rb
|
|
388
392
|
- lib/mutant/result/session.rb
|
|
393
|
+
- lib/mutant/result/session_files.rb
|
|
389
394
|
- lib/mutant/result/test.rb
|
|
390
395
|
- lib/mutant/scope.rb
|
|
391
396
|
- lib/mutant/segment.rb
|
|
392
397
|
- lib/mutant/segment/recorder.rb
|
|
393
398
|
- lib/mutant/selector.rb
|
|
399
|
+
- lib/mutant/selector/context_map.rb
|
|
394
400
|
- lib/mutant/selector/expression.rb
|
|
395
401
|
- lib/mutant/selector/null.rb
|
|
396
402
|
- lib/mutant/subject.rb
|
|
@@ -431,7 +437,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
431
437
|
- !ruby/object:Gem::Version
|
|
432
438
|
version: '0'
|
|
433
439
|
requirements: []
|
|
434
|
-
rubygems_version: 4.0.
|
|
440
|
+
rubygems_version: 4.0.20
|
|
435
441
|
specification_version: 4
|
|
436
442
|
summary: ''
|
|
437
443
|
test_files: []
|