mutation_tester 1.5.1 → 1.6.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.
@@ -86,7 +86,7 @@ module MutationTester
86
86
  end
87
87
 
88
88
  result = run_single_mutation(mutation, :in_memory)
89
- progress_callback.call(mutation, index + 1) if progress_callback
89
+ progress_callback.call(mutation, index + 1, result) if progress_callback
90
90
  results << result
91
91
  break if stop_early?(result)
92
92
  end
@@ -113,20 +113,10 @@ module MutationTester
113
113
 
114
114
  warn "[MutationTester] In-memory execution selected (parallel, #{pool.size} preloaded workers, zero file writes per mutant)."
115
115
  announce_load_time_routing(mutations)
116
- total = mutations.size
117
- completed_count = 0
118
116
  collected = []
119
117
  project_root = discoverable_project_root
120
118
  reserve_fallback_shadow_root
121
-
122
- report_progress = lambda do |_item, _index, result|
123
- completed_count += 1
124
- collected << result
125
- if progress_callback && (completed_count.even? || completed_count == total)
126
- progress_callback.call(nil, completed_count)
127
- end
128
- raise Parallel::Break if stop_early?(result)
129
- end
119
+ report_progress = parallel_progress_reporter(collected, progress_callback)
130
120
 
131
121
  mapped = with_parallel_interrupt_silenced do
132
122
  Parallel.map(mutations, in_processes: pool.size, finish: report_progress) do |mutation|
@@ -141,21 +131,11 @@ module MutationTester
141
131
  end
142
132
 
143
133
  def run_in_shadow_parallel(mutations, &progress_callback)
144
- total = mutations.size
145
- completed_count = 0
146
134
  collected = []
147
135
  project_root = find_project_root
148
136
  shadow_run_root
149
- prepare_worker_preloads(total)
150
-
151
- report_progress = lambda do |_item, _index, result|
152
- completed_count += 1
153
- collected << result
154
- if progress_callback && (completed_count.even? || completed_count == total)
155
- progress_callback.call(nil, completed_count)
156
- end
157
- raise Parallel::Break if stop_early?(result)
158
- end
137
+ prepare_worker_preloads(mutations.size)
138
+ report_progress = parallel_progress_reporter(collected, progress_callback)
159
139
 
160
140
  mapped = with_parallel_interrupt_silenced do
161
141
  Parallel.map(mutations, in_processes: @config.parallel_processes, finish: report_progress) do |mutation|
@@ -180,7 +160,7 @@ module MutationTester
180
160
  results = []
181
161
  mutations.each_with_index do |mutation, index|
182
162
  result = run_single_mutation(mutation, :shadow, project_root)
183
- progress_callback.call(mutation, index + 1) if progress_callback
163
+ progress_callback.call(mutation, index + 1, result) if progress_callback
184
164
  results << result
185
165
  break if stop_early?(result)
186
166
  end
@@ -194,7 +174,7 @@ module MutationTester
194
174
  results = []
195
175
  mutations.each_with_index do |mutation, index|
196
176
  result = run_single_mutation(mutation, :in_place)
197
- progress_callback.call(mutation, index + 1) if progress_callback
177
+ progress_callback.call(mutation, index + 1, result) if progress_callback
198
178
  results << result
199
179
  break if stop_early?(result)
200
180
  end
@@ -218,6 +198,7 @@ module MutationTester
218
198
  status: :survived,
219
199
  description: mutation[:description]
220
200
  }.tap do |result|
201
+ result[:killed_by] = [] if @config.kill_matrix
221
202
  if unparseable?(mutation[:code])
222
203
  mark_stillborn(result)
223
204
  elsif strategy == :in_memory && mutation[:in_memory_safe] == false
@@ -370,17 +351,20 @@ module MutationTester
370
351
  runner = active_in_memory_runner
371
352
  return in_memory_worker_fallback(mutation, result, project_root, 'the in-memory worker is unavailable') unless runner&.ready?
372
353
 
373
- outcome = runner.execute_in_memory(
374
- source: mutation[:code],
375
- path: @source_file,
376
- timeout: @config.effective_timeout,
377
- chdir: Dir.pwd
378
- )
354
+ outcome, tests = recording_failed_tests do |recorder_env|
355
+ runner.execute_in_memory(
356
+ source: mutation[:code],
357
+ path: @source_file,
358
+ timeout: @config.effective_timeout,
359
+ chdir: Dir.pwd,
360
+ env: recorder_env
361
+ )
362
+ end
379
363
 
380
364
  if outcome.status == 'error'
381
365
  in_memory_apply_fallback(mutation, result, project_root, outcome.message || 'in-memory application failed')
382
366
  else
383
- apply_outcome(result, TestCommand::Result.new(outcome.status == 'pass', outcome.status == 'timeout'))
367
+ apply_outcome(result, TestCommand::Result.new(outcome.status == 'pass', outcome.status == 'timeout', nil, tests))
384
368
  end
385
369
  rescue MutationTester::Error => e
386
370
  return in_memory_worker_fallback(mutation, result, project_root, e.message) if project_root
@@ -402,6 +386,7 @@ module MutationTester
402
386
 
403
387
  def subset_filters(mutation)
404
388
  return [] unless @config.test_selection
389
+ return [] if @config.kill_matrix
405
390
  return [] unless detect_test_framework(@spec_file) == :rspec
406
391
 
407
392
  method_name = mutation[:method_name].to_s
@@ -417,6 +402,10 @@ module MutationTester
417
402
  FrameworkDetector.detect(spec_path)
418
403
  end
419
404
 
405
+ def recording_root
406
+ @recording_root ||= discoverable_project_root || File.dirname(@spec_file)
407
+ end
408
+
420
409
  private
421
410
 
422
411
  def with_parallel_interrupt_silenced
@@ -442,7 +431,7 @@ module MutationTester
442
431
  return 'the in-memory worker failed to preload the environment'
443
432
  end
444
433
 
445
- preloaded, message = runner.preload(@spec_file, chdir: Dir.pwd, stop_on_first_failure: true)
434
+ preloaded, message = runner.preload(@spec_file, chdir: Dir.pwd, stop_on_first_failure: stop_on_first_failure?)
446
435
  unless preloaded
447
436
  runner.shutdown
448
437
  return "the spec file could not be preloaded (#{message})"
@@ -479,8 +468,8 @@ module MutationTester
479
468
 
480
469
  def fall_back_to_file_based(reason, mutations, completed: 0, &progress_callback)
481
470
  announce_file_based_fallback(reason)
482
- offset_callback = progress_callback && lambda do |mutation, index|
483
- progress_callback.call(mutation, completed + index)
471
+ offset_callback = progress_callback && lambda do |mutation, index, result|
472
+ progress_callback.call(mutation, completed + index, result)
484
473
  end
485
474
  run_file_based_series(mutations, &offset_callback)
486
475
  end
@@ -615,6 +604,16 @@ module MutationTester
615
604
  @config.fail_fast && result[:status] == :survived
616
605
  end
617
606
 
607
+ def parallel_progress_reporter(collected, progress_callback)
608
+ completed_count = 0
609
+ lambda do |_item, _index, result|
610
+ completed_count += 1
611
+ collected << result
612
+ progress_callback.call(nil, completed_count, result) if progress_callback
613
+ raise Parallel::Break if stop_early?(result)
614
+ end
615
+ end
616
+
618
617
  def backup_path
619
618
  self.class.backup_path_for(@source_file)
620
619
  end
@@ -642,6 +641,17 @@ module MutationTester
642
641
  result[:status] = :killed
643
642
  end
644
643
  result[:kill_phase] = phase if result[:killed] && phase
644
+ result[:killed_by] = TestRecorder.failed_ids(outcome.tests) if @config.kill_matrix && result[:status] == :killed
645
+ end
646
+
647
+ def stop_on_first_failure?
648
+ !@config.kill_matrix
649
+ end
650
+
651
+ def recording_failed_tests(&block)
652
+ return [block.call(nil), nil] unless @config.kill_matrix
653
+
654
+ TestRecorder.capture(scope: :failures, root: recording_root, &block)
645
655
  end
646
656
 
647
657
  def mark_stillborn(result)
@@ -652,6 +662,7 @@ module MutationTester
652
662
  result[:killed] = false
653
663
  result[:timeout] = false
654
664
  result[:status] = :error
665
+ result[:killed_by] = [] if result.key?(:killed_by)
655
666
  result[:description] = "Error: #{error.message}"
656
667
  end
657
668
 
@@ -680,7 +691,9 @@ module MutationTester
680
691
  runner: @config.runner,
681
692
  example_filters: example_filters,
682
693
  worker_env_var: @config.worker_env_var,
683
- stop_on_first_failure: true
694
+ stop_on_first_failure: stop_on_first_failure?,
695
+ record: @config.kill_matrix ? :failures : nil,
696
+ record_root: @config.kill_matrix ? recording_root : nil
684
697
  )
685
698
  end
686
699
  end
@@ -1,43 +1,84 @@
1
1
  module MutationTester
2
2
  class ProgressDisplay
3
- attr_reader :total, :current, :current_mutation
4
-
5
- def initialize(total, config, output_stream: $stdout.clone)
3
+ MONOTONIC_CLOCK = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
4
+ ANSI_STYLE = /\e\[[\d;]*m/
5
+ SEPARATOR = ' | '
6
+ BAR_WIDTH = 20
7
+ LAYOUTS = [
8
+ { bar_width: BAR_WIDTH, compact: false },
9
+ { bar_width: BAR_WIDTH / 2, compact: false },
10
+ { bar_width: BAR_WIDTH / 2, compact: true },
11
+ { bar_width: 0, compact: true }
12
+ ].freeze
13
+ ESTIMATE_MIN_PROCESSED = 5
14
+ ESTIMATE_MIN_ELAPSED = 10
15
+
16
+ attr_reader :total, :current, :survived, :timed_out, :errored
17
+
18
+ def initialize(total, config, output_stream: $stdout.clone, clock: MONOTONIC_CLOCK)
6
19
  @output_stream = output_stream
7
20
  @total = total
8
21
  @current = 0
9
- @current_mutation = nil
22
+ @survived = 0
23
+ @timed_out = 0
24
+ @errored = 0
10
25
  @config = config
11
- @start_time = Time.now
26
+ @clock = clock
27
+ @start_time = @clock.call
28
+ @sample_start_time = nil
29
+ @sample_start_count = nil
30
+ @rendered_length = 0
12
31
  @spinner_frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
13
32
  @spinner_index = 0
14
33
  @lock = Mutex.new
15
34
  @spinner_thread = nil
16
35
  @stop_spinner = false
36
+ @halted = false
17
37
 
18
38
  start_spinner if @config.show_progress
19
39
  end
20
40
 
21
- def update(mutation, index)
41
+ def update(index, result = nil)
22
42
  @lock.synchronize do
43
+ @sample_start_time ||= @clock.call
44
+ @sample_start_count ||= index
23
45
  @current = index
24
- @current_mutation = mutation
46
+ tally(result) if result
25
47
  end
26
48
  end
27
49
 
28
50
  def finish
51
+ halt do
52
+ elapsed = @clock.call - @start_time
53
+ @output_stream.puts " #{Rainbow("Completed in #{format_duration(elapsed)}").green}"
54
+ end
55
+ end
56
+
57
+ def stop
58
+ halt
59
+ end
60
+
61
+ private
62
+
63
+ def halt
29
64
  @lock.synchronize do
30
65
  @stop_spinner = true
31
- if @config.show_progress
66
+ if @config.show_progress && !@halted
32
67
  clear_line
33
- elapsed = Time.now - @start_time
34
- @output_stream.puts " #{Rainbow("Completed in #{format_duration(elapsed)}").green}"
68
+ yield if block_given?
35
69
  end
70
+ @halted = true
36
71
  end
37
72
  stop_spinner
38
73
  end
39
74
 
40
- private
75
+ def tally(result)
76
+ case Reporters::BaseReporter.status_for(result)
77
+ when :survived then @survived += 1
78
+ when :timeout then @timed_out += 1
79
+ when :error then @errored += 1
80
+ end
81
+ end
41
82
 
42
83
  def render(already_locked = false)
43
84
  return unless @config.show_progress
@@ -51,22 +92,88 @@ module MutationTester
51
92
 
52
93
  def _render_internal
53
94
  clear_line
95
+ line = fitted_line
96
+ @rendered_length = visible_length(line)
97
+ @output_stream.print line
98
+ @output_stream.flush
99
+ end
100
+
101
+ def fitted_line
102
+ limit = line_limit
103
+ now = @clock.call
104
+ segments = []
105
+ LAYOUTS.each do |layout|
106
+ segments = line_segments(now, **layout)
107
+ break if fits?(segments, limit)
108
+ end
109
+ segments.pop until fits?(segments, limit)
110
+ segments.join(SEPARATOR)
111
+ end
112
+
113
+ def fits?(segments, limit)
114
+ limit.nil? || visible_length(segments.join(SEPARATOR)) <= limit
115
+ end
54
116
 
117
+ def line_segments(now, bar_width:, compact:)
118
+ processed = Rainbow("#{@current}/#{@total}").cyan
119
+
120
+ [
121
+ progress_segment(bar_width),
122
+ compact ? processed : "#{processed} processed",
123
+ time_segment(now, compact),
124
+ tally_segment
125
+ ]
126
+ end
127
+
128
+ def progress_segment(bar_width)
55
129
  progress = (@current.to_f / @total * 100).round(1)
56
- progress_bar = create_progress_bar(progress)
57
- spinner = @spinner_frames[@spinner_index]
130
+ spinner = Rainbow(@spinner_frames[@spinner_index]).magenta
131
+ percentage = Rainbow("#{progress}%").bright
132
+ return "#{spinner} #{percentage}" if bar_width.zero?
58
133
 
59
- mutation_info = @current_mutation ? " | #{@current_mutation[:type]} | line #{@current_mutation[:line]}" : ''
134
+ "#{spinner} [#{create_progress_bar(progress, bar_width)}] #{percentage}"
135
+ end
60
136
 
61
- line = "#{Rainbow(spinner).magenta} [#{progress_bar}] #{Rainbow(progress.to_s + "%").bright} | " \
62
- "#{Rainbow("#{@current}/#{@total}").cyan} mutations processed#{mutation_info}"
137
+ def time_segment(now, compact)
138
+ elapsed = format_duration((now - @start_time).floor)
139
+ remaining = estimated_remaining(now)
140
+ return "elapsed #{elapsed}" unless remaining
63
141
 
64
- @output_stream.print line
65
- @output_stream.flush
142
+ left = format_duration(remaining.ceil)
143
+ compact ? "#{elapsed}, ~#{left} left" : "elapsed #{elapsed}, remaining ~#{left}"
144
+ end
145
+
146
+ def tally_segment
147
+ survived = Rainbow("#{@survived} survived").color(@survived.zero? ? :green : :red)
148
+ segment = "#{survived}, #{@timed_out} timed out"
149
+ @errored.zero? ? segment : "#{segment}, #{Rainbow("#{@errored} errored").red}"
150
+ end
151
+
152
+ def estimated_remaining(now)
153
+ return if @sample_start_time.nil? || @current >= @total
154
+
155
+ sampled = @current - @sample_start_count
156
+ sample_elapsed = now - @sample_start_time
157
+ return unless sampled.positive?
158
+ return if sampled < ESTIMATE_MIN_PROCESSED && sample_elapsed < ESTIMATE_MIN_ELAPSED
159
+
160
+ sample_elapsed.fdiv(sampled) * (@total - @current)
161
+ end
162
+
163
+ def visible_length(text)
164
+ text.gsub(ANSI_STYLE, '').length
66
165
  end
67
166
 
68
- def create_progress_bar(percentage)
69
- width = 20
167
+ def line_limit
168
+ return unless @output_stream.respond_to?(:winsize) && @output_stream.tty?
169
+
170
+ columns = @output_stream.winsize[1]
171
+ columns - 1 if columns.positive?
172
+ rescue SystemCallError
173
+ nil
174
+ end
175
+
176
+ def create_progress_bar(percentage, width = BAR_WIDTH)
70
177
  filled = (percentage / 100.0 * width).round
71
178
  empty = width - filled
72
179
 
@@ -75,21 +182,18 @@ module MutationTester
75
182
 
76
183
  def clear_line
77
184
  @output_stream.print "\r"
78
- @output_stream.print ' ' * 100
185
+ @output_stream.print ' ' * @rendered_length
79
186
  @output_stream.print "\r"
80
187
  end
81
188
 
82
189
  def format_duration(seconds)
83
- if seconds < 60
84
- "#{seconds.round(1)}s"
85
- elsif seconds < 3600
86
- minutes = (seconds / 60).floor
87
- secs = (seconds % 60).round
88
- "#{minutes}m #{secs}s"
190
+ return "#{seconds.round(1)}s" if seconds.round(1) < 60
191
+
192
+ whole = seconds.round
193
+ if whole < 3600
194
+ "#{whole / 60}m #{whole % 60}s"
89
195
  else
90
- hours = (seconds / 3600).floor
91
- minutes = ((seconds % 3600) / 60).floor
92
- "#{hours}h #{minutes}m"
196
+ "#{whole / 3600}h #{whole % 3600 / 60}m"
93
197
  end
94
198
  end
95
199
 
@@ -17,8 +17,9 @@ module MutationTester
17
17
  result[:status] || (result[:killed] ? :killed : :survived)
18
18
  end
19
19
 
20
- def initialize(results, source_file, spec_file, config, interrupted: false)
20
+ def initialize(results, source_file, spec_file, config, interrupted: false, tests: [])
21
21
  @results = results
22
+ @tests = Array(tests)
22
23
  @source_file = source_file
23
24
  @spec_file = spec_file
24
25
  @config = config
@@ -63,7 +63,8 @@ module MutationTester
63
63
  File.expand_path(entry.source_file),
64
64
  File.expand_path(entry.spec_file),
65
65
  @config,
66
- interrupted: !!entry.interrupted
66
+ interrupted: !!entry.interrupted,
67
+ tests: entry.tests
67
68
  ).report_data
68
69
  end
69
70
  end
@@ -89,6 +89,7 @@ module MutationTester
89
89
 
90
90
  def selection_stats_available?
91
91
  return false unless @config.test_selection && FrameworkDetector.detect(@spec_file) == :rspec
92
+ return false if @config.kill_matrix
92
93
 
93
94
  @config.runner != :in_memory || @results.any? { |result| result.key?(:kill_phase) }
94
95
  end
@@ -21,6 +21,7 @@ module MutationTester
21
21
  {
22
22
  schema_version: SCHEMA_VERSION,
23
23
  interrupted: interrupted?,
24
+ **kill_matrix_fields,
24
25
  metadata: {
25
26
  version: MutationTester::VERSION,
26
27
  generated_at: Time.now.iso8601,
@@ -47,6 +48,12 @@ module MutationTester
47
48
 
48
49
  private
49
50
 
51
+ def kill_matrix_fields
52
+ return {} unless @config.kill_matrix
53
+
54
+ { kill_matrix: true, tests: @tests.map { |test| test.slice(:id, :name, :line, :status) } }
55
+ end
56
+
50
57
  def mutation_entry(result)
51
58
  status = status_of(result)
52
59
  entry = result.merge(status: status)
@@ -1,8 +1,9 @@
1
1
  require 'tempfile'
2
+ require_relative 'test_recorder'
2
3
 
3
4
  module MutationTester
4
5
  class TestCommand
5
- Result = Struct.new(:passed, :timed_out, :output) do
6
+ Result = Struct.new(:passed, :timed_out, :output, :tests) do
6
7
  alias_method :passed?, :passed
7
8
  alias_method :timed_out?, :timed_out
8
9
  end
@@ -13,7 +14,7 @@ module MutationTester
13
14
  attr_reader :spec_file, :framework, :use_bundle_exec, :example_filters
14
15
 
15
16
  def initialize(spec_file, use_bundle_exec:, framework: nil, runner: :spawn, example_filters: [], worker_env_var: nil,
16
- stop_on_first_failure: false)
17
+ stop_on_first_failure: false, record: nil, record_root: nil)
17
18
  @spec_file = spec_file
18
19
  @framework = framework || self.class.detect_framework(spec_file)
19
20
  @use_bundle_exec = use_bundle_exec
@@ -21,10 +22,12 @@ module MutationTester
21
22
  @example_filters = @framework == :rspec ? Array(example_filters) : []
22
23
  @worker_env_var = worker_env_var
23
24
  @stop_on_first_failure = stop_on_first_failure
25
+ @record = record
26
+ @record_root = record_root
24
27
  end
25
28
 
26
29
  def argv
27
- parts = [runner, *interpreter_args, spec_file, *filter_args, *fail_fast_args]
30
+ parts = [runner, *interpreter_args, spec_file, *filter_args, *fail_fast_args, *recorder_args]
28
31
  @use_bundle_exec ? ['bundle', 'exec', *parts] : parts
29
32
  end
30
33
 
@@ -41,29 +44,13 @@ module MutationTester
41
44
  end
42
45
 
43
46
  def run(timeout: nil, chdir: nil, capture: false, mirror_of: nil)
44
- if fork_execution?
45
- fork_runner = ForkRunner.acquire(use_bundle_exec: @use_bundle_exec, framework: @framework)
46
- if fork_runner
47
- return fork_runner.execute(
48
- spec_file,
49
- timeout: timeout,
50
- chdir: chdir || Dir.pwd,
51
- capture: capture,
52
- args: filter_args,
53
- stop_on_first_failure: @stop_on_first_failure,
54
- mirror_of: mirror_of,
55
- env: worker_env_overrides
56
- )
57
- end
58
- end
59
-
60
- return run_captured(timeout: timeout, chdir: chdir) if capture
61
-
62
- spawn_options = { pgroup: true, %i[out err] => File::NULL }
63
- spawn_options[:chdir] = chdir if chdir
47
+ return execute(timeout: timeout, chdir: chdir, capture: capture, mirror_of: mirror_of) unless @record
64
48
 
65
- pid = Process.spawn(*spawn_argv, spawn_options)
66
- wait_with_deadline(pid, timeout)
49
+ result, tests = TestRecorder.capture(scope: @record, root: mirror_of ? chdir : @record_root) do |recorder_env|
50
+ execute(timeout: timeout, chdir: chdir, capture: capture, mirror_of: mirror_of, recorder_env: recorder_env)
51
+ end
52
+ result.tests = tests
53
+ result
67
54
  end
68
55
 
69
56
  def fork_execution?
@@ -93,9 +80,42 @@ module MutationTester
93
80
 
94
81
  private
95
82
 
96
- def spawn_argv
83
+ def execute(timeout:, chdir:, capture:, mirror_of:, recorder_env: nil)
84
+ env = child_env(recorder_env)
85
+ if fork_execution?
86
+ fork_runner = ForkRunner.acquire(use_bundle_exec: @use_bundle_exec, framework: @framework)
87
+ if fork_runner
88
+ return fork_runner.execute(
89
+ spec_file,
90
+ timeout: timeout,
91
+ chdir: chdir || Dir.pwd,
92
+ capture: capture,
93
+ args: filter_args,
94
+ stop_on_first_failure: @stop_on_first_failure,
95
+ mirror_of: mirror_of,
96
+ env: env
97
+ )
98
+ end
99
+ end
100
+
101
+ return run_captured(timeout: timeout, chdir: chdir, env: env) if capture
102
+
103
+ spawn_options = { pgroup: true, %i[out err] => File::NULL }
104
+ spawn_options[:chdir] = chdir if chdir
105
+
106
+ pid = Process.spawn(*spawn_argv(env), spawn_options)
107
+ wait_with_deadline(pid, timeout)
108
+ end
109
+
110
+ def spawn_argv(env)
111
+ env ? [env, *argv] : argv
112
+ end
113
+
114
+ def child_env(recorder_env)
97
115
  overrides = worker_env_overrides
98
- overrides ? [overrides, *argv] : argv
116
+ return overrides unless recorder_env
117
+
118
+ (overrides || {}).merge(recorder_env)
99
119
  end
100
120
 
101
121
  def worker_env_overrides
@@ -114,9 +134,18 @@ module MutationTester
114
134
  end
115
135
 
116
136
  def interpreter_args
117
- return [] unless @stop_on_first_failure && @framework == :minitest
137
+ return [] unless @framework == :minitest
138
+
139
+ args = []
140
+ args += ['-r', MINITEST_FAIL_FAST_PATH] if @stop_on_first_failure
141
+ args += ['-r', TestRecorder::MINITEST_HOOK_PATH] if @record
142
+ args
143
+ end
144
+
145
+ def recorder_args
146
+ return [] unless @record && @framework == :rspec
118
147
 
119
- ['-r', MINITEST_FAIL_FAST_PATH]
148
+ ['--require', TestRecorder::RSPEC_HOOK_PATH]
120
149
  end
121
150
 
122
151
  def fail_fast_args
@@ -125,12 +154,12 @@ module MutationTester
125
154
  ['--fail-fast']
126
155
  end
127
156
 
128
- def run_captured(timeout:, chdir:)
157
+ def run_captured(timeout:, chdir:, env:)
129
158
  log = Tempfile.new(['mutation_tester_baseline', '.log'])
130
159
  spawn_options = { pgroup: true, %i[out err] => log.path }
131
160
  spawn_options[:chdir] = chdir if chdir
132
161
 
133
- pid = Process.spawn(*spawn_argv, spawn_options)
162
+ pid = Process.spawn(*spawn_argv(env), spawn_options)
134
163
  result = wait_with_deadline(pid, timeout)
135
164
  result.output = File.read(log.path)
136
165
  result
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../test_recorder'
4
+ require_relative '../minitest_load_hook'
5
+
6
+ MutationTester::MinitestLoadHook.on_load do
7
+ module MutationTester
8
+ module TestRecorder
9
+ module MinitestHook
10
+ PLUGIN_NAME = :mutation_tester_test_recorder
11
+
12
+ class Reporter < Minitest::AbstractReporter
13
+ def record(result)
14
+ return unless TestRecorder.active?
15
+
16
+ TestRecorder.write([entry_for(result)])
17
+ end
18
+
19
+ private
20
+
21
+ def entry_for(result)
22
+ owner = result.respond_to?(:klass) && result.klass ? result.klass : result.class.name
23
+ {
24
+ id: "#{owner}##{result.name}",
25
+ name: result.name,
26
+ line: line_of(result),
27
+ status: status_of(result)
28
+ }
29
+ end
30
+
31
+ def line_of(result)
32
+ location = result.respond_to?(:source_location) ? result.source_location : nil
33
+ location.is_a?(Array) ? location[1] : nil
34
+ end
35
+
36
+ def status_of(result)
37
+ return 'skipped' if result.skipped?
38
+
39
+ result.passed? ? 'passed' : 'failed'
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ module Minitest
47
+ def self.plugin_mutation_tester_test_recorder_init(_options)
48
+ reporter << MutationTester::TestRecorder::MinitestHook::Reporter.new
49
+ end
50
+ end
51
+
52
+ unless Minitest.extensions.include?(MutationTester::TestRecorder::MinitestHook::PLUGIN_NAME)
53
+ Minitest.register_plugin(MutationTester::TestRecorder::MinitestHook::PLUGIN_NAME)
54
+ end
55
+ end