mutant 0.15.1 → 0.16.2
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/pattern/lexer.rb +119 -47
- data/lib/mutant/cli/command/root.rb +1 -1
- data/lib/mutant/cli/command/session.rb +281 -0
- data/lib/mutant/cli/command.rb +16 -2
- data/lib/mutant/config.rb +1 -1
- data/lib/mutant/expression/method.rb +0 -2
- data/lib/mutant/expression/methods.rb +0 -2
- data/lib/mutant/expression/namespace.rb +0 -2
- data/lib/mutant/integration/null.rb +1 -1
- data/lib/mutant/isolation/fork.rb +3 -7
- data/lib/mutant/isolation/none.rb +1 -1
- data/lib/mutant/isolation.rb +31 -0
- data/lib/mutant/log_capture.rb +89 -0
- data/lib/mutant/matcher/null.rb +1 -1
- data/lib/mutant/mutation/runner/sink.rb +23 -10
- data/lib/mutant/mutation/runner.rb +1 -0
- data/lib/mutant/mutation.rb +3 -20
- data/lib/mutant/mutator/node/literal/integer.rb +61 -0
- data/lib/mutant/parallel/connection.rb +2 -4
- data/lib/mutant/parallel/driver.rb +0 -2
- data/lib/mutant/reporter/cli/printer/alive_results.rb +27 -0
- data/lib/mutant/reporter/cli/printer/env_result.rb +52 -9
- data/lib/mutant/reporter/cli/printer/isolation_result.rb +3 -6
- data/lib/mutant/reporter/cli/printer/subject_result.rb +103 -5
- data/lib/mutant/reporter/cli/printer/test.rb +1 -1
- data/lib/mutant/reporter/cli/printer.rb +24 -1
- data/lib/mutant/repository/diff.rb +1 -2
- data/lib/mutant/result/exception.rb +29 -0
- data/lib/mutant/result/json_writer.rb +43 -0
- data/lib/mutant/result/process_status.rb +37 -0
- data/lib/mutant/result/session.rb +63 -0
- data/lib/mutant/result/test.rb +57 -0
- data/lib/mutant/result.rb +201 -96
- data/lib/mutant/segment/recorder.rb +0 -2
- data/lib/mutant/test/runner/sink.rb +1 -1
- data/lib/mutant/timer.rb +3 -1
- data/lib/mutant/transform/codec.rb +45 -0
- data/lib/mutant/transform.rb +33 -25
- data/lib/mutant/world.rb +6 -0
- data/lib/mutant/zombifier.rb +0 -2
- data/lib/mutant.rb +14 -4
- metadata +34 -7
- data/lib/mutant/reporter/cli/printer/coverage_result.rb +0 -19
- data/lib/mutant/reporter/cli/printer/mutation_result.rb +0 -84
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mutant
|
|
4
|
+
module Result
|
|
5
|
+
# Serializable exception data
|
|
6
|
+
class Exception
|
|
7
|
+
include Anima.new(
|
|
8
|
+
:backtrace,
|
|
9
|
+
:message,
|
|
10
|
+
:original_class
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
# Build from a Ruby exception
|
|
14
|
+
#
|
|
15
|
+
# @param [::Exception] exception
|
|
16
|
+
#
|
|
17
|
+
# @return [Exception]
|
|
18
|
+
def self.from_exception(exception)
|
|
19
|
+
new(
|
|
20
|
+
backtrace: exception.backtrace,
|
|
21
|
+
message: exception.message,
|
|
22
|
+
original_class: exception.class.name
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
CODEC = Transform::Codec.for_anima(self)
|
|
27
|
+
end # Exception
|
|
28
|
+
end # Result
|
|
29
|
+
end # Mutant
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mutant
|
|
4
|
+
module Result
|
|
5
|
+
# Write result JSON to .mutant/results/
|
|
6
|
+
class JSONWriter
|
|
7
|
+
include Anima.new(:env, :result)
|
|
8
|
+
|
|
9
|
+
RESULTS_DIR = '.mutant/results'
|
|
10
|
+
|
|
11
|
+
# Write result JSON file
|
|
12
|
+
#
|
|
13
|
+
# @return [Pathname]
|
|
14
|
+
def call
|
|
15
|
+
dir = env.world.pathname.new(RESULTS_DIR)
|
|
16
|
+
dir.mkpath
|
|
17
|
+
|
|
18
|
+
path = dir.join("#{SESSION_ID}.json")
|
|
19
|
+
path.write(json)
|
|
20
|
+
|
|
21
|
+
path
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def json
|
|
27
|
+
JSON.generate(Session::CODEC.dump(session).from_right)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def session
|
|
31
|
+
Session.new(
|
|
32
|
+
killtime: result.killtime,
|
|
33
|
+
mutant_version: VERSION,
|
|
34
|
+
pid: env.world.process.pid,
|
|
35
|
+
ruby_version: RUBY_VERSION,
|
|
36
|
+
runtime: result.runtime,
|
|
37
|
+
session_id: SESSION_ID,
|
|
38
|
+
subject_results: result.subject_results
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end # JSONWriter
|
|
42
|
+
end # Result
|
|
43
|
+
end # Mutant
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mutant
|
|
4
|
+
module Result
|
|
5
|
+
# Serializable process status
|
|
6
|
+
#
|
|
7
|
+
# Replaces Process::Status in the result tree with a
|
|
8
|
+
# round-trippable value object.
|
|
9
|
+
class ProcessStatus
|
|
10
|
+
include Anima.new(:exitstatus)
|
|
11
|
+
|
|
12
|
+
# Build from a Process::Status object
|
|
13
|
+
#
|
|
14
|
+
# @param [Process::Status] process_status
|
|
15
|
+
#
|
|
16
|
+
# @return [ProcessStatus]
|
|
17
|
+
def self.from_process_status(process_status)
|
|
18
|
+
new(exitstatus: process_status.exitstatus)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Stable inspect without memory address for use in user-facing output
|
|
22
|
+
#
|
|
23
|
+
# @return [String]
|
|
24
|
+
def inspect
|
|
25
|
+
"#<#{self.class.name} exitstatus=#{exitstatus}>"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Test for successful exit
|
|
29
|
+
#
|
|
30
|
+
# @return [Boolean]
|
|
31
|
+
def success?
|
|
32
|
+
exitstatus.equal?(0)
|
|
33
|
+
end
|
|
34
|
+
CODEC = Transform::Codec.for_anima(self)
|
|
35
|
+
end # ProcessStatus
|
|
36
|
+
end # Result
|
|
37
|
+
end # Mutant
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mutant
|
|
4
|
+
module Result
|
|
5
|
+
# Top-level result object containing session metadata and subject results
|
|
6
|
+
class Session
|
|
7
|
+
# Extract timestamp from UUIDv7 session_id
|
|
8
|
+
module Timestamp
|
|
9
|
+
# @return [Time]
|
|
10
|
+
def timestamp
|
|
11
|
+
ms = session_id.delete('-')[0, 12].to_i(16)
|
|
12
|
+
Time.at(ms / 1000.0).utc
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
include Timestamp, Anima.new(
|
|
17
|
+
:killtime,
|
|
18
|
+
:mutant_version,
|
|
19
|
+
:pid,
|
|
20
|
+
:ruby_version,
|
|
21
|
+
:runtime,
|
|
22
|
+
:session_id,
|
|
23
|
+
:subject_results
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
dump = Transform::Success.new(
|
|
27
|
+
block: lambda do |object|
|
|
28
|
+
{
|
|
29
|
+
'killtime' => object.killtime,
|
|
30
|
+
'mutant_version' => object.mutant_version,
|
|
31
|
+
'pid' => object.pid,
|
|
32
|
+
'ruby_version' => object.ruby_version,
|
|
33
|
+
'runtime' => object.runtime,
|
|
34
|
+
'session_id' => object.session_id,
|
|
35
|
+
'subject_results' => object.subject_results.map { |subject_result| Subject::CODEC.dump(subject_result).from_right }
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
load = Transform::Sequence.new(
|
|
41
|
+
steps: [
|
|
42
|
+
Transform::Hash.new(
|
|
43
|
+
required: [
|
|
44
|
+
Transform::Hash::Key.new(value: 'killtime', transform: Transform::FLOAT),
|
|
45
|
+
Transform::Hash::Key.new(value: 'mutant_version', transform: Transform::STRING),
|
|
46
|
+
Transform::Hash::Key.new(value: 'pid', transform: Transform::INTEGER),
|
|
47
|
+
Transform::Hash::Key.new(value: 'ruby_version', transform: Transform::STRING),
|
|
48
|
+
Transform::Hash::Key.new(value: 'runtime', transform: Transform::FLOAT),
|
|
49
|
+
Transform::Hash::Key.new(value: 'session_id', transform: Transform::STRING),
|
|
50
|
+
Transform::Hash::Key.new(value: 'subject_results', transform: Transform::Array.new(transform: Subject::CODEC.load_transform))
|
|
51
|
+
],
|
|
52
|
+
optional: []
|
|
53
|
+
),
|
|
54
|
+
Transform::Hash::Symbolize.new,
|
|
55
|
+
Transform::Success.new(block: method(:new).to_proc)
|
|
56
|
+
]
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
CODEC = Transform::Codec.new(dump_transform: dump, load_transform: load)
|
|
60
|
+
|
|
61
|
+
end # Session
|
|
62
|
+
end # Result
|
|
63
|
+
end # Mutant
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mutant
|
|
4
|
+
module Result
|
|
5
|
+
# Test result
|
|
6
|
+
class Test
|
|
7
|
+
include Anima.new(:job_index, :passed, :runtime, :output)
|
|
8
|
+
|
|
9
|
+
alias_method :success?, :passed
|
|
10
|
+
|
|
11
|
+
class VoidValue < self
|
|
12
|
+
include Singleton
|
|
13
|
+
|
|
14
|
+
# Initialize object
|
|
15
|
+
#
|
|
16
|
+
# @return [undefined]
|
|
17
|
+
def initialize
|
|
18
|
+
super(
|
|
19
|
+
job_index: nil,
|
|
20
|
+
output: LogCapture.from_binary(+''),
|
|
21
|
+
passed: false,
|
|
22
|
+
runtime: 0.0
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end # VoidValue
|
|
26
|
+
|
|
27
|
+
dump = Transform::Success.new(
|
|
28
|
+
block: lambda do |object|
|
|
29
|
+
{
|
|
30
|
+
'job_index' => object.job_index,
|
|
31
|
+
'output' => LogCapture::CODEC.dump(object.output).from_right,
|
|
32
|
+
'passed' => object.passed,
|
|
33
|
+
'runtime' => object.runtime
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
load = Transform::Sequence.new(
|
|
39
|
+
steps: [
|
|
40
|
+
Transform::Hash.new(
|
|
41
|
+
required: [
|
|
42
|
+
Transform::Hash::Key.new(value: 'job_index', transform: Transform::Nullable.new(transform: Transform::INTEGER)),
|
|
43
|
+
Transform::Hash::Key.new(value: 'output', transform: LogCapture::CODEC.load_transform),
|
|
44
|
+
Transform::Hash::Key.new(value: 'passed', transform: Transform::BOOLEAN),
|
|
45
|
+
Transform::Hash::Key.new(value: 'runtime', transform: Transform::FLOAT)
|
|
46
|
+
],
|
|
47
|
+
optional: []
|
|
48
|
+
),
|
|
49
|
+
Transform::Hash::Symbolize.new,
|
|
50
|
+
Transform::Success.new(block: method(:new).to_proc)
|
|
51
|
+
]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
CODEC = Transform::Codec.new(dump_transform: dump, load_transform: load)
|
|
55
|
+
end # Test
|
|
56
|
+
end # Result
|
|
57
|
+
end # Mutant
|
data/lib/mutant/result.rb
CHANGED
|
@@ -48,7 +48,7 @@ module Mutant
|
|
|
48
48
|
end
|
|
49
49
|
end # ClassMethods
|
|
50
50
|
|
|
51
|
-
private_constant(
|
|
51
|
+
private_constant(:CoverageMetric, :ClassMethods)
|
|
52
52
|
|
|
53
53
|
# Hook called when module gets included
|
|
54
54
|
#
|
|
@@ -140,98 +140,6 @@ module Mutant
|
|
|
140
140
|
def amount_tests_success = test_results.count(&:passed)
|
|
141
141
|
end # TestEnv
|
|
142
142
|
|
|
143
|
-
# Test result
|
|
144
|
-
class Test
|
|
145
|
-
include Anima.new(:job_index, :passed, :runtime, :output)
|
|
146
|
-
|
|
147
|
-
alias_method :success?, :passed
|
|
148
|
-
|
|
149
|
-
class VoidValue < self
|
|
150
|
-
include Singleton
|
|
151
|
-
|
|
152
|
-
# Initialize object
|
|
153
|
-
#
|
|
154
|
-
# @return [undefined]
|
|
155
|
-
def initialize
|
|
156
|
-
super(
|
|
157
|
-
job_index: nil,
|
|
158
|
-
output: '',
|
|
159
|
-
passed: false,
|
|
160
|
-
runtime: 0.0
|
|
161
|
-
)
|
|
162
|
-
end
|
|
163
|
-
end # VoidValue
|
|
164
|
-
end # Test
|
|
165
|
-
|
|
166
|
-
# Subject result
|
|
167
|
-
class Subject
|
|
168
|
-
include CoverageMetric, Result, Anima.new(
|
|
169
|
-
:coverage_results,
|
|
170
|
-
:subject,
|
|
171
|
-
:tests
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
sum :killtime, :coverage_results
|
|
175
|
-
sum :runtime, :coverage_results
|
|
176
|
-
|
|
177
|
-
# Test if subject was processed successful
|
|
178
|
-
#
|
|
179
|
-
# @return [Boolean]
|
|
180
|
-
def success?
|
|
181
|
-
uncovered_results.empty?
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# Alive mutations
|
|
185
|
-
#
|
|
186
|
-
# @return [Array<Result::Coverage>]
|
|
187
|
-
def uncovered_results = coverage_results.reject(&:success?)
|
|
188
|
-
memoize :uncovered_results
|
|
189
|
-
|
|
190
|
-
# Amount of mutations
|
|
191
|
-
#
|
|
192
|
-
# @return [Integer]
|
|
193
|
-
def amount_mutation_results = coverage_results.length
|
|
194
|
-
|
|
195
|
-
# Amount of mutations
|
|
196
|
-
#
|
|
197
|
-
# @return [Integer]
|
|
198
|
-
def amount_timeouts = coverage_results.count(&:timeout?)
|
|
199
|
-
|
|
200
|
-
# Amount of mutations
|
|
201
|
-
#
|
|
202
|
-
# @return [Integer]
|
|
203
|
-
def amount_mutations = subject.mutations.length
|
|
204
|
-
|
|
205
|
-
# Number of killed mutations
|
|
206
|
-
#
|
|
207
|
-
# @return [Integer]
|
|
208
|
-
def amount_mutations_killed = covered_results.length
|
|
209
|
-
|
|
210
|
-
# Number of alive mutations
|
|
211
|
-
#
|
|
212
|
-
# @return [Integer]
|
|
213
|
-
def amount_mutations_alive = uncovered_results.length
|
|
214
|
-
|
|
215
|
-
private
|
|
216
|
-
|
|
217
|
-
def covered_results = coverage_results.select(&:success?)
|
|
218
|
-
memoize :covered_results
|
|
219
|
-
|
|
220
|
-
end # Subject
|
|
221
|
-
|
|
222
|
-
# Coverage of a mutation against criteria
|
|
223
|
-
class Coverage
|
|
224
|
-
include Result, Anima.new(
|
|
225
|
-
:mutation_result,
|
|
226
|
-
:criteria_result
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
delegate :killtime, :mutation_result
|
|
230
|
-
delegate :runtime, :mutation_result
|
|
231
|
-
delegate :success?, :criteria_result
|
|
232
|
-
delegate :timeout?, :mutation_result
|
|
233
|
-
end # Coverage
|
|
234
|
-
|
|
235
143
|
class CoverageCriteria
|
|
236
144
|
include Result, Anima.new(*Config::CoverageCriteria.anima.attribute_names)
|
|
237
145
|
|
|
@@ -241,6 +149,8 @@ module Mutant
|
|
|
241
149
|
def success?
|
|
242
150
|
process_abort || test_result || timeout
|
|
243
151
|
end
|
|
152
|
+
|
|
153
|
+
CODEC = Transform::Codec.for_anima(self)
|
|
244
154
|
end
|
|
245
155
|
|
|
246
156
|
class MutationIndex
|
|
@@ -255,13 +165,25 @@ module Mutant
|
|
|
255
165
|
class Mutation
|
|
256
166
|
include Result, Anima.new(
|
|
257
167
|
:isolation_result,
|
|
258
|
-
:
|
|
168
|
+
:mutation_diff,
|
|
169
|
+
:mutation_identification,
|
|
170
|
+
:mutation_node,
|
|
171
|
+
:mutation_source,
|
|
172
|
+
:mutation_type,
|
|
259
173
|
:runtime
|
|
260
174
|
)
|
|
261
175
|
|
|
176
|
+
TEST_PASS_SUCCESS = {
|
|
177
|
+
'evil' => false,
|
|
178
|
+
'neutral' => true,
|
|
179
|
+
'noop' => true
|
|
180
|
+
}.freeze
|
|
181
|
+
|
|
182
|
+
private_constant(:TEST_PASS_SUCCESS)
|
|
183
|
+
|
|
262
184
|
# Create mutation criteria results
|
|
263
185
|
#
|
|
264
|
-
# @
|
|
186
|
+
# @param [Result::CoverageCriteria]
|
|
265
187
|
def criteria_result(coverage_criteria)
|
|
266
188
|
CoverageCriteria.new(
|
|
267
189
|
process_abort: coverage_criteria.process_abort && process_abort?,
|
|
@@ -297,10 +219,193 @@ module Mutant
|
|
|
297
219
|
#
|
|
298
220
|
# @return [Boolean]
|
|
299
221
|
def test_result_success?
|
|
300
|
-
isolation_result.valid_value? &&
|
|
222
|
+
isolation_result.valid_value? && TEST_PASS_SUCCESS.fetch(mutation_type).equal?(isolation_result.value.passed)
|
|
301
223
|
end
|
|
302
224
|
memoize :test_result_success?
|
|
303
225
|
|
|
226
|
+
dump = Transform::Success.new(
|
|
227
|
+
block: lambda do |object|
|
|
228
|
+
{
|
|
229
|
+
'isolation_result' => Isolation::Result::CODEC.dump(object.isolation_result).from_right,
|
|
230
|
+
'mutation_diff' => object.mutation_diff,
|
|
231
|
+
'mutation_identification' => object.mutation_identification,
|
|
232
|
+
'mutation_source' => object.mutation_source,
|
|
233
|
+
'mutation_type' => object.mutation_type,
|
|
234
|
+
'runtime' => object.runtime
|
|
235
|
+
}
|
|
236
|
+
end
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
derive_mutation_node = Transform::Success.new(
|
|
240
|
+
block: ->(hash) { hash.merge('mutation_node' => Unparser.parse(hash.fetch('mutation_source'))) }
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
load = Transform::Sequence.new(
|
|
244
|
+
steps: [
|
|
245
|
+
Transform::Hash.new(
|
|
246
|
+
required: [
|
|
247
|
+
Transform::Hash::Key.new(value: 'isolation_result', transform: Isolation::Result::CODEC.load_transform),
|
|
248
|
+
Transform::Hash::Key.new(value: 'mutation_diff', transform: Transform::OPTIONAL_STRING),
|
|
249
|
+
Transform::Hash::Key.new(value: 'mutation_identification', transform: Transform::STRING),
|
|
250
|
+
Transform::Hash::Key.new(value: 'mutation_source', transform: Transform::STRING),
|
|
251
|
+
Transform::Hash::Key.new(value: 'mutation_type', transform: Transform::STRING),
|
|
252
|
+
Transform::Hash::Key.new(value: 'runtime', transform: Transform::FLOAT)
|
|
253
|
+
],
|
|
254
|
+
optional: []
|
|
255
|
+
),
|
|
256
|
+
derive_mutation_node,
|
|
257
|
+
Transform::Hash::Symbolize.new,
|
|
258
|
+
Transform::Success.new(block: method(:new).to_proc)
|
|
259
|
+
]
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
CODEC = Transform::Codec.new(dump_transform: dump, load_transform: load)
|
|
304
263
|
end # Mutation
|
|
264
|
+
|
|
265
|
+
# Coverage of a mutation against criteria
|
|
266
|
+
class Coverage
|
|
267
|
+
include Result, Anima.new(
|
|
268
|
+
:mutation_result,
|
|
269
|
+
:criteria_result
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
delegate :killtime, :mutation_result
|
|
273
|
+
delegate :runtime, :mutation_result
|
|
274
|
+
delegate :success?, :criteria_result
|
|
275
|
+
delegate :timeout?, :mutation_result
|
|
276
|
+
|
|
277
|
+
dump = Transform::Success.new(
|
|
278
|
+
block: lambda do |object|
|
|
279
|
+
{
|
|
280
|
+
'mutation_result' => Mutation::CODEC.dump(object.mutation_result).from_right,
|
|
281
|
+
'criteria_result' => CoverageCriteria::CODEC.dump(object.criteria_result).from_right
|
|
282
|
+
}
|
|
283
|
+
end
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
load = Transform::Sequence.new(
|
|
287
|
+
steps: [
|
|
288
|
+
Transform::Hash.new(
|
|
289
|
+
required: [
|
|
290
|
+
Transform::Hash::Key.new(value: 'mutation_result', transform: Mutation::CODEC.load_transform),
|
|
291
|
+
Transform::Hash::Key.new(value: 'criteria_result', transform: CoverageCriteria::CODEC.load_transform)
|
|
292
|
+
],
|
|
293
|
+
optional: []
|
|
294
|
+
),
|
|
295
|
+
Transform::Hash::Symbolize.new,
|
|
296
|
+
Transform::Success.new(block: method(:new).to_proc)
|
|
297
|
+
]
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
CODEC = Transform::Codec.new(dump_transform: dump, load_transform: load)
|
|
301
|
+
end # Coverage
|
|
302
|
+
|
|
303
|
+
# Subject result
|
|
304
|
+
class Subject
|
|
305
|
+
include CoverageMetric, Result, Anima.new(
|
|
306
|
+
:amount_mutations,
|
|
307
|
+
:coverage_results,
|
|
308
|
+
:expression_syntax,
|
|
309
|
+
:identification,
|
|
310
|
+
:node,
|
|
311
|
+
:source,
|
|
312
|
+
:source_path,
|
|
313
|
+
:tests
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
sum :killtime, :coverage_results
|
|
317
|
+
sum :runtime, :coverage_results
|
|
318
|
+
|
|
319
|
+
# Test if subject was processed successful
|
|
320
|
+
#
|
|
321
|
+
# @return [Boolean]
|
|
322
|
+
def success?
|
|
323
|
+
uncovered_results.empty?
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Alive mutations
|
|
327
|
+
#
|
|
328
|
+
# @return [Array<Result::Coverage>]
|
|
329
|
+
def uncovered_results = coverage_results.reject(&:success?)
|
|
330
|
+
memoize :uncovered_results
|
|
331
|
+
|
|
332
|
+
# Amount of mutations
|
|
333
|
+
#
|
|
334
|
+
# @return [Integer]
|
|
335
|
+
def amount_mutation_results = coverage_results.length
|
|
336
|
+
|
|
337
|
+
# Amount of mutations
|
|
338
|
+
#
|
|
339
|
+
# @return [Integer]
|
|
340
|
+
def amount_timeouts = coverage_results.count(&:timeout?)
|
|
341
|
+
|
|
342
|
+
# Number of killed mutations
|
|
343
|
+
#
|
|
344
|
+
# @return [Integer]
|
|
345
|
+
def amount_mutations_killed = covered_results.length
|
|
346
|
+
|
|
347
|
+
# Number of alive mutations
|
|
348
|
+
#
|
|
349
|
+
# @return [Integer]
|
|
350
|
+
def amount_mutations_alive = uncovered_results.length
|
|
351
|
+
|
|
352
|
+
private
|
|
353
|
+
|
|
354
|
+
def covered_results = coverage_results.select(&:success?)
|
|
355
|
+
memoize :covered_results
|
|
356
|
+
|
|
357
|
+
dump = Transform::Success.new(
|
|
358
|
+
block: lambda do |object|
|
|
359
|
+
{
|
|
360
|
+
'amount_mutations' => object.amount_mutations,
|
|
361
|
+
'coverage_results' => object.coverage_results
|
|
362
|
+
.map { |coverage_result| Coverage::CODEC.dump(coverage_result).from_right },
|
|
363
|
+
'expression_syntax' => object.expression_syntax,
|
|
364
|
+
'identification' => object.identification,
|
|
365
|
+
'source' => object.source,
|
|
366
|
+
'source_path' => object.source_path,
|
|
367
|
+
'tests' => object.tests.map(&:identification)
|
|
368
|
+
}
|
|
369
|
+
end
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
Transform::Block.capture(:parse_node) do |source|
|
|
373
|
+
Either.wrap_error(::Parser::SyntaxError) { Unparser.parse(source) }
|
|
374
|
+
.lmap(&:message)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
load_test = Transform::Success.new(
|
|
378
|
+
block: ->(id) { Mutant::Test.new(expressions: EMPTY_ARRAY, id:) }
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
derive_node = Transform::Success.new(
|
|
382
|
+
block: ->(hash) { hash.merge('node' => Unparser.parse(hash.fetch('source'))) }
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
load = Transform::Sequence.new(
|
|
386
|
+
steps: [
|
|
387
|
+
Transform::Hash.new(
|
|
388
|
+
required: [
|
|
389
|
+
Transform::Hash::Key.new(value: 'amount_mutations', transform: Transform::INTEGER),
|
|
390
|
+
Transform::Hash::Key.new(value: 'coverage_results', transform: Transform::Array.new(transform: Coverage::CODEC.load_transform)),
|
|
391
|
+
Transform::Hash::Key.new(value: 'expression_syntax', transform: Transform::STRING),
|
|
392
|
+
Transform::Hash::Key.new(value: 'identification', transform: Transform::STRING),
|
|
393
|
+
Transform::Hash::Key.new(value: 'source', transform: Transform::STRING),
|
|
394
|
+
Transform::Hash::Key.new(value: 'source_path', transform: Transform::STRING),
|
|
395
|
+
Transform::Hash::Key.new(
|
|
396
|
+
value: 'tests',
|
|
397
|
+
transform: Transform::Array.new(transform: load_test)
|
|
398
|
+
)
|
|
399
|
+
],
|
|
400
|
+
optional: []
|
|
401
|
+
),
|
|
402
|
+
derive_node,
|
|
403
|
+
Transform::Hash::Symbolize.new,
|
|
404
|
+
Transform::Success.new(block: method(:new).to_proc)
|
|
405
|
+
]
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
CODEC = Transform::Codec.new(dump_transform: dump, load_transform: load)
|
|
409
|
+
end # Subject
|
|
305
410
|
end # Result
|
|
306
411
|
end # Mutant
|
data/lib/mutant/timer.rb
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mutant
|
|
4
|
+
class Transform
|
|
5
|
+
# Bidirectional codec over Ruby objects.
|
|
6
|
+
#
|
|
7
|
+
# Wraps a pair of dump/load transforms that convert between domain
|
|
8
|
+
# objects and a JSON-compatible Ruby structure (hashes, arrays,
|
|
9
|
+
# primitives). The outer layer that converts the structure to/from
|
|
10
|
+
# a JSON string is handled by callers as needed.
|
|
11
|
+
class Codec
|
|
12
|
+
include Anima.new(:dump_transform, :load_transform)
|
|
13
|
+
|
|
14
|
+
# Build a codec for simple Anima objects with primitive fields
|
|
15
|
+
#
|
|
16
|
+
# @param [Class] klass
|
|
17
|
+
#
|
|
18
|
+
# @return [Codec]
|
|
19
|
+
def self.for_anima(klass)
|
|
20
|
+
new(
|
|
21
|
+
dump_transform: Success.new(block: ->(object) { object.to_h.transform_keys(&:to_s) }),
|
|
22
|
+
load_transform: Success.new(block: ->(hash) { klass.new(**hash.transform_keys(&:to_sym)) })
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Dump object to Ruby structure
|
|
27
|
+
#
|
|
28
|
+
# @param [Object] object
|
|
29
|
+
#
|
|
30
|
+
# @return [Either<Error, Object>]
|
|
31
|
+
def dump(object)
|
|
32
|
+
dump_transform.call(object)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Load object from Ruby structure
|
|
36
|
+
#
|
|
37
|
+
# @param [Object] input
|
|
38
|
+
#
|
|
39
|
+
# @return [Either<Error, Object>]
|
|
40
|
+
def load(input)
|
|
41
|
+
load_transform.call(input)
|
|
42
|
+
end
|
|
43
|
+
end # Codec
|
|
44
|
+
end # Transform
|
|
45
|
+
end # Mutant
|