mutant 0.16.0 → 0.16.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d35acd82d2b4cf6222613f653da44131636400215c56a14a00f1ecd2ef212079
4
- data.tar.gz: ae7ad7d71685be5108bb5fcc6be1b0cae5cd32f56e2848d25467c82d2f72771c
3
+ metadata.gz: 1f7a495dc61f610e15c0b43ee64726016fa2feb694ce948e9554478f15c60c03
4
+ data.tar.gz: 11c45dd1db15519c9647ac2b13a8c794ddacb3a4a396e90ea2c6accfb6e71dd0
5
5
  SHA512:
6
- metadata.gz: 1481530eb5c56560e06a828086df6d8a5f12d47b3180bafc47f996ccd3a6db22b2cd0ddd213bb12c6934cf878341bad5f987f8cb91113ad0db58a0c68760d203
7
- data.tar.gz: 657f62dc386d13f729dfb92fc78afed7c426cd4e64f53ca938c403c6cd4ad06ad856b379dde9d890d2a96ae692f64521975d9d3e0fd850095a4b03ab82d9b906
6
+ metadata.gz: 8c52e9fa61e9b8ad827cd02f37469654201115c06251330a7eb695a9909b79d355fa9c9d555002d96650589c36d71046436462736754e7a33438f2b03c11bf37
7
+ data.tar.gz: 7a1a190d3e17f1bfc3bee1584023b6f7756b97d2b79ffa6f42a93ce2b70c0d323e29b0bd7bd4010204d3bbac3a26a82deaa04163c4fc83044d1cb799ecd5d4a8
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.16.0
1
+ 0.16.3
@@ -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
@@ -386,6 +386,16 @@ module Mutant
386
386
  fixed: Node.fixed([[Node::Fixed::Attribute, :value]]),
387
387
  variable: nil
388
388
  ),
389
+ Node.new(
390
+ type: :forward_arg,
391
+ fixed: EMPTY_ARRAY,
392
+ variable: nil
393
+ ),
394
+ Node.new(
395
+ type: :forward_args,
396
+ fixed: EMPTY_ARRAY,
397
+ variable: nil
398
+ ),
389
399
  Node.new(
390
400
  type: :forwarded_args,
391
401
  fixed: EMPTY_ARRAY,
@@ -21,7 +21,7 @@ module Mutant
21
21
 
22
22
  def load_session_file(path)
23
23
  world.parse_json(path.read)
24
- .bind(&Result::Session::JSON.load_transform.public_method(:call))
24
+ .bind(&Result::Session::CODEC.load_transform.public_method(:call))
25
25
  end
26
26
 
27
27
  # Shared base for commands that operate on a session
@@ -30,6 +30,8 @@ module Mutant
30
30
  end
31
31
 
32
32
  def toplevel_consts(node)
33
+ return EMPTY_ARRAY if node.nil?
34
+
33
35
  children = node.children
34
36
 
35
37
  case node.type
@@ -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
@@ -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:
@@ -25,11 +25,11 @@ module Mutant
25
25
  dump = Transform::Success.new(
26
26
  block: lambda do |object|
27
27
  {
28
- 'exception' => object.exception && Mutant::Result::Exception::JSON.dump(object.exception).from_right,
29
- 'log' => object.log,
30
- 'process_status' => object.process_status && Mutant::Result::ProcessStatus::JSON.dump(object.process_status).from_right,
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
31
  'timeout' => object.timeout,
32
- 'value' => object.value && Mutant::Result::Test::JSON.dump(object.value).from_right
32
+ 'value' => object.value && Mutant::Result::Test::CODEC.dump(object.value).from_right
33
33
  }
34
34
  end
35
35
  )
@@ -38,11 +38,11 @@ module Mutant
38
38
  steps: [
39
39
  Transform::Hash.new(
40
40
  required: [
41
- Transform::Hash::Key.new(value: 'exception', transform: Transform::Nullable.new(transform: Mutant::Result::Exception::JSON.load_transform)),
42
- Transform::Hash::Key.new(value: 'log', transform: Transform::STRING),
43
- Transform::Hash::Key.new(value: 'process_status', transform: Transform::Nullable.new(transform: Mutant::Result::ProcessStatus::JSON.load_transform)),
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
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::JSON.load_transform))
45
+ Transform::Hash::Key.new(value: 'value', transform: Transform::Nullable.new(transform: Mutant::Result::Test::CODEC.load_transform))
46
46
  ],
47
47
  optional: []
48
48
  ),
@@ -51,7 +51,7 @@ module Mutant
51
51
  ]
52
52
  )
53
53
 
54
- JSON = Transform::JSON.new(dump_transform: dump, load_transform: load)
54
+ CODEC = Transform::Codec.new(dump_transform: dump, load_transform: load)
55
55
  end # Result
56
56
 
57
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
@@ -18,10 +18,10 @@ module Mutant
18
18
 
19
19
  private_class_method :new
20
20
 
21
- attr_reader :log
22
-
23
21
  def error = Util.max_one(@errors)
24
22
 
23
+ def log = LogCapture.from_binary(@log)
24
+
25
25
  def result = Util.max_one(@results)
26
26
 
27
27
  def initialize(*)
@@ -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
@@ -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
@@ -23,7 +23,7 @@ module Mutant
23
23
  )
24
24
  end
25
25
 
26
- JSON = Transform::JSON.for_anima(self)
26
+ CODEC = Transform::Codec.for_anima(self)
27
27
  end # Exception
28
28
  end # Result
29
29
  end # Mutant
@@ -24,7 +24,7 @@ module Mutant
24
24
  private
25
25
 
26
26
  def json
27
- JSON.generate(Session::JSON.dump(session).from_right)
27
+ JSON.generate(Session::CODEC.dump(session).from_right)
28
28
  end
29
29
 
30
30
  def session
@@ -31,7 +31,7 @@ module Mutant
31
31
  def success?
32
32
  exitstatus.equal?(0)
33
33
  end
34
- JSON = Transform::JSON.for_anima(self)
34
+ CODEC = Transform::Codec.for_anima(self)
35
35
  end # ProcessStatus
36
36
  end # Result
37
37
  end # Mutant
@@ -32,7 +32,7 @@ module Mutant
32
32
  'ruby_version' => object.ruby_version,
33
33
  'runtime' => object.runtime,
34
34
  'session_id' => object.session_id,
35
- 'subject_results' => object.subject_results.map { |subject_result| Subject::JSON.dump(subject_result).from_right }
35
+ 'subject_results' => object.subject_results.map { |subject_result| Subject::CODEC.dump(subject_result).from_right }
36
36
  }
37
37
  end
38
38
  )
@@ -47,7 +47,7 @@ module Mutant
47
47
  Transform::Hash::Key.new(value: 'ruby_version', transform: Transform::STRING),
48
48
  Transform::Hash::Key.new(value: 'runtime', transform: Transform::FLOAT),
49
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::JSON.load_transform))
50
+ Transform::Hash::Key.new(value: 'subject_results', transform: Transform::Array.new(transform: Subject::CODEC.load_transform))
51
51
  ],
52
52
  optional: []
53
53
  ),
@@ -56,7 +56,7 @@ module Mutant
56
56
  ]
57
57
  )
58
58
 
59
- JSON = Transform::JSON.new(dump_transform: dump, load_transform: load)
59
+ CODEC = Transform::Codec.new(dump_transform: dump, load_transform: load)
60
60
 
61
61
  end # Session
62
62
  end # Result
@@ -17,14 +17,41 @@ module Mutant
17
17
  def initialize
18
18
  super(
19
19
  job_index: nil,
20
- output: '',
20
+ output: LogCapture.from_binary(+''),
21
21
  passed: false,
22
22
  runtime: 0.0
23
23
  )
24
24
  end
25
25
  end # VoidValue
26
26
 
27
- JSON = Transform::JSON.for_anima(self)
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)
28
55
  end # Test
29
56
  end # Result
30
57
  end # Mutant
data/lib/mutant/result.rb CHANGED
@@ -150,7 +150,7 @@ module Mutant
150
150
  process_abort || test_result || timeout
151
151
  end
152
152
 
153
- JSON = Transform::JSON.for_anima(self)
153
+ CODEC = Transform::Codec.for_anima(self)
154
154
  end
155
155
 
156
156
  class MutationIndex
@@ -226,7 +226,7 @@ module Mutant
226
226
  dump = Transform::Success.new(
227
227
  block: lambda do |object|
228
228
  {
229
- 'isolation_result' => Isolation::Result::JSON.dump(object.isolation_result).from_right,
229
+ 'isolation_result' => Isolation::Result::CODEC.dump(object.isolation_result).from_right,
230
230
  'mutation_diff' => object.mutation_diff,
231
231
  'mutation_identification' => object.mutation_identification,
232
232
  'mutation_source' => object.mutation_source,
@@ -244,7 +244,7 @@ module Mutant
244
244
  steps: [
245
245
  Transform::Hash.new(
246
246
  required: [
247
- Transform::Hash::Key.new(value: 'isolation_result', transform: Isolation::Result::JSON.load_transform),
247
+ Transform::Hash::Key.new(value: 'isolation_result', transform: Isolation::Result::CODEC.load_transform),
248
248
  Transform::Hash::Key.new(value: 'mutation_diff', transform: Transform::OPTIONAL_STRING),
249
249
  Transform::Hash::Key.new(value: 'mutation_identification', transform: Transform::STRING),
250
250
  Transform::Hash::Key.new(value: 'mutation_source', transform: Transform::STRING),
@@ -259,7 +259,7 @@ module Mutant
259
259
  ]
260
260
  )
261
261
 
262
- JSON = Transform::JSON.new(dump_transform: dump, load_transform: load)
262
+ CODEC = Transform::Codec.new(dump_transform: dump, load_transform: load)
263
263
  end # Mutation
264
264
 
265
265
  # Coverage of a mutation against criteria
@@ -277,8 +277,8 @@ module Mutant
277
277
  dump = Transform::Success.new(
278
278
  block: lambda do |object|
279
279
  {
280
- 'mutation_result' => Mutation::JSON.dump(object.mutation_result).from_right,
281
- 'criteria_result' => CoverageCriteria::JSON.dump(object.criteria_result).from_right
280
+ 'mutation_result' => Mutation::CODEC.dump(object.mutation_result).from_right,
281
+ 'criteria_result' => CoverageCriteria::CODEC.dump(object.criteria_result).from_right
282
282
  }
283
283
  end
284
284
  )
@@ -287,8 +287,8 @@ module Mutant
287
287
  steps: [
288
288
  Transform::Hash.new(
289
289
  required: [
290
- Transform::Hash::Key.new(value: 'mutation_result', transform: Mutation::JSON.load_transform),
291
- Transform::Hash::Key.new(value: 'criteria_result', transform: CoverageCriteria::JSON.load_transform)
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
292
  ],
293
293
  optional: []
294
294
  ),
@@ -297,7 +297,7 @@ module Mutant
297
297
  ]
298
298
  )
299
299
 
300
- JSON = Transform::JSON.new(dump_transform: dump, load_transform: load)
300
+ CODEC = Transform::Codec.new(dump_transform: dump, load_transform: load)
301
301
  end # Coverage
302
302
 
303
303
  # Subject result
@@ -359,7 +359,7 @@ module Mutant
359
359
  {
360
360
  'amount_mutations' => object.amount_mutations,
361
361
  'coverage_results' => object.coverage_results
362
- .map { |coverage_result| Coverage::JSON.dump(coverage_result).from_right },
362
+ .map { |coverage_result| Coverage::CODEC.dump(coverage_result).from_right },
363
363
  'expression_syntax' => object.expression_syntax,
364
364
  'identification' => object.identification,
365
365
  'source' => object.source,
@@ -387,7 +387,7 @@ module Mutant
387
387
  Transform::Hash.new(
388
388
  required: [
389
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::JSON.load_transform)),
390
+ Transform::Hash::Key.new(value: 'coverage_results', transform: Transform::Array.new(transform: Coverage::CODEC.load_transform)),
391
391
  Transform::Hash::Key.new(value: 'expression_syntax', transform: Transform::STRING),
392
392
  Transform::Hash::Key.new(value: 'identification', transform: Transform::STRING),
393
393
  Transform::Hash::Key.new(value: 'source', transform: Transform::STRING),
@@ -405,7 +405,7 @@ module Mutant
405
405
  ]
406
406
  )
407
407
 
408
- JSON = Transform::JSON.new(dump_transform: dump, load_transform: load)
408
+ CODEC = Transform::Codec.new(dump_transform: dump, load_transform: load)
409
409
  end # Subject
410
410
  end # Result
411
411
  end # Mutant
@@ -38,7 +38,7 @@ module Mutant
38
38
  # @return [self]
39
39
  def response(response)
40
40
  if response.error
41
- env.world.stderr.puts(response.log)
41
+ env.world.stderr.puts(response.log.content)
42
42
  fail response.error
43
43
  end
44
44
 
@@ -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
data/lib/mutant.rb CHANGED
@@ -69,7 +69,8 @@ module Mutant
69
69
  record.call(:require_mutant_lib) do
70
70
  require 'mutant/procto'
71
71
  require 'mutant/transform'
72
- require 'mutant/transform/json'
72
+ require 'mutant/transform/codec'
73
+ require 'mutant/log_capture'
73
74
  require 'mutant/variable'
74
75
  require 'mutant/bootstrap'
75
76
  require 'mutant/version'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mutant
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.0
4
+ version: 0.16.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Markus Schirp
@@ -252,6 +252,7 @@ files:
252
252
  - lib/mutant/isolation/fork.rb
253
253
  - lib/mutant/isolation/none.rb
254
254
  - lib/mutant/loader.rb
255
+ - lib/mutant/log_capture.rb
255
256
  - lib/mutant/matcher.rb
256
257
  - lib/mutant/matcher/chain.rb
257
258
  - lib/mutant/matcher/config.rb
@@ -403,7 +404,7 @@ files:
403
404
  - lib/mutant/test/runner/sink.rb
404
405
  - lib/mutant/timer.rb
405
406
  - lib/mutant/transform.rb
406
- - lib/mutant/transform/json.rb
407
+ - lib/mutant/transform/codec.rb
407
408
  - lib/mutant/usage.rb
408
409
  - lib/mutant/util.rb
409
410
  - lib/mutant/variable.rb
@@ -430,7 +431,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
430
431
  - !ruby/object:Gem::Version
431
432
  version: '0'
432
433
  requirements: []
433
- rubygems_version: 3.6.9
434
+ rubygems_version: 4.0.6
434
435
  specification_version: 4
435
436
  summary: ''
436
437
  test_files: []
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Mutant
4
- class Transform
5
- # Bidirectional JSON transform
6
- #
7
- # Wraps a pair of dump/load transforms and optionally adds
8
- # JSON string serialization via .build
9
- class JSON
10
- include Anima.new(:dump_transform, :load_transform)
11
-
12
- # Build a JSON transform that wraps raw transforms with JSON.parse/generate
13
- #
14
- # @param [Transform] dump
15
- # @param [Transform] load
16
- #
17
- # @return [JSON]
18
- # rubocop:disable Metrics/MethodLength
19
- def self.build(dump:, load:)
20
- new(
21
- dump_transform: Sequence.new(
22
- steps: [
23
- dump,
24
- Success.new(block: ::JSON.public_method(:generate))
25
- ]
26
- ),
27
- load_transform: Sequence.new(
28
- steps: [
29
- Exception.new(error_class: ::JSON::ParserError, block: ::JSON.public_method(:parse)),
30
- load
31
- ]
32
- )
33
- )
34
- end
35
- # rubocop:enable Metrics/MethodLength
36
-
37
- # Build a JSON transform for simple Anima objects with JSON-primitive fields
38
- #
39
- # @param [Class] klass
40
- #
41
- # @return [JSON]
42
- def self.for_anima(klass)
43
- new(
44
- dump_transform: Success.new(block: ->(object) { object.to_h.transform_keys(&:to_s) }),
45
- load_transform: Success.new(block: ->(hash) { klass.new(**hash.transform_keys(&:to_sym)) })
46
- )
47
- end
48
-
49
- # Dump object to hash or JSON string
50
- #
51
- # @param [Object] object
52
- #
53
- # @return [Either<Error, Object>]
54
- def dump(object)
55
- dump_transform.call(object)
56
- end
57
-
58
- # Load object from hash or JSON string
59
- #
60
- # @param [Object] input
61
- #
62
- # @return [Either<Error, Object>]
63
- def load(input)
64
- load_transform.call(input)
65
- end
66
- end # JSON
67
- end # Transform
68
- end # Mutant