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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/lib/mutant/ast/pattern/lexer.rb +119 -47
  4. data/lib/mutant/cli/command/root.rb +1 -1
  5. data/lib/mutant/cli/command/session.rb +281 -0
  6. data/lib/mutant/cli/command.rb +16 -2
  7. data/lib/mutant/config.rb +1 -1
  8. data/lib/mutant/expression/method.rb +0 -2
  9. data/lib/mutant/expression/methods.rb +0 -2
  10. data/lib/mutant/expression/namespace.rb +0 -2
  11. data/lib/mutant/integration/null.rb +1 -1
  12. data/lib/mutant/isolation/fork.rb +3 -7
  13. data/lib/mutant/isolation/none.rb +1 -1
  14. data/lib/mutant/isolation.rb +31 -0
  15. data/lib/mutant/log_capture.rb +89 -0
  16. data/lib/mutant/matcher/null.rb +1 -1
  17. data/lib/mutant/mutation/runner/sink.rb +23 -10
  18. data/lib/mutant/mutation/runner.rb +1 -0
  19. data/lib/mutant/mutation.rb +3 -20
  20. data/lib/mutant/mutator/node/literal/integer.rb +61 -0
  21. data/lib/mutant/parallel/connection.rb +2 -4
  22. data/lib/mutant/parallel/driver.rb +0 -2
  23. data/lib/mutant/reporter/cli/printer/alive_results.rb +27 -0
  24. data/lib/mutant/reporter/cli/printer/env_result.rb +52 -9
  25. data/lib/mutant/reporter/cli/printer/isolation_result.rb +3 -6
  26. data/lib/mutant/reporter/cli/printer/subject_result.rb +103 -5
  27. data/lib/mutant/reporter/cli/printer/test.rb +1 -1
  28. data/lib/mutant/reporter/cli/printer.rb +24 -1
  29. data/lib/mutant/repository/diff.rb +1 -2
  30. data/lib/mutant/result/exception.rb +29 -0
  31. data/lib/mutant/result/json_writer.rb +43 -0
  32. data/lib/mutant/result/process_status.rb +37 -0
  33. data/lib/mutant/result/session.rb +63 -0
  34. data/lib/mutant/result/test.rb +57 -0
  35. data/lib/mutant/result.rb +201 -96
  36. data/lib/mutant/segment/recorder.rb +0 -2
  37. data/lib/mutant/test/runner/sink.rb +1 -1
  38. data/lib/mutant/timer.rb +3 -1
  39. data/lib/mutant/transform/codec.rb +45 -0
  40. data/lib/mutant/transform.rb +33 -25
  41. data/lib/mutant/world.rb +6 -0
  42. data/lib/mutant/zombifier.rb +0 -2
  43. data/lib/mutant.rb +14 -4
  44. metadata +34 -7
  45. data/lib/mutant/reporter/cli/printer/coverage_result.rb +0 -19
  46. data/lib/mutant/reporter/cli/printer/mutation_result.rb +0 -84
@@ -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
@@ -4,7 +4,7 @@ module Mutant
4
4
  class Matcher
5
5
  # A null matcher, that does not match any subjects
6
6
  class Null < self
7
- include Concord.new
7
+ include Equalizer.new
8
8
 
9
9
  # Enumerate subjects
10
10
  #
@@ -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
- mutation_result = mutation_result(response.result)
47
-
48
- subject = mutation_result.mutation.subject
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: previous_coverage_results(subject).dup << coverage_result(mutation_result),
53
- tests: env.selections.fetch(subject)
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: mutation_index_result.isolation_result,
71
- mutation: env.mutations.fetch(mutation_index_result.mutation_index),
72
- runtime: mutation_index_result.runtime
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
 
@@ -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 = 'evil'
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(*)
@@ -14,8 +14,6 @@ module Mutant
14
14
  :workers
15
15
  )
16
16
 
17
- private(*anima.attribute_names)
18
-
19
17
  def initialize(**attributes)
20
18
  @alive = true
21
19
  super
@@ -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
- ALIVE_EXPLANATION = <<~'MESSAGE'
12
- Alive mutations require one of two actions:
13
- A) Keep the mutated code: Your tests specify the correct semantics,
14
- and the original code is redundant. Accept the mutation.
15
- B) Add a missing test: The original code is correct, but the tests
16
- do not verify the behavior the mutation removed.
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
- puts(ALIVE_EXPLANATION) if failed_subject_results.any?
24
- visit_collection(SubjectResult, failed_subject_results)
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('Log messages (combined stderr and stdout):')
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 :subject, :uncovered_results, :tests
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(subject.identification)
17
- tests.each do |test|
18
- puts("- #{test.identification}")
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
- visit_collection(CoverageResult, uncovered_results)
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
@@ -110,7 +110,7 @@ module Mutant
110
110
  #
111
111
  # @return [undefined]
112
112
  def run
113
- puts(object.output)
113
+ puts(object.output.content)
114
114
  end
115
115
 
116
116
  end # Result
@@ -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.captures)),
69
+ path: root.join(Util.one(match)),
71
70
  to:,
72
71
  world:
73
72
  )