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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fd77b62cd208529f1fd030e87ffbce9c4df69cf18c097f7895ddd76204f8577d
|
|
4
|
+
data.tar.gz: f4ce398e717e5a8d34aa2ed57eafea5358cdb7af02fb8dc1dbcca2b862f754cc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c6dbb4139015cfe33c91b7c802004aca5d9015299d517552c56bcaa4cc81fa40b5a5f8e39bd17319ff0876da90b2cc7164566c0c65221f6dd0de1758fe767829
|
|
7
|
+
data.tar.gz: caa47a0c66816fe06b76bf0da184d9b91404abceb93eec83d374c42f1d2c3c51ae578e57b98f00b9fb431ffcc770cb864aabfe203bdd9400b30a77fdf8acade3
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.17.1
|
data/lib/mutant/ast/structure.rb
CHANGED
|
@@ -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]]),
|
data/lib/mutant/bootstrap.rb
CHANGED
|
@@ -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).
|
|
81
|
-
env.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
9
|
-
SHORT_DESCRIPTION
|
|
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 =
|
|
10
|
+
RESULTS_DIR = Result::SessionFiles::DIRECTORY
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
GZIP_MAGIC = [0x1F, 0x8B].pack('C2')
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
dir = world.pathname.new(RESULTS_DIR)
|
|
14
|
+
private_constant(:GZIP_MAGIC)
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
private
|
|
18
17
|
|
|
19
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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'
|