mutant 0.10.4 → 0.10.9
Sign up to get free protection for your applications and to get access to all the features.
- 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 +80 -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/commercial.rb +2 -3
- data/lib/mutant/license/subscription/opensource.rb +0 -1
- 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 +91 -30
- data/lib/mutant/runner/sink.rb +12 -5
- 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 +12 -13
- 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
|
@@ -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
|