Class: Tryouts::TestBatch

Inherits:
Object
  • Object
show all
Defined in:
lib/tryouts/test_batch.rb

Overview

Modern TestBatch using Ruby 3.4+ patterns and formatter system

Constant Summary collapse

OUTPUT_CAPTURE_MONITOR =

$stdout/$stderr are process-global, not Fiber- or thread-local. Under --parallel (Concurrent::ThreadPoolExecutor), concurrent redirects would clobber each other and could leave $stdout pointing at a dead StringIO process-wide. Every redirect path (capture_output for setup/teardown and each test, plus execute_with_output_capture) synchronizes on this lock so the redirect/execute/restore window is serialized across threads.

A Monitor (reentrant) is required because execute_with_output_capture runs nested inside capture_output; a plain Mutex would self-deadlock.

Monitor.new

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(testrun, **options) ⇒ TestBatch

Returns a new instance of TestBatch.



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/tryouts/test_batch.rb', line 41

def initialize(testrun, **options)
  @testrun         = testrun
  @container       = Object.new
  @options         = options
  @formatter       = Tryouts::CLI::FormatterFactory.create_formatter(options)
  @output_manager  = options[:output_manager]
  @global_tally    = options[:global_tally]
  @failed_count    = 0
  @status          = :pending
  @results         = []
  @start_time      = nil
  @test_case_count = 0
  @setup_failed    = false
  @orphan_failed   = false

  # Shared-context mode evaluates every block against this one reused
  # Binding so local variables persist across blocks like a plain Ruby
  # script. Fresh-context mode keeps per-container instance_eval isolation.
  @binding = options[:shared_context] ? acquire_container_binding(@container) : nil
  @line_spec       = options[:line_spec]  # For output filtering only

  # Setup container for fresh context mode - preserves @instance_variables from setup
  @setup_container = nil

  # Circuit breaker for batch-level failure protection
  @consecutive_failures     = 0
  @max_consecutive_failures = options[:max_consecutive_failures] || 10
  @circuit_breaker_active   = false

  # Expose context objects for testing - different strategies for each mode
  @shared_context = if options[:shared_context]
                      @container  # Shared mode: single container reused across tests
                    else
                      FreshContextFactory.new  # Fresh mode: factory that creates new containers
                    end
end

Instance Attribute Details

#containerObject (readonly)

Returns the value of attribute container.



39
40
41
# File 'lib/tryouts/test_batch.rb', line 39

def container
  @container
end

#failed_countObject (readonly)

Returns the value of attribute failed_count.



39
40
41
# File 'lib/tryouts/test_batch.rb', line 39

def failed_count
  @failed_count
end

#formatterObject (readonly)

Returns the value of attribute formatter.



39
40
41
# File 'lib/tryouts/test_batch.rb', line 39

def formatter
  @formatter
end

#output_managerObject (readonly)

Returns the value of attribute output_manager.



39
40
41
# File 'lib/tryouts/test_batch.rb', line 39

def output_manager
  @output_manager
end

#resultsObject (readonly)

Returns the value of attribute results.



39
40
41
# File 'lib/tryouts/test_batch.rb', line 39

def results
  @results
end

#statusObject (readonly)

Returns the value of attribute status.



39
40
41
# File 'lib/tryouts/test_batch.rb', line 39

def status
  @status
end

#testrunObject (readonly)

Returns the value of attribute testrun.



39
40
41
# File 'lib/tryouts/test_batch.rb', line 39

def testrun
  @testrun
end

Instance Method Details

#completed?Boolean

Returns:

  • (Boolean)


206
207
208
# File 'lib/tryouts/test_batch.rb', line 206

def completed?
  @status == :completed
end

#empty?Boolean

Returns:

  • (Boolean)


186
187
188
# File 'lib/tryouts/test_batch.rb', line 186

def empty?
  @testrun.empty?
end

#failed?Boolean

Returns:

  • (Boolean)


202
203
204
# File 'lib/tryouts/test_batch.rb', line 202

def failed?
  @failed_count > 0
end

#pathObject



198
199
200
# File 'lib/tryouts/test_batch.rb', line 198

def path
  @testrun.source_file
end

#run(before_test_hook = nil) ⇒ Object

Main execution pipeline using functional composition



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/tryouts/test_batch.rb', line 79

def run(before_test_hook = nil, &)
  return false if empty?

  @start_time      = Time.now
  @test_case_count = @testrun.total_tests

  @output_manager&.execution_phase(@test_case_count)
  @output_manager&.info("Context: #{@options[:shared_context] ? 'shared' : 'fresh'}", 1)
  @output_manager&.file_start(path, context: @options[:shared_context] ? :shared : :fresh)

  if shared_context?
    @output_manager&.info('Running global setup...', 2)
    execute_global_setup

    # Stop execution if setup failed
    if @setup_failed
      @output_manager&.error('Stopping batch execution due to setup failure')
      @status = :failed
      finalize_results([])
      return false
    end
  else
    # Fresh context mode: execute setup once to establish shared @instance_variables
    @output_manager&.info('Running setup for fresh context...', 2)
    execute_fresh_context_setup

    # Stop execution if setup failed
    if @setup_failed
      @output_manager&.error('Stopping batch execution due to setup failure')
      @status = :failed
      finalize_results([])
      return false
    end
  end

  idx               = 0
  execution_results = test_cases.map do |test_case|
    # Orphan blocks run for side effects only, in source order. A raise
    # aborts the batch like a setup failure: every subsequent test's
    # context is suspect.
    if test_case.is_a?(OrphanBlock)
      execute_orphan_block(test_case)
      break if @orphan_failed

      next
    end

    @output_manager&.trace("Test #{idx + 1}/#{@test_case_count}: #{test_case.description}", 2)
    idx += 1

    # Check circuit breaker before executing test
    if @circuit_breaker_active
      @output_manager&.error("Circuit breaker active - skipping remaining tests after #{@consecutive_failures} consecutive failures")
      break
    end

    # Apply line_spec filter for output notifications (test_start/test_end)
    # but still execute ALL tests regardless of filter
    if should_display_test_result?(test_case)
      @output_manager&.test_start(test_case, idx, @test_case_count)
    end

    result = execute_single_test(test_case, before_test_hook, &) # runs the test code

    if should_display_test_result?(test_case)
      @output_manager&.test_end(test_case, idx, @test_case_count)
    end

    # Update circuit breaker state based on result
    update_circuit_breaker(result)

    result
  rescue StandardError => ex
    if should_display_test_result?(test_case)
      @output_manager&.test_end(test_case, idx, @test_case_count)
    end
    # Create error result packet to maintain consistent data flow
    error_result = build_error_result(test_case, ex)
    process_test_result(error_result)

    # Update circuit breaker for exception cases
    update_circuit_breaker(error_result)

    error_result
  end

  if @orphan_failed
    @output_manager&.error('Stopping batch execution due to orphan code block failure')
    @status = :failed
    @failed_count += 1
    finalize_results(execution_results || [])
    return false
  end

  # Used for a separate purpose then execution_phase.
  # e.g. the quiet formatter prints a newline after all test dots
  @output_manager&.file_end(path, context: @options[:shared_context] ? :shared : :fresh)

  @output_manager&.execution_phase(test_cases.size)

  execute_global_teardown
  finalize_results(execution_results)

  @status = :completed
  !failed?
end

#sizeObject



190
191
192
# File 'lib/tryouts/test_batch.rb', line 190

def size
  @testrun.total_tests
end

#test_casesObject



194
195
196
# File 'lib/tryouts/test_batch.rb', line 194

def test_cases
  @testrun.test_cases
end