mutant 0.11.16 → 0.11.18
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/bin/mutant +55 -46
- data/lib/mutant/bootstrap.rb +66 -35
- data/lib/mutant/cli/command/environment/run.rb +1 -1
- data/lib/mutant/cli/command/environment.rb +2 -2
- data/lib/mutant/cli/command.rb +24 -11
- data/lib/mutant/env.rb +9 -0
- data/lib/mutant/mutator/node/literal/regex.rb +8 -8
- data/lib/mutant/mutator/node/send.rb +27 -17
- data/lib/mutant/mutator/regexp.rb +211 -0
- data/lib/mutant/parallel/driver.rb +1 -0
- data/lib/mutant/parallel/worker.rb +5 -1
- data/lib/mutant/runner.rb +8 -6
- data/lib/mutant/segment/recorder.rb +124 -0
- data/lib/mutant/segment.rb +25 -0
- data/lib/mutant/version.rb +1 -1
- data/lib/mutant/world.rb +5 -0
- data/lib/mutant.rb +288 -228
- metadata +12 -33
- data/lib/mutant/ast/regexp/transformer/direct.rb +0 -145
- data/lib/mutant/ast/regexp/transformer/named_group.rb +0 -50
- data/lib/mutant/ast/regexp/transformer/options_group.rb +0 -68
- data/lib/mutant/ast/regexp/transformer/quantifier.rb +0 -92
- data/lib/mutant/ast/regexp/transformer/recursive.rb +0 -56
- data/lib/mutant/ast/regexp/transformer/root.rb +0 -28
- data/lib/mutant/ast/regexp/transformer/text.rb +0 -58
- data/lib/mutant/ast/regexp/transformer.rb +0 -152
- data/lib/mutant/ast/regexp.rb +0 -54
- data/lib/mutant/mutator/node/regexp/alternation_meta.rb +0 -20
- data/lib/mutant/mutator/node/regexp/beginning_of_line_anchor.rb +0 -20
- data/lib/mutant/mutator/node/regexp/capture_group.rb +0 -23
- data/lib/mutant/mutator/node/regexp/character_type.rb +0 -31
- data/lib/mutant/mutator/node/regexp/end_of_line_anchor.rb +0 -20
- data/lib/mutant/mutator/node/regexp/end_of_string_or_before_end_of_line_anchor.rb +0 -20
- data/lib/mutant/mutator/node/regexp/named_group.rb +0 -39
- data/lib/mutant/mutator/node/regexp/zero_or_more.rb +0 -34
- data/lib/mutant/mutator/node/regexp.rb +0 -20
@@ -0,0 +1,211 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mutant
|
4
|
+
class Mutator
|
5
|
+
class Regexp < self
|
6
|
+
def self.regexp_body(node)
|
7
|
+
*body, _options = node.children
|
8
|
+
|
9
|
+
body.map do |child|
|
10
|
+
return unless child.type.equal?(:str)
|
11
|
+
child.children
|
12
|
+
end.join
|
13
|
+
end
|
14
|
+
|
15
|
+
class Registry
|
16
|
+
include Concord.new(:contents)
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
super({})
|
20
|
+
end
|
21
|
+
|
22
|
+
def register(expression_class, mutator_class)
|
23
|
+
(contents[expression_class] ||= []) << mutator_class
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def lookup(expression_class)
|
28
|
+
contents.fetch(expression_class, []).push(Quantifier)
|
29
|
+
end
|
30
|
+
end # Registry
|
31
|
+
|
32
|
+
REGISTRY = Registry.new
|
33
|
+
|
34
|
+
# mutant:disable - boot time code
|
35
|
+
def self.handle(*types)
|
36
|
+
types.each do |type|
|
37
|
+
self::REGISTRY.register(type, self)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private_class_method :handle
|
42
|
+
|
43
|
+
def self.mutate(expression)
|
44
|
+
REGISTRY
|
45
|
+
.lookup(expression.class)
|
46
|
+
.map { |mutator| mutator.call(input: expression, parent: nil) }
|
47
|
+
.reduce(&:merge)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def subexpressions
|
53
|
+
input.expressions
|
54
|
+
end
|
55
|
+
|
56
|
+
def mk_dup
|
57
|
+
Marshal.load(Marshal.dump(input))
|
58
|
+
end
|
59
|
+
|
60
|
+
def emit_expression(klass:, text:)
|
61
|
+
emit(
|
62
|
+
klass.construct(text: text).tap do |new|
|
63
|
+
subexpressions.each do |expression|
|
64
|
+
new << Marshal.load(Marshal.dump(expression))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
def emit_passive_group
|
71
|
+
emit_expression(
|
72
|
+
klass: ::Regexp::Expression::Group::Passive,
|
73
|
+
text: '(?:'
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
class Alternation < self
|
78
|
+
handle(::Regexp::Expression::Alternation)
|
79
|
+
|
80
|
+
def dispatch
|
81
|
+
subexpressions.each_index do |index|
|
82
|
+
emit(mk_dup.tap { |new| new.expressions.delete_at(index) })
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class Quantifier < self
|
88
|
+
MAP = {
|
89
|
+
'*' => '+',
|
90
|
+
'*+' => '++',
|
91
|
+
'*?' => '+?'
|
92
|
+
}.freeze
|
93
|
+
|
94
|
+
def dispatch
|
95
|
+
return unless input.quantifier
|
96
|
+
|
97
|
+
emit_removal
|
98
|
+
emit_replacement
|
99
|
+
end
|
100
|
+
|
101
|
+
def emit_removal
|
102
|
+
emit(mk_dup.tap { |new| new.quantifier = nil })
|
103
|
+
end
|
104
|
+
|
105
|
+
def emit_replacement
|
106
|
+
new_text = MAP[input.quantifier.text]
|
107
|
+
|
108
|
+
emit(mk_dup.tap { |new| new.quantifier.text = new_text })
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class Replacement < self
|
113
|
+
ns = ::Regexp::Expression
|
114
|
+
|
115
|
+
dual_table =
|
116
|
+
[
|
117
|
+
[ns::Anchor::WordBoundary, '\\b', ns::Anchor::NonWordBoundary, '\\B'],
|
118
|
+
[ns::CharacterType::Digit, '\\d', ns::CharacterType::NonDigit, '\\D'],
|
119
|
+
[ns::CharacterType::ExtendedGrapheme, '\\X', ns::CharacterType::Linebreak, '\\R'],
|
120
|
+
[ns::CharacterType::Hex, '\\h', ns::CharacterType::NonHex, '\\H'],
|
121
|
+
[ns::CharacterType::Space, '\\s', ns::CharacterType::NonSpace, '\\S'],
|
122
|
+
[ns::CharacterType::Word, '\\w', ns::CharacterType::NonWord, '\\W']
|
123
|
+
]
|
124
|
+
|
125
|
+
MAP = dual_table.flat_map do |(left_key, left_text, right_key, right_text)|
|
126
|
+
[
|
127
|
+
[left_key, [right_key, right_text]],
|
128
|
+
[right_key, [left_key, left_text]]
|
129
|
+
]
|
130
|
+
end.to_h
|
131
|
+
|
132
|
+
MAP[ns::Anchor::BeginningOfLine] = [ns::Anchor::BeginningOfString, '\\A']
|
133
|
+
MAP[ns::Anchor::EndOfLine] = [ns::Anchor::EndOfString, '\\z']
|
134
|
+
MAP[ns::Anchor::EndOfStringOrBeforeEndOfLine] = [ns::Anchor::EndOfString, '\\z']
|
135
|
+
|
136
|
+
MAP.freeze
|
137
|
+
|
138
|
+
handle(*MAP.keys)
|
139
|
+
|
140
|
+
def dispatch
|
141
|
+
klass, text = MAP.fetch(input.class)
|
142
|
+
|
143
|
+
emit(klass.construct(text: text).tap { |new| new.quantifier = input.quantifier })
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
class GroupCapturePositional < self
|
148
|
+
handle(::Regexp::Expression::Group::Capture)
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
def dispatch
|
153
|
+
emit_passive_group unless subexpressions.empty?
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
class GroupCaptureNamed < self
|
158
|
+
handle(::Regexp::Expression::Group::Named)
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
def dispatch
|
163
|
+
return if input.name.start_with?('_') || subexpressions.empty?
|
164
|
+
|
165
|
+
emit_passive_group
|
166
|
+
|
167
|
+
emit_expression(
|
168
|
+
klass: ::Regexp::Expression::Group::Named,
|
169
|
+
text: "(?<_#{input.name}>"
|
170
|
+
)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
class Recurse < self
|
175
|
+
ns = ::Regexp::Expression
|
176
|
+
|
177
|
+
# This list of nodes with subexessions is not yet complete.
|
178
|
+
handle(ns::Assertion::Base)
|
179
|
+
handle(ns::Assertion::Lookahead)
|
180
|
+
handle(ns::Assertion::Lookbehind)
|
181
|
+
handle(ns::Assertion::NegativeLookahead)
|
182
|
+
handle(ns::Assertion::NegativeLookbehind)
|
183
|
+
handle(ns::Alternative)
|
184
|
+
handle(ns::Alternation)
|
185
|
+
handle(ns::Sequence)
|
186
|
+
handle(ns::CharacterSet)
|
187
|
+
handle(ns::CharacterSet::Range)
|
188
|
+
handle(ns::Conditional::Branch)
|
189
|
+
handle(ns::Conditional::Expression)
|
190
|
+
handle(ns::Group::Absence)
|
191
|
+
handle(ns::Group::Atomic)
|
192
|
+
handle(ns::Group::Capture)
|
193
|
+
handle(ns::Group::Comment)
|
194
|
+
handle(ns::Group::Named)
|
195
|
+
handle(ns::Group::Options)
|
196
|
+
handle(ns::Group::Passive)
|
197
|
+
handle(ns::Root)
|
198
|
+
|
199
|
+
private
|
200
|
+
|
201
|
+
def dispatch
|
202
|
+
subexpressions.each_with_index do |expression, index|
|
203
|
+
self.class.mutate(expression).each do |new_expression|
|
204
|
+
emit(mk_dup.tap { |new| new.expressions[index] = new_expression })
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end # Regexp
|
210
|
+
end # Mutator
|
211
|
+
end # Mutant
|
data/lib/mutant/runner.rb
CHANGED
@@ -15,15 +15,17 @@ module Mutant
|
|
15
15
|
def self.run_mutation_analysis(env)
|
16
16
|
reporter = reporter(env)
|
17
17
|
|
18
|
-
|
19
|
-
reporter,
|
20
|
-
|
21
|
-
).tap do |result|
|
22
|
-
reporter.report(result)
|
23
|
-
end
|
18
|
+
env
|
19
|
+
.record(:analysis) { run_driver(reporter, async_driver(env)) }
|
20
|
+
.tap { |result| env.record(:report) { reporter.report(result) } }
|
24
21
|
end
|
25
22
|
private_class_method :run_mutation_analysis
|
26
23
|
|
24
|
+
def self.async_driver(env)
|
25
|
+
Parallel.async(env.world, mutation_test_config(env))
|
26
|
+
end
|
27
|
+
private_class_method :async_driver
|
28
|
+
|
27
29
|
def self.run_driver(reporter, driver)
|
28
30
|
Signal.trap('INT') do
|
29
31
|
driver.stop
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mutant
|
4
|
+
class Segment
|
5
|
+
class Recorder
|
6
|
+
include Anima.new(
|
7
|
+
:gen_id,
|
8
|
+
:parent_id,
|
9
|
+
:recording_start,
|
10
|
+
:root_id,
|
11
|
+
:segments,
|
12
|
+
:timer
|
13
|
+
)
|
14
|
+
|
15
|
+
private(*anima.attribute_names)
|
16
|
+
|
17
|
+
# rubocop:disable Metrics/MethodLength
|
18
|
+
def record(name)
|
19
|
+
start = timer.now
|
20
|
+
parent_id = parent_id()
|
21
|
+
|
22
|
+
@parent_id = id = gen_id.call
|
23
|
+
|
24
|
+
yield.tap do
|
25
|
+
segments << Segment.new(
|
26
|
+
id: id,
|
27
|
+
name: name,
|
28
|
+
parent_id: parent_id,
|
29
|
+
timestamp_end: timer.now,
|
30
|
+
timestamp_start: start
|
31
|
+
)
|
32
|
+
end
|
33
|
+
ensure
|
34
|
+
@parent_id = parent_id
|
35
|
+
end
|
36
|
+
# rubocop:enable Metrics/MethodLength
|
37
|
+
|
38
|
+
def print_profile(io)
|
39
|
+
print_node(io, tree, 0)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
class Node
|
45
|
+
include Adamantium, Anima.new(:value, :children)
|
46
|
+
end
|
47
|
+
private_constant :Node
|
48
|
+
|
49
|
+
def tree
|
50
|
+
id_index = {}
|
51
|
+
parent_index = {}
|
52
|
+
|
53
|
+
final_segments.each do |segment|
|
54
|
+
id_index[segment.id] = segment
|
55
|
+
|
56
|
+
(parent_index[segment.parent_id] ||= []) << segment
|
57
|
+
end
|
58
|
+
|
59
|
+
build_node(
|
60
|
+
value: id_index.fetch(root_id),
|
61
|
+
parent_index: parent_index
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
def final_segments
|
66
|
+
timestamp_end = timer.now
|
67
|
+
|
68
|
+
segments.map do |segment|
|
69
|
+
if segment.timestamp_end
|
70
|
+
segment
|
71
|
+
else
|
72
|
+
segment.with(timestamp_end: timestamp_end)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def build_node(value:, parent_index:)
|
78
|
+
Node.new(
|
79
|
+
value: value,
|
80
|
+
children: build_children(
|
81
|
+
parent_id: value.id,
|
82
|
+
parent_index: parent_index
|
83
|
+
)
|
84
|
+
)
|
85
|
+
end
|
86
|
+
|
87
|
+
def build_children(parent_id:, parent_index:)
|
88
|
+
parent_index
|
89
|
+
.fetch(parent_id, EMPTY_ARRAY)
|
90
|
+
.map { |value| build_node(value: value, parent_index: parent_index) }
|
91
|
+
end
|
92
|
+
|
93
|
+
def print_node(io, node, indent)
|
94
|
+
segment = node.value
|
95
|
+
|
96
|
+
indent_str = ' ' * indent
|
97
|
+
|
98
|
+
print_line(io, :offset_start, segment, indent_str)
|
99
|
+
|
100
|
+
return unless node.children.any?
|
101
|
+
|
102
|
+
node.children.each do |child|
|
103
|
+
print_node(io, child, indent.succ)
|
104
|
+
end
|
105
|
+
print_line(io, :offset_end, segment, indent_str)
|
106
|
+
end
|
107
|
+
|
108
|
+
# rubocop:disable Metrics/ParameterLists
|
109
|
+
# rubocop:disable Style/FormatStringToken
|
110
|
+
def print_line(io, offset, segment, indent_str)
|
111
|
+
io.puts(
|
112
|
+
'%4.4f: (%4.4fs) %s %s' % [
|
113
|
+
segment.public_send(offset, recording_start),
|
114
|
+
segment.elapsed,
|
115
|
+
indent_str,
|
116
|
+
segment.name
|
117
|
+
]
|
118
|
+
)
|
119
|
+
end
|
120
|
+
# rubocop:enable Metrics/ParameterLists
|
121
|
+
# rubocop:enable Style/FormatStringToken
|
122
|
+
end # Recorder
|
123
|
+
end # Segment
|
124
|
+
end # Mutant
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mutant
|
4
|
+
class Segment
|
5
|
+
include Adamantium, Anima.new(
|
6
|
+
:id,
|
7
|
+
:name,
|
8
|
+
:parent_id,
|
9
|
+
:timestamp_end,
|
10
|
+
:timestamp_start
|
11
|
+
)
|
12
|
+
|
13
|
+
def elapsed
|
14
|
+
timestamp_end - timestamp_start
|
15
|
+
end
|
16
|
+
|
17
|
+
def offset_start(recording_start)
|
18
|
+
timestamp_start - recording_start
|
19
|
+
end
|
20
|
+
|
21
|
+
def offset_end(recording_start)
|
22
|
+
timestamp_end - recording_start
|
23
|
+
end
|
24
|
+
end # Segment
|
25
|
+
end # Mutant
|
data/lib/mutant/version.rb
CHANGED
data/lib/mutant/world.rb
CHANGED
@@ -19,6 +19,7 @@ module Mutant
|
|
19
19
|
:pathname,
|
20
20
|
:process,
|
21
21
|
:random,
|
22
|
+
:recorder,
|
22
23
|
:stderr,
|
23
24
|
:stdout,
|
24
25
|
:thread,
|
@@ -77,5 +78,9 @@ module Mutant
|
|
77
78
|
Timer::Deadline::None.new
|
78
79
|
end
|
79
80
|
end
|
81
|
+
|
82
|
+
def record(name, &block)
|
83
|
+
recorder.record(name, &block)
|
84
|
+
end
|
80
85
|
end # World
|
81
86
|
end # Mutant
|