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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f7a495dc61f610e15c0b43ee64726016fa2feb694ce948e9554478f15c60c03
4
- data.tar.gz: 11c45dd1db15519c9647ac2b13a8c794ddacb3a4a396e90ea2c6accfb6e71dd0
3
+ metadata.gz: fd77b62cd208529f1fd030e87ffbce9c4df69cf18c097f7895ddd76204f8577d
4
+ data.tar.gz: f4ce398e717e5a8d34aa2ed57eafea5358cdb7af02fb8dc1dbcca2b862f754cc
5
5
  SHA512:
6
- metadata.gz: 8c52e9fa61e9b8ad827cd02f37469654201115c06251330a7eb695a9909b79d355fa9c9d555002d96650589c36d71046436462736754e7a33438f2b03c11bf37
7
- data.tar.gz: 7a1a190d3e17f1bfc3bee1584023b6f7756b97d2b79ffa6f42a93ce2b70c0d323e29b0bd7bd4010204d3bbac3a26a82deaa04163c4fc83044d1cb799ecd5d4a8
6
+ metadata.gz: c6dbb4139015cfe33c91b7c802004aca5d9015299d517552c56bcaa4cc81fa40b5a5f8e39bd17319ff0876da90b2cc7164566c0c65221f6dd0de1758fe767829
7
+ data.tar.gz: caa47a0c66816fe06b76bf0da184d9b91404abceb93eec83d374c42f1d2c3c51ae578e57b98f00b9fb431ffcc770cb864aabfe203bdd9400b30a77fdf8acade3
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.16.3
1
+ 0.17.1
@@ -177,6 +177,11 @@ module Mutant
177
177
  fixed: EMPTY_ARRAY,
178
178
  variable: Node::Variable::Descendants.new(name: :members, range: 0..)
179
179
  ),
180
+ Node.new(
181
+ type: :array_pattern_with_tail,
182
+ fixed: EMPTY_ARRAY,
183
+ variable: Node::Variable::Descendants.new(name: :members, range: 0..)
184
+ ),
180
185
  Node.new(
181
186
  type: :back_ref,
182
187
  fixed: Node.fixed([[Node::Fixed::Attribute, :name]]),
@@ -381,6 +386,11 @@ module Mutant
381
386
  fixed: EMPTY_ARRAY,
382
387
  variable: nil
383
388
  ),
389
+ Node.new(
390
+ type: :find_pattern,
391
+ fixed: EMPTY_ARRAY,
392
+ variable: Node::Variable::Descendants.new(name: :members, range: 0..)
393
+ ),
384
394
  Node.new(
385
395
  type: :float,
386
396
  fixed: Node.fixed([[Node::Fixed::Attribute, :value]]),
@@ -442,6 +452,11 @@ module Mutant
442
452
  fixed: EMPTY_ARRAY,
443
453
  variable: Node::Variable::Descendants.new(name: :members, range: 0..)
444
454
  ),
455
+ Node.new(
456
+ type: :hash_pattern,
457
+ fixed: EMPTY_ARRAY,
458
+ variable: Node::Variable::Descendants.new(name: :members, range: 0..)
459
+ ),
445
460
  Node.new(
446
461
  type: :index,
447
462
  fixed: Node.fixed([[Node::Fixed::Descendant, :receiver]]),
@@ -505,6 +520,17 @@ module Mutant
505
520
  ),
506
521
  variable: nil
507
522
  ),
523
+ Node.new(
524
+ type: :itblock,
525
+ fixed: Node.fixed(
526
+ [
527
+ [Node::Fixed::Descendant, :receiver],
528
+ [Node::Fixed::Attribute, :parameter],
529
+ [Node::Fixed::Descendant, :body]
530
+ ]
531
+ ),
532
+ variable: nil
533
+ ),
508
534
  Node.new(
509
535
  type: :ivar,
510
536
  fixed: Node.fixed([[Node::Fixed::Attribute, :name]]),
@@ -535,6 +561,11 @@ module Mutant
535
561
  fixed: EMPTY_ARRAY,
536
562
  variable: Node::Variable::Descendants.new(name: :members, range: 0..)
537
563
  ),
564
+ Node.new(
565
+ type: :kwnilarg,
566
+ fixed: EMPTY_ARRAY,
567
+ variable: nil
568
+ ),
538
569
  Node.new(
539
570
  type: :kwoptarg,
540
571
  fixed: Node.fixed(
@@ -575,11 +606,36 @@ module Mutant
575
606
  ),
576
607
  variable: nil
577
608
  ),
609
+ Node.new(
610
+ type: :match_alt,
611
+ fixed: Node.fixed(
612
+ [
613
+ [Node::Fixed::Descendant, :left],
614
+ [Node::Fixed::Descendant, :right]
615
+ ]
616
+ ),
617
+ variable: nil
618
+ ),
619
+ Node.new(
620
+ type: :match_as,
621
+ fixed: Node.fixed(
622
+ [
623
+ [Node::Fixed::Descendant, :pattern],
624
+ [Node::Fixed::Descendant, :target]
625
+ ]
626
+ ),
627
+ variable: nil
628
+ ),
578
629
  Node.new(
579
630
  type: :match_current_line,
580
631
  fixed: Node.fixed([[Node::Fixed::Descendant, :pattern]]),
581
632
  variable: nil
582
633
  ),
634
+ Node.new(
635
+ type: :match_nil_pattern,
636
+ fixed: EMPTY_ARRAY,
637
+ variable: nil
638
+ ),
583
639
  Node.new(
584
640
  type: :match_pattern,
585
641
  fixed: Node.fixed(
@@ -590,6 +646,16 @@ module Mutant
590
646
  ),
591
647
  variable: nil
592
648
  ),
649
+ Node.new(
650
+ type: :match_pattern_p,
651
+ fixed: Node.fixed(
652
+ [
653
+ [Node::Fixed::Descendant, :target],
654
+ [Node::Fixed::Descendant, :pattern]
655
+ ]
656
+ ),
657
+ variable: nil
658
+ ),
593
659
  Node.new(
594
660
  type: :match_rest,
595
661
  fixed: Node.fixed([[Node::Fixed::Descendant, :value]]),
@@ -794,6 +860,11 @@ module Mutant
794
860
  ),
795
861
  variable: nil
796
862
  ),
863
+ Node.new(
864
+ type: :pin,
865
+ fixed: Node.fixed([[Node::Fixed::Descendant, :value]]),
866
+ variable: nil
867
+ ),
797
868
  Node.new(
798
869
  type: :postexe,
799
870
  fixed: Node.fixed([[Node::Fixed::Descendant, :body]]),
@@ -12,6 +12,15 @@ module Mutant
12
12
  module Bootstrap
13
13
  include Adamantium, Anima.new(:config, :parser, :world)
14
14
 
15
+ UNMATCHED_CONTEXT_MAP = <<~'MESSAGE'
16
+ The coverage recording names no test that this suite runs, so it cannot
17
+ say which tests cover a subject.
18
+
19
+ Record it again from the project root, with the suite mutant runs. A suite
20
+ that starts simplecov unconditionally rewrites the recording every time
21
+ mutant runs it, so start simplecov only when measuring coverage.
22
+ MESSAGE
23
+
15
24
  SEMANTICS_MESSAGE_FORMAT =
16
25
  "%<message>s. Fix your lib to follow normal ruby semantics!\n" \
17
26
  '{Module,Class}#name should return resolvable constant name as String or nil'
@@ -77,18 +86,58 @@ module Mutant
77
86
  env.record(__method__) do
78
87
  hooks = env.hooks
79
88
  hooks.run(:setup_integration_pre)
80
- Integration.setup(env).fmap do |integration|
81
- env.with(
82
- integration:,
83
- mutations:,
84
- selector: Selector::Expression.new(integration:),
85
- subjects: selected_subjects
86
- ).tap { hooks.run(:setup_integration_post) }
89
+ Integration.setup(env).bind do |integration|
90
+ setup_selector(env:, integration:).fmap do |selector|
91
+ env.with(
92
+ integration:,
93
+ mutations:,
94
+ selector:,
95
+ subjects: selected_subjects
96
+ ).tap { hooks.run(:setup_integration_post) }
97
+ end
87
98
  end
88
99
  end
89
100
  end
90
101
  private_class_method :setup_integration
91
102
 
103
+ # The selector the configured strategy asks for
104
+ #
105
+ # The default is to use the coverage recording whenever there is a usable
106
+ # one and the expressions otherwise, so a project that records per test
107
+ # coverage gets the better selection without asking for it. Naming
108
+ # `context_map` explicitly turns every reason the recording cannot be used
109
+ # into a failed run, rather than a silent downgrade to a selection the user
110
+ # did not ask for.
111
+ #
112
+ # @return [Either<String, Selector>]
113
+ def self.setup_selector(env:, integration:)
114
+ selection = env.config.selection
115
+ fallback = Selector::Expression.new(integration:)
116
+
117
+ return Either::Right.new(fallback) if selection.expression?
118
+
119
+ result = env.record(__method__) do
120
+ ContextMap::Loader
121
+ .call(path: env.world.pathname.new(selection.effective_path), world: env.world)
122
+ .bind { |context_map| context_map_selector(context_map:, fallback:, integration:) }
123
+ end
124
+
125
+ selection.context_map? ? result : Either::Right.new(result.from_right { fallback })
126
+ end
127
+ private_class_method :setup_selector
128
+
129
+ # A recording that names no test of this suite was taken somewhere else, and
130
+ # answering selection from it would report every mutation alive without ever
131
+ # running a test that could have killed it.
132
+ #
133
+ # @return [Either<String, Selector>]
134
+ def self.context_map_selector(context_map:, fallback:, integration:)
135
+ selector = Selector::ContextMap.new(context_map:, fallback:, integration:)
136
+
137
+ selector.unmatched? ? Either::Left.new(UNMATCHED_CONTEXT_MAP) : Either::Right.new(selector)
138
+ end
139
+ private_class_method :context_map_selector
140
+
92
141
  # rubocop:enable Metrics/MethodLength
93
142
  def self.load_hooks(env)
94
143
  env.record(__method__) do
@@ -5,8 +5,9 @@ module Mutant
5
5
  class Command
6
6
  # rubocop:disable Metrics/ClassLength
7
7
  class Environment < self
8
- NAME = 'environment'
9
- SHORT_DESCRIPTION = 'Environment subcommands'
8
+ NAME = 'environment'
9
+ SHORT_DESCRIPTION = 'Environment subcommands'
10
+ SELECTION_DESCRIPTION = 'Select tests via STRATEGY: auto|expression|context_map'
10
11
 
11
12
  OPTIONS =
12
13
  %i[
@@ -14,6 +15,7 @@ module Mutant
14
15
  add_runner_options
15
16
  add_integration_options
16
17
  add_matcher_options
18
+ add_selection_options
17
19
  add_reporter_options
18
20
  add_usage_options
19
21
  ].freeze
@@ -116,6 +118,22 @@ module Mutant
116
118
  end
117
119
  end
118
120
 
121
+ def add_selection_options(parser)
122
+ parser.separator('Selection:')
123
+
124
+ parser.on('--selection STRATEGY', Config::Selection::STRATEGIES, SELECTION_DESCRIPTION) do |strategy|
125
+ selection(strategy:)
126
+ end
127
+
128
+ parser.on('--selection-path PATH', 'Read the coverage report from PATH') do |path|
129
+ selection(path:)
130
+ end
131
+ end
132
+
133
+ def selection(**attributes)
134
+ set(selection: @config.selection.with(attributes))
135
+ end
136
+
119
137
  def add_runner_options(parser)
120
138
  parser.separator('Runner:')
121
139
 
@@ -7,23 +7,29 @@ module Mutant
7
7
  NAME = 'session'
8
8
  SHORT_DESCRIPTION = 'Session history subcommands'
9
9
 
10
- RESULTS_DIR = '.mutant/results'
10
+ RESULTS_DIR = Result::SessionFiles::DIRECTORY
11
11
 
12
- private
12
+ GZIP_MAGIC = [0x1F, 0x8B].pack('C2')
13
13
 
14
- def session_files
15
- dir = world.pathname.new(RESULTS_DIR)
14
+ private_constant(:GZIP_MAGIC)
16
15
 
17
- return [] unless dir.directory?
16
+ private
18
17
 
19
- dir.glob('*.json')
18
+ def session_files
19
+ Result::SessionFiles.new(world:).paths
20
20
  end
21
21
 
22
22
  def load_session_file(path)
23
- world.parse_json(path.read)
23
+ world.parse_json(read_session_file(path))
24
24
  .bind(&Result::Session::CODEC.load_transform.public_method(:call))
25
25
  end
26
26
 
27
+ def read_session_file(path)
28
+ content = path.binread
29
+
30
+ content.start_with?(GZIP_MAGIC) ? Zlib.gunzip(content) : content
31
+ end
32
+
27
33
  # Shared base for commands that operate on a session
28
34
  class SessionCommand < self
29
35
  OPTIONS = %i[add_session_id_option].freeze
@@ -68,7 +74,9 @@ module Mutant
68
74
 
69
75
  def resolve_session_path
70
76
  if @session_id
71
- world.pathname.new("#{RESULTS_DIR}/#{@session_id}.json")
77
+ compressed = world.pathname.new("#{RESULTS_DIR}/#{@session_id}.json.gz")
78
+
79
+ compressed.file? ? compressed : world.pathname.new("#{RESULTS_DIR}/#{@session_id}.json")
72
80
  else
73
81
  session_files.last
74
82
  end
@@ -131,9 +139,9 @@ module Mutant
131
139
  end
132
140
 
133
141
  def colorize_unsupported(path)
134
- session_id = path.basename('.json')
142
+ session_id = path.basename.to_s.delete_suffix('.gz').delete_suffix('.json')
135
143
 
136
- Unparser::Color::RED.format(INCOMPATIBLE.ljust(54)) + session_id.to_s
144
+ Unparser::Color::RED.format(INCOMPATIBLE.ljust(54)) + session_id
137
145
  end
138
146
  end # List
139
147
 
@@ -230,7 +238,7 @@ module Mutant
230
238
  SUBCOMMANDS = [].freeze
231
239
  OPTIONS = %i[add_gc_options].freeze
232
240
 
233
- DEFAULT_KEEP = 100
241
+ DEFAULT_KEEP = Result::SessionFiles::KEEP
234
242
 
235
243
  def initialize(*)
236
244
  super
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ class Config
5
+ # Configuration of how tests get selected for a subject
6
+ class Selection
7
+ include Adamantium, Anima.new(:path, :strategy)
8
+
9
+ # Use the coverage recording when there is one, and the expressions
10
+ # otherwise. Mutant's default.
11
+ AUTO = 'auto'
12
+
13
+ # Select tests whose expressions match the subject's
14
+ EXPRESSION = 'expression'
15
+
16
+ # Select tests a coverage recording says executed the subject's lines
17
+ CONTEXT_MAP = 'context_map'
18
+
19
+ STRATEGIES = [AUTO, EXPRESSION, CONTEXT_MAP].freeze
20
+
21
+ # Directory coverage tools write their report into
22
+ DEFAULT_PATH = 'coverage'
23
+
24
+ UNKNOWN_STRATEGY = 'Unknown test selection strategy %s, expected one of: %s'
25
+
26
+ private_constant(:UNKNOWN_STRATEGY)
27
+
28
+ # Both attributes stay nil until set, so a value from mutant.yml is not
29
+ # overwritten by a default coming from the CLI end of the merge.
30
+ DEFAULT = new(path: nil, strategy: nil)
31
+
32
+ TRANSFORM =
33
+ Transform::Sequence.new(
34
+ steps: [
35
+ Transform::Hash.new(
36
+ optional: [
37
+ Transform::Hash::Key.new(
38
+ transform: Transform::STRING,
39
+ value: 'path'
40
+ ),
41
+ Transform::Hash::Key.new(
42
+ transform: Transform::STRING,
43
+ value: 'strategy'
44
+ )
45
+ ],
46
+ required: []
47
+ ),
48
+ Transform::Hash::Symbolize.new,
49
+ Transform::Block.capture(:selection) { |value| new(**DEFAULT.to_h, **value).validate }
50
+ ]
51
+ )
52
+
53
+ # Whether the recording is the only acceptable source of tests
54
+ #
55
+ # @return [Boolean]
56
+ def context_map? = strategy.eql?(CONTEXT_MAP)
57
+
58
+ # Whether the expressions are the only acceptable source of tests
59
+ #
60
+ # @return [Boolean]
61
+ def expression? = strategy.eql?(EXPRESSION)
62
+
63
+ # The configured recording location, or the conventional one
64
+ #
65
+ # @return [String]
66
+ def effective_path = path || DEFAULT_PATH
67
+
68
+ # Reject a strategy mutant does not implement
69
+ #
70
+ # @return [Either<String, Selection>]
71
+ def validate
72
+ return Either::Right.new(self) if strategy.nil? || STRATEGIES.include?(strategy)
73
+
74
+ Either::Left.new(UNKNOWN_STRATEGY % [strategy.inspect, STRATEGIES.join(', ')])
75
+ end
76
+
77
+ # Merge with other selection config
78
+ #
79
+ # Values from the other instance have precedence.
80
+ #
81
+ # @param [Selection] other
82
+ #
83
+ # @return [Selection]
84
+ def merge(other)
85
+ self.class.new(
86
+ path: other.path || path,
87
+ strategy: other.strategy || strategy
88
+ )
89
+ end
90
+ end # Selection
91
+ end # Config
92
+ end # Mutant
data/lib/mutant/config.rb CHANGED
@@ -22,6 +22,7 @@ module Mutant
22
22
  :mutation,
23
23
  :reporter,
24
24
  :requires,
25
+ :selection,
25
26
  :usage
26
27
  )
27
28
 
@@ -89,6 +90,7 @@ module Mutant
89
90
  matcher: matcher.merge(other.matcher),
90
91
  mutation: mutation.merge(other.mutation),
91
92
  requires: requires + other.requires,
93
+ selection: selection.merge(other.selection),
92
94
  usage: other.usage.merge(usage)
93
95
  )
94
96
  end
@@ -269,6 +271,10 @@ module Mutant
269
271
  transform: Transform::STRING_ARRAY,
270
272
  value: 'requires'
271
273
  ),
274
+ Transform::Hash::Key.new(
275
+ transform: ->(value) { Selection::TRANSFORM.call(value) },
276
+ value: 'selection'
277
+ ),
272
278
  Transform::Hash::Key.new(
273
279
  transform: Usage::TRANSFORM,
274
280
  value: 'usage'