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.
@@ -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
@@ -6,6 +6,11 @@ module Mutant
6
6
  class Expression < self
7
7
  include Anima.new(:integration)
8
8
 
9
+ # Name of the strategy this selector implements
10
+ #
11
+ # @return [String]
12
+ def name = 'expression'
13
+
9
14
  # Tests for subject
10
15
  #
11
16
  # @param [Subject] subject
@@ -6,6 +6,11 @@ module Mutant
6
6
  class Null < self
7
7
  include Equalizer.new
8
8
 
9
+ # Name of the strategy this selector implements
10
+ #
11
+ # @return [String]
12
+ def name = 'null'
13
+
9
14
  # Tests for subject
10
15
  #
11
16
  # @param [Subject] subject
@@ -12,5 +12,10 @@ module Mutant
12
12
  # @return [Enumerable<Test>]
13
13
  abstract_method :call
14
14
 
15
+ # Name of the strategy this selector implements
16
+ #
17
+ # @return [String]
18
+ abstract_method :name
19
+
15
20
  end # Selector
16
21
  end # Mutant
@@ -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
@@ -5,7 +5,8 @@ module Mutant
5
5
  class Test
6
6
  include Anima.new(
7
7
  :expressions,
8
- :id
8
+ :id,
9
+ :location
9
10
  )
10
11
 
11
12
  # Identification string
@@ -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.16.3
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.6
440
+ rubygems_version: 4.0.20
435
441
  specification_version: 4
436
442
  summary: ''
437
443
  test_files: []