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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d4eb76ac40bf4c263a5201b009dbc0760df5191918754bd47452017a4c97ac9b
4
- data.tar.gz: b7ed8bb7728602995b796d7950d7c5ed445a3d9e775f7ad69b0dd5e91da3a69f
3
+ metadata.gz: 8f5176c82dd4c6a57785deebd566c3b29b14b4496d35c06e15c20ff3b98a0b37
4
+ data.tar.gz: 9f29a560a6c144480d8808cf24ec2ec2127b78bc7f9ef88e8e8a3e0f541e0974
5
5
  SHA512:
6
- metadata.gz: 27d3948ff6e67c670aefe552fad358d333ee584d48090f13b14d15bee01a756dd6982be5922200dd0d2d939916b9d97e02c862b5d3487c9fd7572688affa544d
7
- data.tar.gz: 3155f5599aa51d7d8644fbac0f4f6edb0f2e63d26bd70ca87f20b820f492ba07fa773ec974ad474f3ec08b652acc89eb23033124925c37f933e84eee803f0219
6
+ metadata.gz: a5e05c7b33041fb609ebcc89683461dba406c53592801d69603fa9003e0d8ee923f54d7b6f0bc9abcfd811ade64054ef3a8b9bf2bc30dbd44f37ca1f1ae2d4b6
7
+ data.tar.gz: 659e58f9ce856224a2a8f67d073c9006e340bcd838b0f6543cc57f8a12cbf6cc29c6dccad248744748a9f176750dae5240b719b797c7339a8552532e10d4ce22
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.15.1
1
+ 0.16.2
@@ -3,20 +3,43 @@
3
3
  module Mutant
4
4
  class AST
5
5
  class Pattern
6
+ # rubocop:disable Metrics/ClassLength
6
7
  class Lexer
7
- WHITESPACE = [' ', "\t", "\n"].to_set.freeze
8
- STRING_PATTERN = /\A[a-zA-Z][_a-zA-Z0-9]*\z/
8
+ WHITESPACE = [' ', "\t", "\n"].to_set.freeze
9
9
 
10
- SINGLE_CHAR =
10
+ STRUCTURAL =
11
11
  {
12
12
  '(' => :group_start,
13
13
  ')' => :group_end,
14
14
  ',' => :delimiter,
15
- '=' => :eq,
16
15
  '{' => :properties_start,
17
16
  '}' => :properties_end
18
17
  }.freeze
19
18
 
19
+ EQ_OPERATORS = %w[=== == =~].freeze
20
+
21
+ OPERATORS_BY_START =
22
+ {
23
+ '!' => %w[!= !~ !].freeze,
24
+ '<' => %w[<=> << <= <].freeze,
25
+ '>' => %w[>> >= >].freeze,
26
+ '+' => %w[+@ +].freeze,
27
+ '-' => %w[-@ -].freeze,
28
+ '*' => %w[** *].freeze,
29
+ '[' => ['[]=', '[]'].freeze,
30
+ '/' => ['/'].freeze,
31
+ '%' => ['%'].freeze,
32
+ '&' => ['&'].freeze,
33
+ '|' => ['|'].freeze,
34
+ '^' => ['^'].freeze,
35
+ '~' => ['~'].freeze
36
+ }.freeze
37
+
38
+ IDENTIFIER_START = /[a-zA-Z]/
39
+ IDENTIFIER_CONTINUE = /[a-zA-Z0-9_]/
40
+
41
+ SETTER_TERMINATORS = (WHITESPACE + [',', ')', '}']).freeze
42
+
20
43
  def self.call(string)
21
44
  new(string).__send__(:run)
22
45
  end
@@ -31,7 +54,7 @@ module Mutant
31
54
  #{token.display_location}
32
55
  MESSAGE
33
56
  end
34
- end # Token
57
+ end # InvalidToken
35
58
  end # Error
36
59
 
37
60
  private_class_method :new
@@ -58,71 +81,123 @@ module Mutant
58
81
  end
59
82
 
60
83
  def consume
61
- while next? && !instance_variable_defined?(:@error)
84
+ loop do
62
85
  skip_whitespace
86
+ break unless next? && !instance_variable_defined?(:@error)
63
87
 
64
- consume_char || consume_string
65
-
66
- skip_whitespace
88
+ consume_structural \
89
+ || consume_eq \
90
+ || consume_operator \
91
+ || consume_identifier \
92
+ || consume_invalid
67
93
  end
68
94
  end
69
95
 
70
- def consume_char
96
+ def consume_structural
97
+ char = peek
98
+ type = STRUCTURAL.fetch(char) { return }
71
99
  start_position = @next_position
100
+ advance_position
101
+ @tokens << token(type:, start_position:)
102
+ end
72
103
 
73
- char = peek
104
+ def consume_eq
105
+ return unless peek.eql?('=')
106
+
107
+ start_position = @next_position
74
108
 
75
- type = SINGLE_CHAR.fetch(char) { return }
109
+ EQ_OPERATORS.each do |op|
110
+ next unless matches?(op)
111
+
112
+ advance_positions(op.length)
113
+ @tokens << token(type: :string, start_position:, value: op)
114
+ return true
115
+ end
76
116
 
77
117
  advance_position
118
+ @tokens << token(type: :eq, start_position:)
119
+ end
78
120
 
79
- @tokens << token(type:, start_position:)
121
+ def consume_operator
122
+ operators = OPERATORS_BY_START[peek] or return
123
+ match = operators.detect { |op| matches?(op) } or return
124
+
125
+ start_position = @next_position
126
+ advance_positions(match.length)
127
+ @tokens << token(type: :string, start_position:, value: match)
80
128
  end
81
129
 
82
- def token(type:, start_position:, value: nil)
83
- Token.new(
84
- type:,
85
- value:,
86
- location: Source::Location.new(
87
- source: @source,
88
- line_index: @line_index,
89
- line_start: @line_start,
90
- range: range_from(start_position)
91
- )
130
+ def consume_identifier
131
+ return unless IDENTIFIER_START.match?(peek)
132
+
133
+ start_position = @next_position
134
+ advance_position while IDENTIFIER_CONTINUE.match?(peek)
135
+ consume_identifier_suffix
136
+
137
+ @tokens << token(
138
+ type: :string,
139
+ start_position:,
140
+ value: @string[range_from(start_position)]
92
141
  )
93
142
  end
94
143
 
95
- def consume_string
96
- start_position = @next_position
144
+ def consume_identifier_suffix
145
+ advance_position if suffix_char?(peek)
146
+ end
97
147
 
98
- token = build_string(start_position, read_string_body)
148
+ def suffix_char?(char)
149
+ char.eql?('!') || char.eql?('?') || (char.eql?('=') && setter_suffix_follows?)
150
+ end
99
151
 
100
- if valid_string?(token.value)
101
- @tokens << token
102
- else
103
- @error = Error::InvalidToken.new(token:)
104
- end
152
+ def setter_suffix_follows?
153
+ next_char = @string[@next_position.succ]
154
+
155
+ next_char.nil? || SETTER_TERMINATORS.include?(next_char)
105
156
  end
106
157
 
107
- def read_string_body
108
- string = +''
158
+ def consume_invalid
159
+ start_position = @next_position
160
+ advance_invalid
161
+ @error = Error::InvalidToken.new(
162
+ token: token(
163
+ type: :string,
164
+ start_position:,
165
+ value: @string[range_from(start_position)]
166
+ )
167
+ )
168
+ end
109
169
 
110
- while next?
111
- char = peek
112
- break if SINGLE_CHAR.key?(char) || whitespace?(char)
170
+ def advance_invalid
171
+ loop do
172
+ break unless next?
173
+ break if terminates_invalid?(peek)
113
174
 
114
- string << char
115
175
  advance_position
116
176
  end
177
+ end
117
178
 
118
- string
179
+ def terminates_invalid?(char)
180
+ STRUCTURAL.key?(char) || whitespace?(char)
119
181
  end
120
182
 
121
- def build_string(start_position, string)
122
- token(
123
- type: :string,
124
- value: string,
125
- start_position:
183
+ def matches?(string)
184
+ @string[@next_position, string.length].eql?(string)
185
+ end
186
+
187
+ def advance_positions(count)
188
+ count.times { advance_position }
189
+ end
190
+
191
+ def token(type:, start_position:, value: nil)
192
+ Token.new(
193
+ type:,
194
+ value:,
195
+ location: Source::Location.new(
196
+ source: @source,
197
+ line_index: @line_index,
198
+ line_start: @line_start,
199
+ range: range_from(start_position)
200
+ )
126
201
  )
127
202
  end
128
203
 
@@ -130,10 +205,6 @@ module Mutant
130
205
  start_position...@next_position
131
206
  end
132
207
 
133
- def valid_string?(string)
134
- STRING_PATTERN.match?(string)
135
- end
136
-
137
208
  def advance_position
138
209
  @next_position += 1
139
210
  end
@@ -165,6 +236,7 @@ module Mutant
165
236
  @next_position < @string.length
166
237
  end
167
238
  end # Lexer
239
+ # rubocop:enable Metrics/ClassLength
168
240
  end # Pattern
169
241
  end # AST
170
242
  end # Mutant
@@ -10,7 +10,7 @@ module Mutant
10
10
  class Root < self
11
11
  NAME = 'mutant'
12
12
  SHORT_DESCRIPTION = 'mutation testing engine main command'
13
- SUBCOMMANDS = [Environment::Run, Environment::Test::Run::Root, Environment, Util].freeze
13
+ SUBCOMMANDS = [Environment::Run, Environment::Test::Run::Root, Environment, Session, Util].freeze
14
14
  end # Root
15
15
  end # Command
16
16
  end # CLI
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ module CLI
5
+ class Command
6
+ class Session < self
7
+ NAME = 'session'
8
+ SHORT_DESCRIPTION = 'Session history subcommands'
9
+
10
+ RESULTS_DIR = '.mutant/results'
11
+
12
+ private
13
+
14
+ def session_files
15
+ dir = world.pathname.new(RESULTS_DIR)
16
+
17
+ return [] unless dir.directory?
18
+
19
+ dir.glob('*.json')
20
+ end
21
+
22
+ def load_session_file(path)
23
+ world.parse_json(path.read)
24
+ .bind(&Result::Session::CODEC.load_transform.public_method(:call))
25
+ end
26
+
27
+ # Shared base for commands that operate on a session
28
+ class SessionCommand < self
29
+ OPTIONS = %i[add_session_id_option].freeze
30
+
31
+ UUID_FORMAT = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/
32
+
33
+ private_constant(:UUID_FORMAT)
34
+
35
+ def display_config
36
+ @display_config || Reporter::CLI::Printer::DisplayConfig::DEFAULT
37
+ end
38
+
39
+ def action
40
+ path = resolve_session_path or return Either::Left.new('No sessions found')
41
+
42
+ return Either::Left.new("Session file not found: #{path}") unless path.file?
43
+
44
+ load_session_file(path).either(
45
+ lambda { |error|
46
+ Either::Left.new("Failed to load session: #{error}\nRun `mutant session gc` to remove incompatible sessions.")
47
+ },
48
+ method(:run_report)
49
+ )
50
+ end
51
+
52
+ private
53
+
54
+ def add_session_id_option(parser)
55
+ parser.on('--session-id=ID', 'Session ID to operate on (default: latest)') do |value|
56
+ fail(OptionParser::InvalidArgument, "invalid UUID format: #{value}") unless UUID_FORMAT.match?(value)
57
+
58
+ @session_id = value
59
+ end
60
+ end
61
+
62
+ def add_verbose_option(parser)
63
+ parser.separator("\nDisplay Options:\n\n")
64
+ parser.on('--verbose', 'Show verbose output') do
65
+ @display_config = Reporter::CLI::Printer::DisplayConfig::VERBOSE
66
+ end
67
+ end
68
+
69
+ def resolve_session_path
70
+ if @session_id
71
+ world.pathname.new("#{RESULTS_DIR}/#{@session_id}.json")
72
+ else
73
+ session_files.last
74
+ end
75
+ end
76
+
77
+ def run_report(session)
78
+ print("Session: #{session.session_id}")
79
+
80
+ print_report(session)
81
+ end
82
+
83
+ abstract_method :print_report
84
+ end # SessionCommand
85
+
86
+ class List < self
87
+ NAME = 'list'
88
+ SHORT_DESCRIPTION = 'List past mutation testing sessions'
89
+ SUBCOMMANDS = [].freeze
90
+
91
+ HEADER_FORMAT = '%-6s %-10s %-8s %-10s %-10s %-36s %s'
92
+ ROW_FORMAT = '%-6s %-10s %-8s %-10s %-10s %-36s %s'
93
+ INCOMPATIBLE = '--------------- [incompatible] ---------------'
94
+
95
+ def action
96
+ print_header
97
+ session_files.reverse_each(&method(:print_session))
98
+
99
+ Either::Right.new(nil)
100
+ end
101
+
102
+ private
103
+
104
+ def print_header
105
+ print(HEADER_FORMAT % ['ALIVE', 'MUTATIONS', 'SUBJECTS', 'RUNTIME', 'KILLTIME', 'SESSION ID', 'TIMESTAMP'])
106
+ end
107
+
108
+ def print_session(path)
109
+ load_session_file(path).either(
110
+ ->(_error) { print(colorize_unsupported(path)) },
111
+ method(:print_session_row)
112
+ )
113
+ end
114
+
115
+ def print_session_row(session)
116
+ subjects = session.subject_results
117
+
118
+ print(ROW_FORMAT % [
119
+ subjects.sum(&:amount_mutations_alive),
120
+ subjects.sum(&:amount_mutations),
121
+ subjects.length,
122
+ format_time(session.runtime),
123
+ format_time(session.killtime),
124
+ session.session_id,
125
+ session.timestamp.strftime('%Y-%m-%d %H:%M:%S')
126
+ ])
127
+ end
128
+
129
+ def format_time(seconds)
130
+ '%.2fs' % seconds
131
+ end
132
+
133
+ def colorize_unsupported(path)
134
+ session_id = path.basename('.json')
135
+
136
+ Unparser::Color::RED.format(INCOMPATIBLE.ljust(54)) + session_id.to_s
137
+ end
138
+ end # List
139
+
140
+ class Show < SessionCommand
141
+ include Mutant::Reporter::CLI::Printer::AliveResults
142
+
143
+ NAME = 'show'
144
+ SHORT_DESCRIPTION = 'Show results of a past session'
145
+ SUBCOMMANDS = [].freeze
146
+ OPTIONS = (superclass::OPTIONS + %i[add_verbose_option]).freeze
147
+
148
+ private
149
+
150
+ def print_report(session)
151
+ failed = session.subject_results.reject(&:success?)
152
+
153
+ print("Time: #{session.timestamp.strftime('%Y-%m-%d %H:%M:%S')}")
154
+ print("Version: #{session.mutant_version}")
155
+ print("Ruby: #{session.ruby_version}")
156
+ print("Subjects: #{session.subject_results.length}")
157
+ print("Alive: #{failed.flat_map(&:uncovered_results).length}")
158
+
159
+ print_alive_results(failed)
160
+
161
+ Either::Right.new(nil)
162
+ end
163
+ end # Show
164
+
165
+ class Subject < SessionCommand
166
+ include Mutant::Reporter::CLI::Printer::AliveResults
167
+
168
+ NAME = 'subject'
169
+ SHORT_DESCRIPTION = 'List subjects or show alive mutations for a specific subject'
170
+ SUBCOMMANDS = [].freeze
171
+ OPTIONS = (superclass::OPTIONS + %i[add_verbose_option]).freeze
172
+
173
+ HEADER_FORMAT = '%-6s %-6s %s'
174
+ ROW_FORMAT = '%-6s %-6s %s'
175
+
176
+ def parse_remaining_arguments(arguments)
177
+ case arguments.length
178
+ when 0 then Either::Right.new(self)
179
+ when 1
180
+ @expression = Mutant::Util.one(arguments)
181
+ Either::Right.new(self)
182
+ else
183
+ Either::Left.new('Expected zero or one subject expression argument')
184
+ end
185
+ end
186
+
187
+ private
188
+
189
+ def print_report(session)
190
+ if @expression
191
+ print_subject_detail(session)
192
+ else
193
+ print_subject_list(session)
194
+ end
195
+ end
196
+
197
+ def print_subject_list(session)
198
+ print(HEADER_FORMAT % %w[ALIVE TOTAL SUBJECT])
199
+
200
+ session.subject_results
201
+ .sort_by { |subject_result| -subject_result.uncovered_results.length }
202
+ .each(&method(:print_subject_row))
203
+
204
+ Either::Right.new(nil)
205
+ end
206
+
207
+ def print_subject_row(subject_result)
208
+ alive = subject_result.uncovered_results.length
209
+ total = subject_result.amount_mutations
210
+
211
+ print(ROW_FORMAT % [alive, total, subject_result.expression_syntax])
212
+ end
213
+
214
+ def print_subject_detail(session)
215
+ subject_result = session.subject_results.detect do |subject_result|
216
+ subject_result.expression_syntax.eql?(@expression)
217
+ end
218
+
219
+ return Either::Left.new("Subject not found: #{@expression}") unless subject_result
220
+
221
+ print_alive_results([subject_result])
222
+
223
+ Either::Right.new(nil)
224
+ end
225
+ end # Subject
226
+
227
+ class GC < self
228
+ NAME = 'gc'
229
+ SHORT_DESCRIPTION = 'Remove incompatible and old session results'
230
+ SUBCOMMANDS = [].freeze
231
+ OPTIONS = %i[add_gc_options].freeze
232
+
233
+ DEFAULT_KEEP = 100
234
+
235
+ def initialize(*)
236
+ super
237
+ @keep = DEFAULT_KEEP
238
+ end
239
+
240
+ def action
241
+ incompatible, compatible = partition_sessions
242
+
243
+ incompatible.each(&:delete)
244
+
245
+ excess = compatible.length > @keep ? compatible.first(compatible.length - @keep) : []
246
+ excess.each(&:delete)
247
+
248
+ print("Removed #{incompatible.length + excess.length} session(s)")
249
+
250
+ Either::Right.new(nil)
251
+ end
252
+
253
+ private
254
+
255
+ def add_gc_options(parser)
256
+ parser.on('--keep=N', Integer, "Keep N most recent sessions (default: #{DEFAULT_KEEP})") do |value|
257
+ @keep = value
258
+ end
259
+ end
260
+
261
+ def partition_sessions
262
+ incompatible = []
263
+ compatible = []
264
+
265
+ session_files.each do |path|
266
+ load_session_file(path).either(
267
+ ->(_error) { incompatible << path },
268
+ ->(_session) { compatible << path }
269
+ )
270
+ end
271
+
272
+ [incompatible, compatible]
273
+ end
274
+
275
+ end # GC
276
+
277
+ SUBCOMMANDS = [List, Show, Subject, GC].freeze
278
+ end # Session
279
+ end # Command
280
+ end # CLI
281
+ end # Mutant
@@ -220,8 +220,22 @@ module Mutant
220
220
  "#{full_name}: #{message}\n\n#{parser}"
221
221
  end
222
222
 
223
- def print(message)
224
- world.stdout.puts(message)
223
+ def output
224
+ world.stdout
225
+ end
226
+
227
+ def puts(message)
228
+ output.puts(message)
229
+ end
230
+
231
+ alias_method :print, :puts
232
+
233
+ def parse_remaining_arguments(arguments)
234
+ if arguments.empty?
235
+ Either::Right.new(self)
236
+ else
237
+ Either::Left.new("unexpected arguments: #{arguments.join(' ')}")
238
+ end
225
239
  end
226
240
  end # Command
227
241
  # rubocop:enable Metrics/ClassLength
data/lib/mutant/config.rb CHANGED
@@ -120,7 +120,7 @@ module Mutant
120
120
  .lmap { |exception| "MUTANT_JOBS environment variable has invalid value: #{jobs.inspect} - #{exception}" }
121
121
  .fmap { |jobs_value| DEFAULT.with(jobs: jobs_value) }
122
122
  else
123
- Either::Right.new(DEFAULT)
123
+ Either::Right.new(DEFAULT.with(coverage_criteria: CoverageCriteria::EMPTY))
124
124
  end
125
125
  end
126
126
  private_class_method :load_env_config
@@ -13,8 +13,6 @@ module Mutant
13
13
  :scope_symbol
14
14
  )
15
15
 
16
- private(*anima.attribute_names)
17
-
18
16
  MATCHERS = {
19
17
  '.' => [Matcher::Methods::Singleton, Matcher::Methods::Metaclass].freeze,
20
18
  '#' => [Matcher::Methods::Instance].freeze
@@ -10,8 +10,6 @@ module Mutant
10
10
  :scope_symbol
11
11
  )
12
12
 
13
- private(*anima.attribute_names)
14
-
15
13
  MATCHERS = {
16
14
  '.' => [Matcher::Methods::Singleton, Matcher::Methods::Metaclass].freeze,
17
15
  '#' => [Matcher::Methods::Instance].freeze
@@ -6,8 +6,6 @@ module Mutant
6
6
  class Namespace < self
7
7
  include AbstractType, Anima.new(:scope_name)
8
8
 
9
- private(*anima.attribute_names)
10
-
11
9
  # Recursive namespace expression
12
10
  class Recursive < self
13
11
  REGEXP = /\A#{SCOPE_NAME_PATTERN}?\*\z/
@@ -17,7 +17,7 @@ module Mutant
17
17
  def call(_tests)
18
18
  Result::Test.new(
19
19
  job_index: nil,
20
- output: '',
20
+ output: LogCapture::String.new(content: ''),
21
21
  passed: true,
22
22
  runtime: 0.0
23
23
  )
@@ -89,7 +89,7 @@ module Mutant
89
89
  def result
90
90
  Result.new(
91
91
  exception: @exception,
92
- log: @log_fragments.join,
92
+ log: LogCapture.from_binary(@log_fragments.join),
93
93
  process_status: @process_status,
94
94
  timeout: @timeout,
95
95
  value: @value
@@ -132,11 +132,7 @@ module Mutant
132
132
  def load_result(result_fragments)
133
133
  @value = world.marshal.load(result_fragments.join)
134
134
  rescue ArgumentError => exception
135
- @exception = Exception.new(
136
- backtrace: exception.backtrace,
137
- message: exception.message,
138
- original_class: exception.class
139
- )
135
+ @exception = Mutant::Result::Exception.from_exception(exception)
140
136
  end
141
137
 
142
138
  # rubocop:disable Metrics/MethodLength
@@ -197,7 +193,7 @@ module Mutant
197
193
  end
198
194
 
199
195
  def handle_status(status)
200
- @process_status = status
196
+ @process_status = Mutant::Result::ProcessStatus.from_process_status(status)
201
197
  end
202
198
 
203
199
  def peek_child
@@ -23,7 +23,7 @@ module Mutant
23
23
 
24
24
  Result.new(
25
25
  exception:,
26
- log: '',
26
+ log: LogCapture::String.new(content: ''),
27
27
  process_status: nil,
28
28
  timeout: nil,
29
29
  value: