mutant 0.15.1 → 0.16.0

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/lib/mutant/cli/command/root.rb +1 -1
  4. data/lib/mutant/cli/command/session.rb +281 -0
  5. data/lib/mutant/cli/command.rb +16 -2
  6. data/lib/mutant/config.rb +1 -1
  7. data/lib/mutant/expression/method.rb +0 -2
  8. data/lib/mutant/expression/methods.rb +0 -2
  9. data/lib/mutant/expression/namespace.rb +0 -2
  10. data/lib/mutant/isolation/fork.rb +2 -6
  11. data/lib/mutant/isolation.rb +31 -0
  12. data/lib/mutant/matcher/null.rb +1 -1
  13. data/lib/mutant/mutation/runner/sink.rb +23 -10
  14. data/lib/mutant/mutation/runner.rb +1 -0
  15. data/lib/mutant/mutation.rb +3 -20
  16. data/lib/mutant/mutator/node/literal/integer.rb +61 -0
  17. data/lib/mutant/parallel/connection.rb +0 -2
  18. data/lib/mutant/parallel/driver.rb +0 -2
  19. data/lib/mutant/reporter/cli/printer/alive_results.rb +27 -0
  20. data/lib/mutant/reporter/cli/printer/env_result.rb +52 -9
  21. data/lib/mutant/reporter/cli/printer/subject_result.rb +103 -5
  22. data/lib/mutant/reporter/cli/printer.rb +24 -1
  23. data/lib/mutant/repository/diff.rb +1 -2
  24. data/lib/mutant/result/exception.rb +29 -0
  25. data/lib/mutant/result/json_writer.rb +43 -0
  26. data/lib/mutant/result/process_status.rb +37 -0
  27. data/lib/mutant/result/session.rb +63 -0
  28. data/lib/mutant/result/test.rb +30 -0
  29. data/lib/mutant/result.rb +201 -96
  30. data/lib/mutant/segment/recorder.rb +0 -2
  31. data/lib/mutant/timer.rb +3 -1
  32. data/lib/mutant/transform/json.rb +68 -0
  33. data/lib/mutant/transform.rb +33 -25
  34. data/lib/mutant/world.rb +6 -0
  35. data/lib/mutant/zombifier.rb +0 -2
  36. data/lib/mutant.rb +13 -4
  37. metadata +33 -7
  38. data/lib/mutant/reporter/cli/printer/coverage_result.rb +0 -19
  39. 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: d35acd82d2b4cf6222613f653da44131636400215c56a14a00f1ecd2ef212079
4
+ data.tar.gz: ae7ad7d71685be5108bb5fcc6be1b0cae5cd32f56e2848d25467c82d2f72771c
5
5
  SHA512:
6
- metadata.gz: 27d3948ff6e67c670aefe552fad358d333ee584d48090f13b14d15bee01a756dd6982be5922200dd0d2d939916b9d97e02c862b5d3487c9fd7572688affa544d
7
- data.tar.gz: 3155f5599aa51d7d8644fbac0f4f6edb0f2e63d26bd70ca87f20b820f492ba07fa773ec974ad474f3ec08b652acc89eb23033124925c37f933e84eee803f0219
6
+ metadata.gz: 1481530eb5c56560e06a828086df6d8a5f12d47b3180bafc47f996ccd3a6db22b2cd0ddd213bb12c6934cf878341bad5f987f8cb91113ad0db58a0c68760d203
7
+ data.tar.gz: 657f62dc386d13f729dfb92fc78afed7c426cd4e64f53ca938c403c6cd4ad06ad856b379dde9d890d2a96ae692f64521975d9d3e0fd850095a4b03ab82d9b906
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.15.1
1
+ 0.16.0
@@ -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::JSON.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/
@@ -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
@@ -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::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,
31
+ 'timeout' => object.timeout,
32
+ 'value' => object.value && Mutant::Result::Test::JSON.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::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)),
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))
46
+ ],
47
+ optional: []
48
+ ),
49
+ Transform::Hash::Symbolize.new,
50
+ Transform::Success.new(block: method(:new).to_proc)
51
+ ]
52
+ )
53
+
54
+ JSON = Transform::JSON.new(dump_transform: dump, load_transform: load)
24
55
  end # Result
25
56
 
26
57
  # Call block in isolation
@@ -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,8 +16,6 @@ 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
21
  attr_reader :log
@@ -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