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
data/lib/mutant/isolation.rb
CHANGED
|
@@ -21,6 +21,37 @@ module Mutant
|
|
|
21
21
|
def valid_value?
|
|
22
22
|
timeout.nil? && exception.nil? && (process_status.nil? || process_status.success?)
|
|
23
23
|
end
|
|
24
|
+
|
|
25
|
+
dump = Transform::Success.new(
|
|
26
|
+
block: lambda do |object|
|
|
27
|
+
{
|
|
28
|
+
'exception' => object.exception && Mutant::Result::Exception::CODEC.dump(object.exception).from_right,
|
|
29
|
+
'log' => LogCapture::CODEC.dump(object.log).from_right,
|
|
30
|
+
'process_status' => object.process_status && Mutant::Result::ProcessStatus::CODEC.dump(object.process_status).from_right,
|
|
31
|
+
'timeout' => object.timeout,
|
|
32
|
+
'value' => object.value && Mutant::Result::Test::CODEC.dump(object.value).from_right
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
load = Transform::Sequence.new(
|
|
38
|
+
steps: [
|
|
39
|
+
Transform::Hash.new(
|
|
40
|
+
required: [
|
|
41
|
+
Transform::Hash::Key.new(value: 'exception', transform: Transform::Nullable.new(transform: Mutant::Result::Exception::CODEC.load_transform)),
|
|
42
|
+
Transform::Hash::Key.new(value: 'log', transform: LogCapture::CODEC.load_transform),
|
|
43
|
+
Transform::Hash::Key.new(value: 'process_status', transform: Transform::Nullable.new(transform: Mutant::Result::ProcessStatus::CODEC.load_transform)),
|
|
44
|
+
Transform::Hash::Key.new(value: 'timeout', transform: Transform::Nullable.new(transform: Transform::FLOAT)),
|
|
45
|
+
Transform::Hash::Key.new(value: 'value', transform: Transform::Nullable.new(transform: Mutant::Result::Test::CODEC.load_transform))
|
|
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)
|
|
24
55
|
end # Result
|
|
25
56
|
|
|
26
57
|
# Call block in isolation
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mutant
|
|
4
|
+
# Captured log output from a worker or forked process.
|
|
5
|
+
#
|
|
6
|
+
# Raw worker log bytes may or may not be valid UTF-8 — they commonly contain
|
|
7
|
+
# terminal control sequences emitted by test frameworks. This class preserves
|
|
8
|
+
# those bytes unaltered for terminal emission while distinguishing UTF-8 text
|
|
9
|
+
# from arbitrary bytes in the codec representation.
|
|
10
|
+
class LogCapture
|
|
11
|
+
include AbstractType, Anima.new(:content)
|
|
12
|
+
|
|
13
|
+
# Build the appropriate subclass from raw bytes.
|
|
14
|
+
#
|
|
15
|
+
# Takes ownership of +bytes+; encoding is mutated in place.
|
|
16
|
+
#
|
|
17
|
+
# @param [::String] bytes
|
|
18
|
+
#
|
|
19
|
+
# @return [LogCapture]
|
|
20
|
+
def self.from_binary(bytes)
|
|
21
|
+
if bytes.force_encoding(Encoding::UTF_8).valid_encoding?
|
|
22
|
+
String.new(content: bytes)
|
|
23
|
+
else
|
|
24
|
+
Binary.new(content: bytes.force_encoding(Encoding::ASCII_8BIT))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# UTF-8 valid log capture
|
|
29
|
+
class String < self
|
|
30
|
+
TYPE = 'string'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Non UTF-8 log capture, preserved as raw bytes
|
|
34
|
+
class Binary < self
|
|
35
|
+
TYPE = 'binary'
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
dump = Transform::Success.new(
|
|
39
|
+
block: lambda do |object|
|
|
40
|
+
case object
|
|
41
|
+
when String
|
|
42
|
+
{ 'type' => String::TYPE, 'content' => object.content }
|
|
43
|
+
when Binary
|
|
44
|
+
{ 'type' => Binary::TYPE, 'content' => [object.content].pack('m0') }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Normalize legacy plain-string log representation into the tagged form.
|
|
50
|
+
# Session JSON files written before LogCapture store +log+ and +output+
|
|
51
|
+
# as plain strings; promote those to string-typed captures on load.
|
|
52
|
+
legacy = Transform::Block.capture('log_capture_legacy') do |input|
|
|
53
|
+
case input
|
|
54
|
+
when ::String
|
|
55
|
+
Either::Right.new('type' => String::TYPE, 'content' => input)
|
|
56
|
+
else
|
|
57
|
+
Either::Right.new(input)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
load = Transform::Sequence.new(
|
|
62
|
+
steps: [
|
|
63
|
+
legacy,
|
|
64
|
+
Transform::Hash.new(
|
|
65
|
+
required: [
|
|
66
|
+
Transform::Hash::Key.new(value: 'type', transform: Transform::STRING),
|
|
67
|
+
Transform::Hash::Key.new(value: 'content', transform: Transform::STRING)
|
|
68
|
+
],
|
|
69
|
+
optional: []
|
|
70
|
+
),
|
|
71
|
+
Transform::Block.capture('log_capture') do |hash|
|
|
72
|
+
type = hash.fetch('type')
|
|
73
|
+
content = hash.fetch('content')
|
|
74
|
+
|
|
75
|
+
case type
|
|
76
|
+
when String::TYPE
|
|
77
|
+
Either::Right.new(String.new(content: content))
|
|
78
|
+
when Binary::TYPE
|
|
79
|
+
Either::Right.new(Binary.new(content: content.unpack1('m0').force_encoding(Encoding::ASCII_8BIT)))
|
|
80
|
+
else
|
|
81
|
+
Either::Left.new("Unknown log capture type: #{type.inspect}")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
]
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
CODEC = Transform::Codec.new(dump_transform: dump, load_transform: load)
|
|
88
|
+
end # LogCapture
|
|
89
|
+
end # Mutant
|
data/lib/mutant/matcher/null.rb
CHANGED
|
@@ -40,21 +40,30 @@ module Mutant
|
|
|
40
40
|
# @param [Parallel::Response] response
|
|
41
41
|
#
|
|
42
42
|
# @return [self]
|
|
43
|
+
# rubocop:disable Metrics/AbcSize
|
|
44
|
+
# rubocop:disable Metrics/MethodLength
|
|
43
45
|
def response(response)
|
|
44
46
|
fail response.error if response.error
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
mutation = env.mutations.fetch(response.result.mutation_index)
|
|
49
|
+
subject = mutation.subject
|
|
50
|
+
mutation_result = mutation_result(mutation, response.result)
|
|
49
51
|
|
|
50
52
|
@subject_results[subject] = Result::Subject.new(
|
|
51
|
-
subject
|
|
52
|
-
coverage_results:
|
|
53
|
-
|
|
53
|
+
amount_mutations: subject.mutations.length,
|
|
54
|
+
coverage_results: previous_coverage_results(subject).dup << coverage_result(mutation_result),
|
|
55
|
+
expression_syntax: subject.expression.syntax,
|
|
56
|
+
identification: subject.identification,
|
|
57
|
+
node: subject.node,
|
|
58
|
+
source: subject.source,
|
|
59
|
+
source_path: subject.source_path.to_s,
|
|
60
|
+
tests: env.selections.fetch(subject)
|
|
54
61
|
)
|
|
55
62
|
|
|
56
63
|
self
|
|
57
64
|
end
|
|
65
|
+
# rubocop:enable Metrics/AbcSize
|
|
66
|
+
# rubocop:enable Metrics/MethodLength
|
|
58
67
|
|
|
59
68
|
private
|
|
60
69
|
|
|
@@ -65,11 +74,15 @@ module Mutant
|
|
|
65
74
|
)
|
|
66
75
|
end
|
|
67
76
|
|
|
68
|
-
def mutation_result(mutation_index_result)
|
|
77
|
+
def mutation_result(mutation, mutation_index_result)
|
|
69
78
|
Result::Mutation.new(
|
|
70
|
-
isolation_result:
|
|
71
|
-
mutation
|
|
72
|
-
|
|
79
|
+
isolation_result: mutation_index_result.isolation_result,
|
|
80
|
+
mutation_diff: mutation.diff.diff,
|
|
81
|
+
mutation_identification: mutation.identification,
|
|
82
|
+
mutation_node: mutation.node,
|
|
83
|
+
mutation_source: mutation.source,
|
|
84
|
+
mutation_type: mutation.class::SYMBOL,
|
|
85
|
+
runtime: mutation_index_result.runtime
|
|
73
86
|
)
|
|
74
87
|
end
|
|
75
88
|
|
|
@@ -18,6 +18,7 @@ module Mutant
|
|
|
18
18
|
env
|
|
19
19
|
.record(:analysis) { run_driver(reporter, async_driver(env)) }
|
|
20
20
|
.tap { |result| env.record(:report) { reporter.report(result) } }
|
|
21
|
+
.tap { |result| Result::JSONWriter.new(env:, result:).call }
|
|
21
22
|
end
|
|
22
23
|
private_class_method :run_mutation_analysis
|
|
23
24
|
|
data/lib/mutant/mutation.rb
CHANGED
|
@@ -90,15 +90,6 @@ module Mutant
|
|
|
90
90
|
# @return [String]
|
|
91
91
|
def original_source = subject.source
|
|
92
92
|
|
|
93
|
-
# Test if mutation is killed by test reports
|
|
94
|
-
#
|
|
95
|
-
# @param [Result::Test] test_result
|
|
96
|
-
#
|
|
97
|
-
# @return [Boolean]
|
|
98
|
-
def self.success?(test_result)
|
|
99
|
-
self::TEST_PASS_SUCCESS.equal?(test_result.passed)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
93
|
# Insert mutated node
|
|
103
94
|
#
|
|
104
95
|
# @param kernel [Kernel]
|
|
@@ -126,25 +117,17 @@ module Mutant
|
|
|
126
117
|
|
|
127
118
|
# Evil mutation that should case mutations to fail tests
|
|
128
119
|
class Evil < self
|
|
129
|
-
SYMBOL
|
|
130
|
-
TEST_PASS_SUCCESS = false
|
|
131
|
-
|
|
120
|
+
SYMBOL = 'evil'
|
|
132
121
|
end # Evil
|
|
133
122
|
|
|
134
123
|
# Neutral mutation that should not cause mutations to fail tests
|
|
135
124
|
class Neutral < self
|
|
136
|
-
|
|
137
|
-
SYMBOL = 'neutral'
|
|
138
|
-
TEST_PASS_SUCCESS = true
|
|
139
|
-
|
|
125
|
+
SYMBOL = 'neutral'
|
|
140
126
|
end # Neutral
|
|
141
127
|
|
|
142
128
|
# Noop mutation, special case of neutral
|
|
143
129
|
class Noop < Neutral
|
|
144
|
-
|
|
145
|
-
SYMBOL = 'noop'
|
|
146
|
-
TEST_PASS_SUCCESS = true
|
|
147
|
-
|
|
130
|
+
SYMBOL = 'noop'
|
|
148
131
|
end # Noop
|
|
149
132
|
|
|
150
133
|
end # Mutation
|
|
@@ -11,11 +11,72 @@ module Mutant
|
|
|
11
11
|
|
|
12
12
|
children :value
|
|
13
13
|
|
|
14
|
+
# Integer overflow boundary probe zones.
|
|
15
|
+
#
|
|
16
|
+
# Each entry pairs a zone boundary with a safe prime sentinel
|
|
17
|
+
# value that falls within that zone. The literal is mutated to
|
|
18
|
+
# the sentinel of the next zone above its absolute value,
|
|
19
|
+
# probing whether consuming code handles values that cross the
|
|
20
|
+
# boundary of the next integer width.
|
|
21
|
+
#
|
|
22
|
+
# Safe primes (p = 2q + 1 where both p and q are prime) are
|
|
23
|
+
# chosen as sentinels because they cannot arise from simple
|
|
24
|
+
# arithmetic (multiplication, bit shifts, masking), providing
|
|
25
|
+
# strong guarantees against coincidental mutation kills. A
|
|
26
|
+
# mutation surviving against a safe prime is a strong signal
|
|
27
|
+
# of missing boundary validation for that integer width.
|
|
28
|
+
#
|
|
29
|
+
# Zone layout:
|
|
30
|
+
#
|
|
31
|
+
# Zone Boundary Sentinel
|
|
32
|
+
# int8 128 167
|
|
33
|
+
# uint8 256 467
|
|
34
|
+
# int16 32768 55_487
|
|
35
|
+
# uint16 65536 108_503
|
|
36
|
+
# int32 2^31 2_667_278_543
|
|
37
|
+
# uint32 2^32 7_980_081_959
|
|
38
|
+
# int64 2^63 15_508_464_536_481_899_903
|
|
39
|
+
#
|
|
40
|
+
# A literal snaps to the next zone above its absolute value.
|
|
41
|
+
# For example, a literal 100 (below int8 boundary 128) emits
|
|
42
|
+
# sentinel 167. A literal 200 (above int8, below uint8) emits
|
|
43
|
+
# sentinel 467. Values at or above 2^63 emit no sentinel as
|
|
44
|
+
# there is no higher zone to probe.
|
|
45
|
+
#
|
|
46
|
+
# Future versions of mutant will add infrastructure to explain
|
|
47
|
+
# alive mutations, including which overflow zone a surviving
|
|
48
|
+
# sentinel belongs to and what class of bug it indicates.
|
|
49
|
+
class OverflowZone
|
|
50
|
+
include Anima.new(:name, :boundary, :sentinel)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
OVERFLOW_ZONES = [
|
|
54
|
+
OverflowZone.new(name: :int8, boundary: 2**7, sentinel: 167),
|
|
55
|
+
OverflowZone.new(name: :uint8, boundary: 2**8, sentinel: 467),
|
|
56
|
+
OverflowZone.new(name: :int16, boundary: 2**15, sentinel: 55_487),
|
|
57
|
+
OverflowZone.new(name: :uint16, boundary: 2**16, sentinel: 108_503),
|
|
58
|
+
OverflowZone.new(name: :int32, boundary: 2**31, sentinel: 2_667_278_543),
|
|
59
|
+
OverflowZone.new(name: :uint32, boundary: 2**32, sentinel: 7_980_081_959),
|
|
60
|
+
OverflowZone.new(name: :int64, boundary: 2**63, sentinel: 15_508_464_536_481_899_903)
|
|
61
|
+
].freeze
|
|
62
|
+
|
|
14
63
|
private
|
|
15
64
|
|
|
16
65
|
def dispatch
|
|
17
66
|
emit_singletons
|
|
18
67
|
emit_values
|
|
68
|
+
emit_overflow_sentinel
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def emit_overflow_sentinel
|
|
72
|
+
absolute = value.abs
|
|
73
|
+
|
|
74
|
+
OVERFLOW_ZONES.each do |zone|
|
|
75
|
+
if absolute < zone.boundary
|
|
76
|
+
emit_type(zone.sentinel)
|
|
77
|
+
return
|
|
78
|
+
end
|
|
79
|
+
end
|
|
19
80
|
end
|
|
20
81
|
|
|
21
82
|
def values
|
|
@@ -16,14 +16,12 @@ module Mutant
|
|
|
16
16
|
class Reader
|
|
17
17
|
include Anima.new(:deadline, :io, :marshal, :response_reader, :log_reader)
|
|
18
18
|
|
|
19
|
-
private(*anima.attribute_names)
|
|
20
|
-
|
|
21
19
|
private_class_method :new
|
|
22
20
|
|
|
23
|
-
attr_reader :log
|
|
24
|
-
|
|
25
21
|
def error = Util.max_one(@errors)
|
|
26
22
|
|
|
23
|
+
def log = LogCapture.from_binary(@log)
|
|
24
|
+
|
|
27
25
|
def result = Util.max_one(@results)
|
|
28
26
|
|
|
29
27
|
def initialize(*)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mutant
|
|
4
|
+
class Reporter
|
|
5
|
+
class CLI
|
|
6
|
+
class Printer
|
|
7
|
+
# Shared logic for printing alive mutation results
|
|
8
|
+
module AliveResults
|
|
9
|
+
ALIVE_EXPLANATION = <<~'MESSAGE'
|
|
10
|
+
Uncovered mutations detected, exiting nonzero!
|
|
11
|
+
Alive mutations require one of two actions:
|
|
12
|
+
A) Keep the mutated code: Your tests specify the correct semantics,
|
|
13
|
+
and the original code is redundant. Accept the mutation.
|
|
14
|
+
B) Add a missing test: The original code is correct, but the tests
|
|
15
|
+
do not verify the behavior the mutation removed.
|
|
16
|
+
MESSAGE
|
|
17
|
+
|
|
18
|
+
def print_alive_results(failed_subject_results)
|
|
19
|
+
failed_subject_results.each do |subject_result|
|
|
20
|
+
SubjectResult.call(display_config:, output:, object: subject_result)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end # AliveResults
|
|
24
|
+
end # Printer
|
|
25
|
+
end # CLI
|
|
26
|
+
end # Reporter
|
|
27
|
+
end # Mutant
|
|
@@ -8,22 +8,65 @@ module Mutant
|
|
|
8
8
|
class EnvResult < self
|
|
9
9
|
delegate(:failed_subject_results)
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
MESSAGE
|
|
11
|
+
SEPARATOR = '-----------------------'
|
|
12
|
+
MORE_MESSAGE = '(%d more alive mutation(s), use `mutant session subject %s` to see all details)'
|
|
13
|
+
STATS_FORMAT = 'tests: %d, runtime: %.2fs, killtime: %.2fs'
|
|
14
|
+
NO_DIFF_MESSAGE = 'BUG: No diff generated, please report circumstances to https://github.com/mbj/mutant'
|
|
15
|
+
|
|
16
|
+
private_constant(*constants(false))
|
|
18
17
|
|
|
19
18
|
# Run printer
|
|
20
19
|
#
|
|
21
20
|
# @return [undefined]
|
|
22
21
|
def run
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
unless failed_subject_results.empty?
|
|
23
|
+
puts(AliveResults::ALIVE_EXPLANATION)
|
|
24
|
+
failed_subject_results.each(&method(:print_subject_summary))
|
|
25
|
+
end
|
|
25
26
|
visit(EnvProgress, object)
|
|
26
27
|
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def print_subject_summary(subject_result)
|
|
32
|
+
uncovered = subject_result.uncovered_results
|
|
33
|
+
|
|
34
|
+
if uncovered.any? { |coverage_result| critical?(coverage_result.mutation_result) }
|
|
35
|
+
SubjectResult.call(output:, object: subject_result)
|
|
36
|
+
return
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
print_subject_line(subject_result)
|
|
40
|
+
print_mutation_diff(uncovered.first.mutation_result)
|
|
41
|
+
|
|
42
|
+
remaining = uncovered.length - 1
|
|
43
|
+
|
|
44
|
+
return unless remaining.positive?
|
|
45
|
+
|
|
46
|
+
puts(MORE_MESSAGE % [remaining, subject_result.expression_syntax])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def print_subject_line(subject_result)
|
|
50
|
+
status(subject_result.identification)
|
|
51
|
+
puts(STATS_FORMAT % [subject_result.tests.length, subject_result.runtime, subject_result.killtime])
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def critical?(mutation_result)
|
|
55
|
+
!mutation_result.mutation_type.eql?('evil')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def print_mutation_diff(mutation_result)
|
|
59
|
+
puts(mutation_result.mutation_identification)
|
|
60
|
+
puts(SEPARATOR)
|
|
61
|
+
diff = mutation_result.mutation_diff
|
|
62
|
+
|
|
63
|
+
if diff
|
|
64
|
+
output.write(color? ? colorize_diff(diff) : diff)
|
|
65
|
+
else
|
|
66
|
+
puts(NO_DIFF_MESSAGE)
|
|
67
|
+
end
|
|
68
|
+
puts(SEPARATOR)
|
|
69
|
+
end
|
|
27
70
|
end # EnvResult
|
|
28
71
|
end # Printer
|
|
29
72
|
end # CLI
|
|
@@ -51,15 +51,12 @@ module Mutant
|
|
|
51
51
|
private
|
|
52
52
|
|
|
53
53
|
def print_log_messages
|
|
54
|
-
log = object.log
|
|
54
|
+
log = object.log.content
|
|
55
55
|
|
|
56
56
|
return if log.empty?
|
|
57
57
|
|
|
58
|
-
puts('
|
|
59
|
-
|
|
60
|
-
log.each_line do |line|
|
|
61
|
-
puts('[killfork] %<line>s' % { line: })
|
|
62
|
-
end
|
|
58
|
+
puts('Killfork log (combined stderr and stdout):')
|
|
59
|
+
puts(log)
|
|
63
60
|
end
|
|
64
61
|
|
|
65
62
|
def print_process_status
|
|
@@ -5,19 +5,117 @@ module Mutant
|
|
|
5
5
|
class CLI
|
|
6
6
|
class Printer
|
|
7
7
|
# Subject result printer
|
|
8
|
+
#
|
|
9
|
+
# Renders subject identification, tests, and all uncovered mutations
|
|
10
|
+
# inline so that subject context (source, node) is available when
|
|
11
|
+
# printing mutation details.
|
|
8
12
|
class SubjectResult < self
|
|
9
13
|
|
|
10
|
-
delegate :
|
|
14
|
+
delegate :uncovered_results, :tests
|
|
15
|
+
|
|
16
|
+
MAP = {
|
|
17
|
+
'evil' => :evil_details,
|
|
18
|
+
'neutral' => :neutral_details,
|
|
19
|
+
'noop' => :noop_details
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
NEUTRAL_MESSAGE = <<~'MESSAGE'
|
|
23
|
+
--- Neutral failure ---
|
|
24
|
+
Original code was inserted unmutated. And the test did NOT PASS.
|
|
25
|
+
Your tests do not pass initially or you found a bug in mutant / unparser.
|
|
26
|
+
Subject AST:
|
|
27
|
+
%s
|
|
28
|
+
Unparsed Source:
|
|
29
|
+
%s
|
|
30
|
+
MESSAGE
|
|
31
|
+
|
|
32
|
+
NO_DIFF_MESSAGE = <<~'MESSAGE'
|
|
33
|
+
--- Internal failure ---
|
|
34
|
+
BUG: A generated mutation did not result in exactly one diff hunk!
|
|
35
|
+
This is an invariant violation by the mutation generation engine.
|
|
36
|
+
Please report a reproduction to https://github.com/mbj/mutant
|
|
37
|
+
Original unparsed source:
|
|
38
|
+
%s
|
|
39
|
+
Original AST:
|
|
40
|
+
%s
|
|
41
|
+
Mutated unparsed source:
|
|
42
|
+
%s
|
|
43
|
+
Mutated AST:
|
|
44
|
+
%s
|
|
45
|
+
MESSAGE
|
|
46
|
+
|
|
47
|
+
NOOP_MESSAGE = <<~'MESSAGE'
|
|
48
|
+
---- Noop failure -----
|
|
49
|
+
No code was inserted. And the test did NOT PASS.
|
|
50
|
+
This is typically a problem of your specs not passing unmutated.
|
|
51
|
+
MESSAGE
|
|
52
|
+
|
|
53
|
+
SEPARATOR = '-----------------------'
|
|
54
|
+
STATS_FORMAT = 'tests: %d, runtime: %.2fs, killtime: %.2fs'
|
|
55
|
+
|
|
56
|
+
private_constant(*constants(false))
|
|
11
57
|
|
|
12
58
|
# Run report printer
|
|
13
59
|
#
|
|
14
60
|
# @return [undefined]
|
|
15
61
|
def run
|
|
16
|
-
status(
|
|
17
|
-
tests.
|
|
18
|
-
|
|
62
|
+
status(object.identification)
|
|
63
|
+
puts(STATS_FORMAT % [tests.length, object.runtime, object.killtime])
|
|
64
|
+
uncovered_results.each do |coverage_result|
|
|
65
|
+
print_mutation_result(coverage_result.mutation_result)
|
|
66
|
+
end
|
|
67
|
+
print_selected_tests
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def print_selected_tests
|
|
73
|
+
if tests.empty?
|
|
74
|
+
puts('no selected tests')
|
|
75
|
+
else
|
|
76
|
+
puts("selected tests (#{tests.length}):")
|
|
77
|
+
tests.each do |test|
|
|
78
|
+
puts("- #{test.identification}")
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def print_mutation_result(mutation_result)
|
|
84
|
+
puts(mutation_result.mutation_identification)
|
|
85
|
+
puts(SEPARATOR)
|
|
86
|
+
visit(IsolationResult, mutation_result.isolation_result) if show_isolation_logs?(mutation_result)
|
|
87
|
+
__send__(MAP.fetch(mutation_result.mutation_type), mutation_result)
|
|
88
|
+
puts(SEPARATOR)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def show_isolation_logs?(mutation_result)
|
|
92
|
+
display_config.isolation_logs || !mutation_result.mutation_type.eql?('evil')
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# rubocop:disable Metrics/MethodLength
|
|
96
|
+
def evil_details(mutation_result)
|
|
97
|
+
diff = mutation_result.mutation_diff
|
|
98
|
+
|
|
99
|
+
if diff
|
|
100
|
+
output.write(color? ? colorize_diff(diff) : diff)
|
|
101
|
+
else
|
|
102
|
+
info(
|
|
103
|
+
NO_DIFF_MESSAGE,
|
|
104
|
+
object.source,
|
|
105
|
+
object.node.inspect,
|
|
106
|
+
mutation_result.mutation_source,
|
|
107
|
+
mutation_result.mutation_node.inspect
|
|
108
|
+
)
|
|
19
109
|
end
|
|
20
|
-
|
|
110
|
+
end
|
|
111
|
+
# rubocop:enable Metrics/MethodLength
|
|
112
|
+
|
|
113
|
+
def noop_details(_mutation_result)
|
|
114
|
+
info(NOOP_MESSAGE)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def neutral_details(mutation_result)
|
|
118
|
+
info(NEUTRAL_MESSAGE, object.node.inspect, mutation_result.mutation_source)
|
|
21
119
|
end
|
|
22
120
|
|
|
23
121
|
end # SubjectResult
|
|
@@ -5,13 +5,25 @@ module Mutant
|
|
|
5
5
|
class CLI
|
|
6
6
|
# CLI runner status printer base class
|
|
7
7
|
class Printer
|
|
8
|
+
# Printer display options
|
|
9
|
+
class DisplayConfig
|
|
10
|
+
include Anima.new(:isolation_logs)
|
|
11
|
+
|
|
12
|
+
DEFAULT = new(isolation_logs: false)
|
|
13
|
+
VERBOSE = new(isolation_logs: true)
|
|
14
|
+
end
|
|
15
|
+
|
|
8
16
|
include(
|
|
9
17
|
AbstractType,
|
|
10
18
|
Adamantium,
|
|
11
|
-
Anima.new(:output, :object),
|
|
19
|
+
Anima.new(:display_config, :output, :object),
|
|
12
20
|
Procto
|
|
13
21
|
)
|
|
14
22
|
|
|
23
|
+
def self.call(output:, object:, display_config: DisplayConfig::DEFAULT)
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
|
|
15
27
|
private_class_method :new
|
|
16
28
|
|
|
17
29
|
def call = run
|
|
@@ -82,6 +94,17 @@ module Mutant
|
|
|
82
94
|
end
|
|
83
95
|
|
|
84
96
|
alias_method :color?, :tty?
|
|
97
|
+
|
|
98
|
+
def colorize_diff(raw_diff)
|
|
99
|
+
raw_diff.lines.map do |line|
|
|
100
|
+
case line[0]
|
|
101
|
+
when '+' then Unparser::Color::GREEN.format(line)
|
|
102
|
+
when '-' then Unparser::Color::RED.format(line)
|
|
103
|
+
else
|
|
104
|
+
line
|
|
105
|
+
end
|
|
106
|
+
end.join
|
|
107
|
+
end
|
|
85
108
|
end # Printer
|
|
86
109
|
end # CLI
|
|
87
110
|
end # Reporter
|
|
@@ -60,14 +60,13 @@ module Mutant
|
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
# rubocop:disable Metrics/MethodLength
|
|
63
|
-
# mutant:disable (3.2 specific mutation)
|
|
64
63
|
def parse_line(root, line)
|
|
65
64
|
match = FORMAT.match(line)
|
|
66
65
|
|
|
67
66
|
if match
|
|
68
67
|
Either::Right.new(
|
|
69
68
|
Path.new(
|
|
70
|
-
path: root.join(Util.one(match
|
|
69
|
+
path: root.join(Util.one(match)),
|
|
71
70
|
to:,
|
|
72
71
|
world:
|
|
73
72
|
)
|