mutant 0.16.3 → 0.17.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 +4 -4
- data/VERSION +1 -1
- data/lib/mutant/bootstrap.rb +56 -7
- data/lib/mutant/cli/command/environment.rb +20 -2
- 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/meta.rb +1 -1
- data/lib/mutant/mutation/runner/sink.rb +27 -1
- data/lib/mutant/reporter/cli/printer/env.rb +6 -0
- data/lib/mutant/result/json_writer.rb +7 -1
- 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/test.rb +2 -1
- data/lib/mutant/transform.rb +69 -0
- data/lib/mutant.rb +5 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 752a1938022756ae4d788946052f227269843741bafc857cb9be24bd94b5a272
|
|
4
|
+
data.tar.gz: b7f0ec5bc7a221c9503c923d20781a061826826acb3caf605a4601e77a808bb5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ae4b675e810a9b94f340e9888722eb1a72b78f8bcb008dd0dec44919a3f5d4ef3d440f117c4c159db59528a6362b3b53ea6e0ec808833798d7f9f4ca1c0ab633
|
|
7
|
+
data.tar.gz: a95c6b2b83be6d672284bc6139cd8f37fb29a1a02e5babc3a5c304b6a6eeba143b37d68b6dee39c5de6355c93800435443e9abdf68e4ffd301cdb60b334d9dd5
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.17.0
|
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
|
|
|
@@ -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'
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mutant
|
|
4
|
+
class ContextMap
|
|
5
|
+
# Reads simplecov's `coverage.json` into a ContextMap
|
|
6
|
+
#
|
|
7
|
+
# Every way the read can fail ends in an explanation of how to produce the
|
|
8
|
+
# recording, because the recording is the whole reason the strategy was
|
|
9
|
+
# asked for.
|
|
10
|
+
#
|
|
11
|
+
# rubocop:disable Metrics/ClassLength
|
|
12
|
+
class Loader
|
|
13
|
+
include Anima.new(:artifact, :world)
|
|
14
|
+
|
|
15
|
+
MISSING_ARTIFACT = <<~'MESSAGE'
|
|
16
|
+
Test selection strategy `context_map` needs a per test coverage recording,
|
|
17
|
+
but none exists at:
|
|
18
|
+
|
|
19
|
+
%<path>s
|
|
20
|
+
|
|
21
|
+
Record one with simplecov 1.2.0 or newer:
|
|
22
|
+
|
|
23
|
+
# spec/spec_helper.rb, or test/test_helper.rb
|
|
24
|
+
require 'simplecov'
|
|
25
|
+
SimpleCov.start do
|
|
26
|
+
track_tests
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
Run the test suite once to write the recording, then re-run mutant. The
|
|
30
|
+
default HTML formatter writes `coverage.json` beside its report, as does
|
|
31
|
+
`SimpleCov::Formatter::JSONFormatter`.
|
|
32
|
+
|
|
33
|
+
Pass `--selection-path DIRECTORY` if the recording is not under ./coverage.
|
|
34
|
+
MESSAGE
|
|
35
|
+
|
|
36
|
+
UNREADABLE_ARTIFACT = <<~'MESSAGE'
|
|
37
|
+
Unable to read the coverage recording at:
|
|
38
|
+
|
|
39
|
+
%<path>s
|
|
40
|
+
|
|
41
|
+
%<error>s
|
|
42
|
+
MESSAGE
|
|
43
|
+
|
|
44
|
+
INVALID_ARTIFACT = <<~'MESSAGE'
|
|
45
|
+
The coverage recording at:
|
|
46
|
+
|
|
47
|
+
%<path>s
|
|
48
|
+
|
|
49
|
+
is not a simplecov report mutant can read:
|
|
50
|
+
|
|
51
|
+
%<error>s
|
|
52
|
+
|
|
53
|
+
Delete it and record it again with simplecov 1.2.0 or newer.
|
|
54
|
+
MESSAGE
|
|
55
|
+
|
|
56
|
+
UNKNOWN_SCHEMA = <<~'MESSAGE'
|
|
57
|
+
The coverage recording at:
|
|
58
|
+
|
|
59
|
+
%<path>s
|
|
60
|
+
|
|
61
|
+
uses report schema version %<schema>s, which mutant does not understand.
|
|
62
|
+
Upgrade mutant, or record again with a simplecov that writes schema
|
|
63
|
+
version 1.
|
|
64
|
+
MESSAGE
|
|
65
|
+
|
|
66
|
+
NO_CONTEXTS = <<~'MESSAGE'
|
|
67
|
+
The coverage recording at:
|
|
68
|
+
|
|
69
|
+
%<path>s
|
|
70
|
+
|
|
71
|
+
has no per test data, so mutant cannot tell which tests cover a subject.
|
|
72
|
+
|
|
73
|
+
Enable tracking with simplecov 1.2.0 or newer:
|
|
74
|
+
|
|
75
|
+
SimpleCov.start do
|
|
76
|
+
track_tests
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
Run the test suite once with tracking enabled, then re-run mutant.
|
|
80
|
+
MESSAGE
|
|
81
|
+
|
|
82
|
+
UNKNOWN_CONTEXT = '%<file>s refers to a context the recording does not list'
|
|
83
|
+
NOT_DIGITS = 'Expected: digits in base %<base>d but got: %<actual>s'
|
|
84
|
+
|
|
85
|
+
# An integer written as digits in a base, and nothing else
|
|
86
|
+
#
|
|
87
|
+
# Kernel#Integer would also take a sign, a prefix, whitespace or
|
|
88
|
+
# underscores, none of which the format allows.
|
|
89
|
+
def self.parse_digits(string, base:, pattern:)
|
|
90
|
+
if string.match?(pattern)
|
|
91
|
+
Either::Right.new(Integer(string, base))
|
|
92
|
+
else
|
|
93
|
+
Either::Left.new(NOT_DIGITS % { actual: string.inspect, base: })
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
private_class_method :parse_digits
|
|
97
|
+
|
|
98
|
+
CONTEXT_INDEX = Transform::Sequence.new(
|
|
99
|
+
steps: [
|
|
100
|
+
Transform::STRING,
|
|
101
|
+
Transform::Block.capture(:decimal) { |string| parse_digits(string, base: 10, pattern: /\A\d+\z/) }
|
|
102
|
+
]
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
LINE_BITMAP = Transform::Sequence.new(
|
|
106
|
+
steps: [
|
|
107
|
+
Transform::STRING,
|
|
108
|
+
Transform::Block.capture(:hexadecimal) { |string| parse_digits(string, base: 16, pattern: /\A\h+\z/) }
|
|
109
|
+
]
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Per source file: the index of each test that executed it, and a bitmap
|
|
113
|
+
# of the lines it executed
|
|
114
|
+
TABLE = Transform::Hash::Map.new(key: CONTEXT_INDEX, value: LINE_BITMAP)
|
|
115
|
+
|
|
116
|
+
# The report carries far more than mutant reads, and gains keys between
|
|
117
|
+
# minor schema versions. Each hash below is sliced to the keys mutant
|
|
118
|
+
# names before the strict transform sees it.
|
|
119
|
+
#
|
|
120
|
+
# A file with no `contexts` section is one no recorded test executed, and
|
|
121
|
+
# contributes nothing rather than making the recording unreadable.
|
|
122
|
+
FILE = Transform::Sequence.new(
|
|
123
|
+
steps: [
|
|
124
|
+
Transform::Hash::Slice.new(keys: %w[contexts]),
|
|
125
|
+
Transform::Hash.new(
|
|
126
|
+
optional: [Transform::Hash::Key.new(value: 'contexts', transform: TABLE)],
|
|
127
|
+
required: []
|
|
128
|
+
)
|
|
129
|
+
]
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
META = Transform::Sequence.new(
|
|
133
|
+
steps: [
|
|
134
|
+
Transform::Hash::Slice.new(keys: %w[root schema_version]),
|
|
135
|
+
Transform::Hash.new(
|
|
136
|
+
optional: [],
|
|
137
|
+
required: [
|
|
138
|
+
Transform::Hash::Key.new(value: 'root', transform: Transform::STRING),
|
|
139
|
+
Transform::Hash::Key.new(value: 'schema_version', transform: Transform::STRING)
|
|
140
|
+
]
|
|
141
|
+
)
|
|
142
|
+
]
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
COVERAGE = Transform::Hash::Map.new(key: Transform::STRING, value: FILE)
|
|
146
|
+
|
|
147
|
+
DOCUMENT = Transform::Sequence.new(
|
|
148
|
+
steps: [
|
|
149
|
+
Transform::Hash::Slice.new(keys: %w[contexts coverage meta]),
|
|
150
|
+
Transform::Hash.new(
|
|
151
|
+
optional: [Transform::Hash::Key.new(value: 'contexts', transform: Transform::STRING_ARRAY)],
|
|
152
|
+
required: [
|
|
153
|
+
Transform::Hash::Key.new(value: 'coverage', transform: COVERAGE),
|
|
154
|
+
Transform::Hash::Key.new(value: 'meta', transform: META)
|
|
155
|
+
]
|
|
156
|
+
)
|
|
157
|
+
]
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
private_constant(*constants(false))
|
|
161
|
+
|
|
162
|
+
# Load the recording under path
|
|
163
|
+
#
|
|
164
|
+
# @return [Either<String, ContextMap>]
|
|
165
|
+
def self.call(path:, world:)
|
|
166
|
+
artifact = path.file? ? path : path.join(ARTIFACT_BASENAME)
|
|
167
|
+
|
|
168
|
+
new(artifact:, world:).call
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Load the recording
|
|
172
|
+
#
|
|
173
|
+
# @return [Either<String, ContextMap>]
|
|
174
|
+
def call
|
|
175
|
+
return error(MISSING_ARTIFACT) unless artifact.file?
|
|
176
|
+
|
|
177
|
+
read
|
|
178
|
+
.bind(&method(:parse))
|
|
179
|
+
.bind(&method(:decode))
|
|
180
|
+
.bind(&method(:from_document))
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
def read
|
|
186
|
+
Either
|
|
187
|
+
.wrap_error(SystemCallError) { artifact.read }
|
|
188
|
+
.lmap { |exception| unreadable(exception) }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def parse(contents)
|
|
192
|
+
world.parse_json(contents).lmap { |exception| unreadable(exception) }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def decode(data)
|
|
196
|
+
DOCUMENT.call(data).lmap { |error| INVALID_ARTIFACT % { error: error.compact_message, path: artifact } }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def unreadable(exception)
|
|
200
|
+
UNREADABLE_ARTIFACT % { error: exception, path: artifact }
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def error(message, **arguments)
|
|
204
|
+
Either::Left.new(message % { path: artifact, **arguments })
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def from_document(document)
|
|
208
|
+
meta = document.fetch('meta')
|
|
209
|
+
schema = meta.fetch('schema_version')
|
|
210
|
+
|
|
211
|
+
return error(UNKNOWN_SCHEMA, schema: schema.inspect) unless schema.match?(SUPPORTED_SCHEMA)
|
|
212
|
+
|
|
213
|
+
contexts = document.fetch('contexts', nil) or return error(NO_CONTEXTS)
|
|
214
|
+
|
|
215
|
+
load_tables(contexts:, coverage: document.fetch('coverage'), root: meta.fetch('root'))
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def load_tables(contexts:, coverage:, root:)
|
|
219
|
+
ids = contexts.map { |context| ContextMap.normalize(context, root) }
|
|
220
|
+
|
|
221
|
+
tables = coverage.each_with_object({}) do |(file, entry), tables|
|
|
222
|
+
table = entry.fetch('contexts', nil) or next
|
|
223
|
+
|
|
224
|
+
resolved = resolve(ids:, table:) or return error(INVALID_ARTIFACT, error: UNKNOWN_CONTEXT % { file: })
|
|
225
|
+
|
|
226
|
+
tables[File.expand_path(file.delete_prefix('/'), root)] = resolved
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
Either::Right.new(ContextMap.new(root:, tables:))
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# nil on an index outside the recording's list. A half read recording
|
|
233
|
+
# would answer coverage questions with silent gaps, and a silent gap is
|
|
234
|
+
# a mutation reported alive that no test was ever given the chance to
|
|
235
|
+
# kill.
|
|
236
|
+
def resolve(ids:, table:)
|
|
237
|
+
table.each_with_object({}) do |(index, bitmap), resolved|
|
|
238
|
+
id = ids.at(index) or return nil
|
|
239
|
+
|
|
240
|
+
resolved[id] = resolved.fetch(id, 0) | bitmap
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end # Loader
|
|
244
|
+
# rubocop:enable Metrics/ClassLength
|
|
245
|
+
end # ContextMap
|
|
246
|
+
end # Mutant
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mutant
|
|
4
|
+
# Per test coverage, read from the artifact a coverage tool wrote.
|
|
5
|
+
#
|
|
6
|
+
# Mutant does not depend on the tool that produces the recording. It reads
|
|
7
|
+
# the `contexts` sections of simplecov's `coverage.json`, which appear once
|
|
8
|
+
# `track_tests` is enabled: an interned list of test ids, and per source file
|
|
9
|
+
# a bitmap of the lines each of those tests executed.
|
|
10
|
+
#
|
|
11
|
+
# The recording answers the question the expression selector can only guess
|
|
12
|
+
# at, which tests actually executed the lines a subject is made of.
|
|
13
|
+
class ContextMap
|
|
14
|
+
include Adamantium, Anima.new(:root, :tables)
|
|
15
|
+
|
|
16
|
+
# Basename simplecov writes its report under
|
|
17
|
+
ARTIFACT_BASENAME = 'coverage.json'
|
|
18
|
+
|
|
19
|
+
# Report schema versions this reader understands
|
|
20
|
+
SUPPORTED_SCHEMA = /\A1\./
|
|
21
|
+
|
|
22
|
+
# Absolute `path:line` form of a location
|
|
23
|
+
#
|
|
24
|
+
# Test ids are recorded relative to the project root, while the
|
|
25
|
+
# integrations report locations the way their framework spells them. Both
|
|
26
|
+
# sides pass through here so that they compare as strings.
|
|
27
|
+
#
|
|
28
|
+
# @param [String] location
|
|
29
|
+
# @param [String] root
|
|
30
|
+
#
|
|
31
|
+
# @return [String]
|
|
32
|
+
def self.normalize(location, root)
|
|
33
|
+
path, separator, line = location.rpartition(':')
|
|
34
|
+
|
|
35
|
+
return location if separator.empty?
|
|
36
|
+
|
|
37
|
+
"#{File.expand_path(path, root)}:#{line}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Absolute `path:line` form of a location, against this recording's root
|
|
41
|
+
#
|
|
42
|
+
# @param [String] location
|
|
43
|
+
#
|
|
44
|
+
# @return [String]
|
|
45
|
+
def normalize(location) = self.class.normalize(location, root)
|
|
46
|
+
|
|
47
|
+
# How many of `lines` in `path` each test executed, for the tests that
|
|
48
|
+
# executed any of them
|
|
49
|
+
#
|
|
50
|
+
# The count is what ranks the tests: one that ran the whole method body is
|
|
51
|
+
# likelier to reach a mutated expression than one that grazed a guard.
|
|
52
|
+
#
|
|
53
|
+
# @param [Pathname] path
|
|
54
|
+
# @param [Range<Integer>] lines
|
|
55
|
+
#
|
|
56
|
+
# @return [Hash{String => Integer}]
|
|
57
|
+
def reach(path:, lines:)
|
|
58
|
+
table = tables.fetch(File.expand_path(path), nil)
|
|
59
|
+
|
|
60
|
+
return EMPTY_HASH unless table
|
|
61
|
+
|
|
62
|
+
# Bit N is line N+1, so the subject's lines are the bits from
|
|
63
|
+
# `lines.begin - 1` up to and including `lines.end - 1`.
|
|
64
|
+
mask = (1 << lines.end) - (1 << (lines.begin - 1))
|
|
65
|
+
|
|
66
|
+
table.each_with_object({}) do |(context, bitmap), reached|
|
|
67
|
+
covered = self.class.popcount(bitmap & mask)
|
|
68
|
+
|
|
69
|
+
reached[context] = covered unless covered.zero?
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Lines a test executed across the whole project
|
|
74
|
+
#
|
|
75
|
+
# The share of that footprint falling inside a subject is how focused the
|
|
76
|
+
# test is on it. A unit test spends most of its lines there, while a
|
|
77
|
+
# feature spec touching thousands of lines spends almost none.
|
|
78
|
+
#
|
|
79
|
+
# @param [String] context
|
|
80
|
+
#
|
|
81
|
+
# @return [Integer]
|
|
82
|
+
def footprint(context) = footprints.fetch(context, 0)
|
|
83
|
+
|
|
84
|
+
# Number of set bits
|
|
85
|
+
#
|
|
86
|
+
# Ruby has no population count on Integer, and a binary string is both the
|
|
87
|
+
# idiom and fast enough at the sizes a line bitmap reaches.
|
|
88
|
+
#
|
|
89
|
+
# @param [Integer] value
|
|
90
|
+
#
|
|
91
|
+
# @return [Integer]
|
|
92
|
+
def self.popcount(value) = value.to_s(2).count('1')
|
|
93
|
+
|
|
94
|
+
# Every test id the recording knows about
|
|
95
|
+
#
|
|
96
|
+
# @return [Set<String>]
|
|
97
|
+
def context_ids = tables.each_value.flat_map(&:keys).to_set
|
|
98
|
+
memoize :context_ids
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def footprints
|
|
103
|
+
tables.each_value.with_object(Hash.new(0)) do |table, totals|
|
|
104
|
+
table.each { |context, bitmap| totals[context] += self.class.popcount(bitmap) }
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
memoize :footprints
|
|
108
|
+
end # ContextMap
|
|
109
|
+
end # Mutant
|
data/lib/mutant/integration.rb
CHANGED
|
@@ -95,7 +95,7 @@ module Mutant
|
|
|
95
95
|
|
|
96
96
|
def self.attempt_const_get(env)
|
|
97
97
|
integration_name = env.config.integration.name
|
|
98
|
-
constant_name = integration_name.capitalize
|
|
98
|
+
constant_name = integration_name.split(/[-_]/).map(&:capitalize).join
|
|
99
99
|
|
|
100
100
|
Either.wrap_error(NameError) { const_get(constant_name) }.lmap do |exception|
|
|
101
101
|
CONST_MESSAGE % {
|
data/lib/mutant/meta.rb
CHANGED
|
@@ -8,12 +8,16 @@ module Mutant
|
|
|
8
8
|
|
|
9
9
|
include Anima.new(:env)
|
|
10
10
|
|
|
11
|
+
# Minimum seconds between two session flushes
|
|
12
|
+
FLUSH_INTERVAL = 1.0
|
|
13
|
+
|
|
11
14
|
# Initialize object
|
|
12
15
|
#
|
|
13
16
|
# @return [undefined]
|
|
14
17
|
def initialize(*)
|
|
15
18
|
super
|
|
16
19
|
@start = env.world.timer.now
|
|
20
|
+
@last_flush_at = @start - FLUSH_INTERVAL
|
|
17
21
|
@subject_results = {}
|
|
18
22
|
end
|
|
19
23
|
|
|
@@ -48,10 +52,11 @@ module Mutant
|
|
|
48
52
|
mutation = env.mutations.fetch(response.result.mutation_index)
|
|
49
53
|
subject = mutation.subject
|
|
50
54
|
mutation_result = mutation_result(mutation, response.result)
|
|
55
|
+
coverage = coverage_result(mutation_result)
|
|
51
56
|
|
|
52
57
|
@subject_results[subject] = Result::Subject.new(
|
|
53
58
|
amount_mutations: subject.mutations.length,
|
|
54
|
-
coverage_results: previous_coverage_results(subject).dup <<
|
|
59
|
+
coverage_results: previous_coverage_results(subject).dup << coverage,
|
|
55
60
|
expression_syntax: subject.expression.syntax,
|
|
56
61
|
identification: subject.identification,
|
|
57
62
|
node: subject.node,
|
|
@@ -60,6 +65,8 @@ module Mutant
|
|
|
60
65
|
tests: env.selections.fetch(subject)
|
|
61
66
|
)
|
|
62
67
|
|
|
68
|
+
flush_session unless coverage.success?
|
|
69
|
+
|
|
63
70
|
self
|
|
64
71
|
end
|
|
65
72
|
# rubocop:enable Metrics/AbcSize
|
|
@@ -67,6 +74,25 @@ module Mutant
|
|
|
67
74
|
|
|
68
75
|
private
|
|
69
76
|
|
|
77
|
+
# Persist the session on alive mutations, so survivors are
|
|
78
|
+
# inspectable while a long run is still going, rather than only
|
|
79
|
+
# after its final write.
|
|
80
|
+
#
|
|
81
|
+
# Alive mutations can arrive far faster than once per second, and
|
|
82
|
+
# each flush serializes the whole result tree on the main process,
|
|
83
|
+
# so flushes are rate limited against the monotonic timer. A
|
|
84
|
+
# survivor that lands inside the suppressed window is picked up by
|
|
85
|
+
# the next flush, or by the final write in the runner.
|
|
86
|
+
def flush_session
|
|
87
|
+
now = env.world.timer.now
|
|
88
|
+
|
|
89
|
+
return if (now - @last_flush_at) < FLUSH_INTERVAL
|
|
90
|
+
|
|
91
|
+
@last_flush_at = now
|
|
92
|
+
|
|
93
|
+
Result::JSONWriter.new(env:, result: status).call
|
|
94
|
+
end
|
|
95
|
+
|
|
70
96
|
def coverage_result(mutation_result)
|
|
71
97
|
Result::Coverage.new(
|
|
72
98
|
mutation_result:,
|
|
@@ -13,10 +13,12 @@ module Mutant
|
|
|
13
13
|
:amount_subjects,
|
|
14
14
|
:amount_all_tests,
|
|
15
15
|
:config,
|
|
16
|
+
:selector,
|
|
16
17
|
:test_subject_ratio
|
|
17
18
|
)
|
|
18
19
|
|
|
19
20
|
FORMATS = [
|
|
21
|
+
[:info, 'Selection: %s', :selection_name ],
|
|
20
22
|
[:info, 'Subjects: %s', :amount_subjects ],
|
|
21
23
|
[:info, 'All-Tests: %s', :amount_all_tests ],
|
|
22
24
|
[:info, 'Available-Tests: %s', :amount_available_tests],
|
|
@@ -35,6 +37,10 @@ module Mutant
|
|
|
35
37
|
__send__(report, format, __send__(value))
|
|
36
38
|
end
|
|
37
39
|
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def selection_name = selector.name
|
|
38
44
|
end # Env
|
|
39
45
|
end # Printer
|
|
40
46
|
end # CLI
|
|
@@ -10,13 +10,19 @@ module Mutant
|
|
|
10
10
|
|
|
11
11
|
# Write result JSON file
|
|
12
12
|
#
|
|
13
|
+
# Written to a temporary file first and renamed into place, so a
|
|
14
|
+
# concurrent reader of the session file never observes a partial
|
|
15
|
+
# document. This matters once the file is rewritten during a run.
|
|
16
|
+
#
|
|
13
17
|
# @return [Pathname]
|
|
14
18
|
def call
|
|
15
19
|
dir = env.world.pathname.new(RESULTS_DIR)
|
|
16
20
|
dir.mkpath
|
|
17
21
|
|
|
18
22
|
path = dir.join("#{SESSION_ID}.json")
|
|
19
|
-
|
|
23
|
+
tmp_path = dir.join("#{SESSION_ID}.json.tmp")
|
|
24
|
+
tmp_path.write(json)
|
|
25
|
+
tmp_path.rename(path)
|
|
20
26
|
|
|
21
27
|
path
|
|
22
28
|
end
|
data/lib/mutant/result.rb
CHANGED
|
@@ -375,7 +375,7 @@ module Mutant
|
|
|
375
375
|
end
|
|
376
376
|
|
|
377
377
|
load_test = Transform::Success.new(
|
|
378
|
-
block: ->(id) { Mutant::Test.new(expressions: EMPTY_ARRAY, id:) }
|
|
378
|
+
block: ->(id) { Mutant::Test.new(expressions: EMPTY_ARRAY, id:, location: nil) }
|
|
379
379
|
)
|
|
380
380
|
|
|
381
381
|
derive_node = Transform::Success.new(
|
|
@@ -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
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
|
@@ -225,13 +225,17 @@ module Mutant
|
|
|
225
225
|
require 'mutant/timer'
|
|
226
226
|
require 'mutant/integration'
|
|
227
227
|
require 'mutant/integration/null'
|
|
228
|
+
require 'mutant/context_map'
|
|
229
|
+
require 'mutant/context_map/loader'
|
|
228
230
|
require 'mutant/selector'
|
|
231
|
+
require 'mutant/selector/context_map'
|
|
229
232
|
require 'mutant/selector/expression'
|
|
230
233
|
require 'mutant/selector/null'
|
|
231
234
|
require 'mutant/world'
|
|
232
235
|
require 'mutant/hooks'
|
|
233
236
|
require 'mutant/config'
|
|
234
237
|
require 'mutant/config/coverage_criteria'
|
|
238
|
+
require 'mutant/config/selection'
|
|
235
239
|
require 'mutant/cli'
|
|
236
240
|
require 'mutant/cli/command'
|
|
237
241
|
require 'mutant/cli/command/environment'
|
|
@@ -378,6 +382,7 @@ module Mutant
|
|
|
378
382
|
mutation: Mutation::Config::EMPTY,
|
|
379
383
|
reporter: Reporter::CLI.build(WORLD.stdout),
|
|
380
384
|
requires: EMPTY_ARRAY,
|
|
385
|
+
selection: Config::Selection::DEFAULT,
|
|
381
386
|
usage: Usage::Unknown.new
|
|
382
387
|
)
|
|
383
388
|
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.0
|
|
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
|
|
@@ -391,6 +394,7 @@ files:
|
|
|
391
394
|
- lib/mutant/segment.rb
|
|
392
395
|
- lib/mutant/segment/recorder.rb
|
|
393
396
|
- lib/mutant/selector.rb
|
|
397
|
+
- lib/mutant/selector/context_map.rb
|
|
394
398
|
- lib/mutant/selector/expression.rb
|
|
395
399
|
- lib/mutant/selector/null.rb
|
|
396
400
|
- lib/mutant/subject.rb
|