mutant 0.10.0 → 0.10.7
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 +0 -2
- data/lib/mutant.rb +7 -5
- data/lib/mutant/cli/command.rb +8 -6
- data/lib/mutant/cli/command/run.rb +23 -10
- data/lib/mutant/config.rb +78 -77
- data/lib/mutant/env.rb +14 -4
- data/lib/mutant/integration.rb +7 -10
- data/lib/mutant/integration/null.rb +0 -1
- data/lib/mutant/isolation.rb +11 -48
- data/lib/mutant/isolation/fork.rb +107 -40
- data/lib/mutant/isolation/none.rb +18 -5
- data/lib/mutant/license/subscription.rb +1 -1
- data/lib/mutant/license/subscription/commercial.rb +2 -3
- data/lib/mutant/license/subscription/opensource.rb +2 -2
- data/lib/mutant/matcher/config.rb +13 -0
- data/lib/mutant/matcher/method/instance.rb +0 -2
- data/lib/mutant/mutator/node/send.rb +1 -1
- data/lib/mutant/parallel.rb +0 -1
- data/lib/mutant/parallel/worker.rb +0 -2
- data/lib/mutant/reporter/cli.rb +0 -2
- data/lib/mutant/reporter/cli/printer/config.rb +9 -5
- data/lib/mutant/reporter/cli/printer/coverage_result.rb +19 -0
- data/lib/mutant/reporter/cli/printer/env_progress.rb +2 -0
- data/lib/mutant/reporter/cli/printer/isolation_result.rb +19 -35
- data/lib/mutant/reporter/cli/printer/mutation_result.rb +4 -9
- data/lib/mutant/reporter/cli/printer/subject_result.rb +2 -2
- data/lib/mutant/result.rb +81 -30
- data/lib/mutant/runner/sink.rb +12 -5
- data/lib/mutant/selector/expression.rb +3 -1
- data/lib/mutant/test.rb +1 -1
- data/lib/mutant/timer.rb +60 -11
- data/lib/mutant/transform.rb +25 -21
- data/lib/mutant/version.rb +1 -1
- data/lib/mutant/warnings.rb +0 -1
- data/lib/mutant/world.rb +67 -0
- metadata +13 -15
- data/lib/mutant/minitest/coverage.rb +0 -53
- data/lib/mutant/reporter/cli/printer/mutation_progress_result.rb +0 -28
- data/lib/mutant/reporter/cli/printer/subject_progress.rb +0 -58
- data/lib/mutant/reporter/cli/printer/test_result.rb +0 -32
data/lib/mutant/integration.rb
CHANGED
@@ -4,7 +4,7 @@ module Mutant
|
|
4
4
|
|
5
5
|
# Abstract base class mutant test framework integrations
|
6
6
|
class Integration
|
7
|
-
include AbstractType, Adamantium::Flat,
|
7
|
+
include AbstractType, Adamantium::Flat, Anima.new(:expression_parser, :timer)
|
8
8
|
|
9
9
|
LOAD_MESSAGE = <<~'MESSAGE'
|
10
10
|
Unable to load integration mutant-%<integration_name>s:
|
@@ -27,9 +27,12 @@ module Mutant
|
|
27
27
|
#
|
28
28
|
# @return [Either<String, Integration>]
|
29
29
|
def self.setup(env)
|
30
|
-
attempt_require(env)
|
31
|
-
.
|
32
|
-
|
30
|
+
attempt_require(env).bind { attempt_const_get(env) }.fmap do |klass|
|
31
|
+
klass.new(
|
32
|
+
expression_parser: env.config.expression_parser,
|
33
|
+
timer: env.world.timer
|
34
|
+
).setup
|
35
|
+
end
|
33
36
|
end
|
34
37
|
|
35
38
|
# rubocop:disable Style/MultilineBlockChain
|
@@ -80,11 +83,5 @@ module Mutant
|
|
80
83
|
#
|
81
84
|
# @return [Enumerable<Test>]
|
82
85
|
abstract_method :all_tests
|
83
|
-
|
84
|
-
private
|
85
|
-
|
86
|
-
def expression_parser
|
87
|
-
config.expression_parser
|
88
|
-
end
|
89
86
|
end # Integration
|
90
87
|
end # Mutant
|
data/lib/mutant/isolation.rb
CHANGED
@@ -7,57 +7,20 @@ module Mutant
|
|
7
7
|
|
8
8
|
# Isolated computation result
|
9
9
|
class Result
|
10
|
-
include
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
# Add error on top of current result
|
21
|
-
#
|
22
|
-
# @param [Result] error
|
23
|
-
#
|
24
|
-
# @return [Result]
|
25
|
-
def add_error(error)
|
26
|
-
ErrorChain.new(error, self)
|
27
|
-
end
|
28
|
-
|
29
|
-
# The log captured from integration
|
30
|
-
#
|
31
|
-
# @return [String]
|
32
|
-
def log
|
33
|
-
NULL_LOG
|
34
|
-
end
|
35
|
-
|
36
|
-
# Test for success
|
10
|
+
include Anima.new(
|
11
|
+
:exception,
|
12
|
+
:log,
|
13
|
+
:process_status,
|
14
|
+
:timeout,
|
15
|
+
:value
|
16
|
+
)
|
17
|
+
|
18
|
+
# Test for successful result
|
37
19
|
#
|
38
20
|
# @return [Boolean]
|
39
|
-
def
|
40
|
-
|
21
|
+
def valid_value?
|
22
|
+
timeout.nil? && exception.nil? && (process_status.nil? || process_status.success?)
|
41
23
|
end
|
42
|
-
|
43
|
-
# Successful result producing value
|
44
|
-
class Success < self
|
45
|
-
include Concord::Public.new(:value, :log)
|
46
|
-
|
47
|
-
def self.new(_value, _log = '')
|
48
|
-
super
|
49
|
-
end
|
50
|
-
end # Success
|
51
|
-
|
52
|
-
# Unsuccessful result by unexpected exception
|
53
|
-
class Exception < self
|
54
|
-
include Concord::Public.new(:value)
|
55
|
-
end # Error
|
56
|
-
|
57
|
-
# Result when there where many results
|
58
|
-
class ErrorChain < Result
|
59
|
-
include Concord::Public.new(:value, :next)
|
60
|
-
end # ChainError
|
61
24
|
end # Result
|
62
25
|
|
63
26
|
# Call block in isolation
|
@@ -3,22 +3,36 @@
|
|
3
3
|
module Mutant
|
4
4
|
class Isolation
|
5
5
|
# Isolation via the fork(2) systemcall.
|
6
|
+
#
|
7
|
+
# Communication between parent and child process is done
|
8
|
+
# via anonymous pipes.
|
9
|
+
#
|
10
|
+
# Timeouts are initially handled relatively efficiently via IO.select
|
11
|
+
# but once the child process pipes are on eof via busy looping on
|
12
|
+
# waitpid2 with Process::WNOHANG set.
|
13
|
+
#
|
14
|
+
# Handling timeouts this way is not the conceptually most
|
15
|
+
# efficient solution. But its cross platform.
|
16
|
+
#
|
17
|
+
# Design constraints:
|
18
|
+
#
|
19
|
+
# * Support Linux
|
20
|
+
# * Support MacOSX
|
21
|
+
# * Avoid platform specific APIs and code.
|
22
|
+
# * Only use ruby corelib.
|
23
|
+
# * Do not use any named resource.
|
24
|
+
# * Never block on latency inducing systemcall without a
|
25
|
+
# timeout.
|
26
|
+
# * Child process freezing before closing the pipes needs to
|
27
|
+
# be detected by parent process.
|
28
|
+
# * Child process freezing after closing the pipes needs to be
|
29
|
+
# detected by parent process.
|
6
30
|
class Fork < self
|
7
31
|
include(Adamantium::Flat, Concord.new(:world))
|
8
32
|
|
9
33
|
READ_SIZE = 4096
|
10
34
|
|
11
|
-
ATTRIBUTES = %i[block log_pipe result_pipe world].freeze
|
12
|
-
|
13
|
-
# Unsuccessful result as child exited nonzero
|
14
|
-
class ChildError < Result
|
15
|
-
include Concord::Public.new(:value, :log)
|
16
|
-
end # ChildError
|
17
|
-
|
18
|
-
# Unsuccessful result as fork failed
|
19
|
-
class ForkError < Result
|
20
|
-
include Equalizer.new
|
21
|
-
end # ForkError
|
35
|
+
ATTRIBUTES = %i[block deadline log_pipe result_pipe world].freeze
|
22
36
|
|
23
37
|
# Pipe abstraction
|
24
38
|
class Pipe
|
@@ -50,7 +64,6 @@ module Mutant
|
|
50
64
|
end
|
51
65
|
end # Pipe
|
52
66
|
|
53
|
-
# ignore :reek:InstanceVariableAssumption
|
54
67
|
class Parent
|
55
68
|
include(
|
56
69
|
Anima.new(*ATTRIBUTES),
|
@@ -64,15 +77,28 @@ module Mutant
|
|
64
77
|
#
|
65
78
|
# @return [Result]
|
66
79
|
def call
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
@
|
80
|
+
@exception = nil
|
81
|
+
@log_fragments = []
|
82
|
+
@timeout = nil
|
83
|
+
@value = nil
|
84
|
+
@pid = start_child
|
85
|
+
|
86
|
+
read_child_result
|
87
|
+
result
|
72
88
|
end
|
73
89
|
|
74
90
|
private
|
75
91
|
|
92
|
+
def result
|
93
|
+
Result.new(
|
94
|
+
exception: @exception,
|
95
|
+
log: @log_fragments.join,
|
96
|
+
process_status: @process_status,
|
97
|
+
timeout: @timeout,
|
98
|
+
value: @value
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
76
102
|
def start_child
|
77
103
|
world.process.fork do
|
78
104
|
Child.call(
|
@@ -85,30 +111,43 @@ module Mutant
|
|
85
111
|
end
|
86
112
|
|
87
113
|
# rubocop:disable Metrics/MethodLength
|
88
|
-
def read_child_result
|
114
|
+
def read_child_result
|
89
115
|
result_fragments = []
|
90
|
-
log_fragments = []
|
91
116
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
117
|
+
targets =
|
118
|
+
{
|
119
|
+
log_pipe.parent => @log_fragments,
|
120
|
+
result_pipe.parent => result_fragments
|
121
|
+
}
|
96
122
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
123
|
+
read_targets(targets)
|
124
|
+
|
125
|
+
if targets.empty?
|
126
|
+
load_result(result_fragments)
|
127
|
+
terminate_graceful
|
101
128
|
else
|
102
|
-
|
129
|
+
@timeout = deadline.allowed_time
|
130
|
+
terminate_ungraceful
|
103
131
|
end
|
104
|
-
ensure
|
105
|
-
wait_child(pid, log_fragments)
|
106
132
|
end
|
107
133
|
# rubocop:enable Metrics/MethodLength
|
108
134
|
|
109
|
-
def
|
135
|
+
def load_result(result_fragments)
|
136
|
+
@value = world.marshal.load(result_fragments.join)
|
137
|
+
rescue ArgumentError => exception
|
138
|
+
@exception = exception
|
139
|
+
end
|
140
|
+
|
141
|
+
# rubocop:disable Metrics/MethodLength
|
142
|
+
def read_targets(targets)
|
110
143
|
until targets.empty?
|
111
|
-
|
144
|
+
status = deadline.status
|
145
|
+
|
146
|
+
break unless status.ok?
|
147
|
+
|
148
|
+
ready, = world.io.select(targets.keys, [], [], status.time_left)
|
149
|
+
|
150
|
+
break unless ready
|
112
151
|
|
113
152
|
ready.each do |fd|
|
114
153
|
if fd.eof?
|
@@ -119,14 +158,42 @@ module Mutant
|
|
119
158
|
end
|
120
159
|
end
|
121
160
|
end
|
161
|
+
# rubocop:enable Metrics/MethodLength
|
122
162
|
|
123
|
-
|
124
|
-
|
163
|
+
# rubocop:disable Metrics/MethodLength
|
164
|
+
def terminate_graceful
|
165
|
+
status = nil
|
166
|
+
|
167
|
+
loop do
|
168
|
+
status = peek_child
|
169
|
+
break if status || deadline.expired?
|
170
|
+
world.kernel.sleep(0.1)
|
171
|
+
end
|
125
172
|
|
126
|
-
|
127
|
-
|
173
|
+
if status
|
174
|
+
handle_status(status)
|
175
|
+
else
|
176
|
+
terminate_ungraceful
|
128
177
|
end
|
129
178
|
end
|
179
|
+
# rubocop:enable Metrics/MethodLength
|
180
|
+
|
181
|
+
def terminate_ungraceful
|
182
|
+
world.process.kill('KILL', @pid)
|
183
|
+
|
184
|
+
_pid, status = world.process.wait2(@pid)
|
185
|
+
|
186
|
+
handle_status(status)
|
187
|
+
end
|
188
|
+
|
189
|
+
def handle_status(status)
|
190
|
+
@process_status = status
|
191
|
+
end
|
192
|
+
|
193
|
+
def peek_child
|
194
|
+
_pid, status = world.process.wait2(@pid, Process::WNOHANG)
|
195
|
+
status
|
196
|
+
end
|
130
197
|
|
131
198
|
def add_result(result)
|
132
199
|
@result = defined?(@result) ? @result.add_error(result) : result
|
@@ -157,17 +224,16 @@ module Mutant
|
|
157
224
|
# Call block in isolation
|
158
225
|
#
|
159
226
|
# @return [Result]
|
160
|
-
# execution result
|
161
|
-
#
|
162
|
-
# ignore :reek:NestedIterators
|
163
227
|
#
|
164
228
|
# rubocop:disable Metrics/MethodLength
|
165
|
-
def call(&block)
|
229
|
+
def call(timeout, &block)
|
230
|
+
deadline = world.deadline(timeout)
|
166
231
|
io = world.io
|
167
232
|
Pipe.with(io) do |result|
|
168
233
|
Pipe.with(io) do |log|
|
169
234
|
Parent.call(
|
170
235
|
block: block,
|
236
|
+
deadline: deadline,
|
171
237
|
log_pipe: log,
|
172
238
|
result_pipe: result,
|
173
239
|
world: world
|
@@ -176,6 +242,7 @@ module Mutant
|
|
176
242
|
end
|
177
243
|
end
|
178
244
|
# rubocop:enable Metrics/MethodLength
|
245
|
+
|
179
246
|
end # Fork
|
180
247
|
end # Isolation
|
181
248
|
end # Mutant
|
@@ -12,12 +12,25 @@ module Mutant
|
|
12
12
|
#
|
13
13
|
# @return [Result]
|
14
14
|
#
|
15
|
-
#
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
15
|
+
# rubocop:disable Lint/SuppressedException
|
16
|
+
# rubocop:disable Metrics/MethodLength
|
17
|
+
# ^^ it actually isn not suppressed, it assigns an lvar
|
18
|
+
def call(_timeout)
|
19
|
+
begin
|
20
|
+
value = yield
|
21
|
+
rescue => exception
|
22
|
+
end
|
23
|
+
|
24
|
+
Result.new(
|
25
|
+
exception: exception,
|
26
|
+
log: '',
|
27
|
+
process_status: nil,
|
28
|
+
timeout: nil,
|
29
|
+
value: value
|
30
|
+
)
|
20
31
|
end
|
32
|
+
# rubocop:enable Lint/SuppressedException
|
33
|
+
# rubocop:enable Metrics/MethodLength
|
21
34
|
|
22
35
|
end # None
|
23
36
|
end # Isolation
|
@@ -12,7 +12,7 @@ module Mutant
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def self.from_json(value)
|
15
|
-
new(value.fetch('authors').map(&Author.
|
15
|
+
new(value.fetch('authors').map(&Author.public_method(:new)).to_set)
|
16
16
|
end
|
17
17
|
|
18
18
|
def apply(world)
|
@@ -39,12 +39,11 @@ module Mutant
|
|
39
39
|
capture(world, %w[git show --quiet --pretty=format:%ae])
|
40
40
|
end
|
41
41
|
|
42
|
-
# ignore :reek:UtilityFunction
|
43
42
|
def capture(world, command)
|
44
43
|
world
|
45
44
|
.capture_stdout(command)
|
46
45
|
.fmap(&:chomp)
|
47
|
-
.fmap(&Author.
|
46
|
+
.fmap(&Author.public_method(:new))
|
48
47
|
.fmap { |value| Set.new([value]) }
|
49
48
|
.from_right { Set.new }
|
50
49
|
end
|
@@ -46,6 +46,7 @@ module Mutant
|
|
46
46
|
value
|
47
47
|
.fetch('repositories')
|
48
48
|
.map(&Repository.public_method(:parse))
|
49
|
+
.to_set
|
49
50
|
)
|
50
51
|
end
|
51
52
|
|
@@ -59,14 +60,13 @@ module Mutant
|
|
59
60
|
private
|
60
61
|
|
61
62
|
def check_subscription(actual)
|
62
|
-
if (licensed
|
63
|
+
if (licensed & actual).any?
|
63
64
|
success
|
64
65
|
else
|
65
66
|
failure(licensed, actual)
|
66
67
|
end
|
67
68
|
end
|
68
69
|
|
69
|
-
# ignore :reek:UtilityFunction
|
70
70
|
def parse_remotes(input)
|
71
71
|
input.lines.map(&Repository.method(:parse_remote)).to_set
|
72
72
|
end
|
@@ -44,6 +44,19 @@ module Mutant
|
|
44
44
|
with(attribute => public_send(attribute) + [value])
|
45
45
|
end
|
46
46
|
|
47
|
+
# Merge with other config
|
48
|
+
#
|
49
|
+
# @param [Config] other
|
50
|
+
#
|
51
|
+
# @return [Config]
|
52
|
+
def merge(other)
|
53
|
+
self.class.new(
|
54
|
+
to_h
|
55
|
+
.map { |name, value| [name, value + other.public_send(name)] }
|
56
|
+
.to_h
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
47
60
|
private
|
48
61
|
|
49
62
|
def present_attributes
|