tryouts 3.3.1 → 3.4.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.
@@ -0,0 +1,157 @@
1
+ # lib/tryouts/cli/formatters/token_budget.rb
2
+
3
+ class Tryouts
4
+ class CLI
5
+ # Token budget tracking for agent-optimized output
6
+ class TokenBudget
7
+ DEFAULT_LIMIT = 5000
8
+ BUFFER_PERCENT = 0.05 # 5% buffer to avoid going over
9
+
10
+ attr_reader :limit, :used, :remaining
11
+
12
+ def initialize(limit = DEFAULT_LIMIT)
13
+ @limit = limit
14
+ @used = 0
15
+ @buffer_size = (@limit * BUFFER_PERCENT).to_i
16
+ end
17
+
18
+ # Estimate tokens in text (rough approximation: 1 token ≈ 4 characters)
19
+ def estimate_tokens(text)
20
+ return 0 if text.nil? || text.empty?
21
+
22
+ (text.length / 4.0).ceil
23
+ end
24
+
25
+ # Check if text would exceed budget
26
+ def would_exceed?(text)
27
+ token_count = estimate_tokens(text)
28
+ (@used + token_count) > (@limit - @buffer_size)
29
+ end
30
+
31
+ # Add text to budget if within limits
32
+ def consume(text)
33
+ return false if would_exceed?(text)
34
+
35
+ @used += estimate_tokens(text)
36
+ true
37
+ end
38
+
39
+ # Force consume (for critical information that must be included)
40
+ def force_consume(text)
41
+ @used += estimate_tokens(text)
42
+ true
43
+ end
44
+
45
+ # Get remaining budget
46
+ def remaining
47
+ [@limit - @used - @buffer_size, 0].max
48
+ end
49
+
50
+ # Check if we have budget remaining
51
+ def has_budget?
52
+ remaining > 0
53
+ end
54
+
55
+ # Get utilization percentage
56
+ def utilization
57
+ (@used.to_f / @limit * 100).round(1)
58
+ end
59
+
60
+ # Try to fit text within remaining budget by truncating
61
+ def fit_text(text, preserve_suffix: nil)
62
+ token_count = estimate_tokens(text)
63
+
64
+ return text if token_count <= remaining
65
+ return '' unless has_budget?
66
+
67
+ # Calculate how many characters we can fit
68
+ max_chars = remaining * 4
69
+
70
+ if preserve_suffix
71
+ suffix_chars = preserve_suffix.length
72
+ return preserve_suffix if max_chars <= suffix_chars
73
+
74
+ available_chars = max_chars - suffix_chars - 3 # 3 for "..."
75
+ return "#{text[0, available_chars]}...#{preserve_suffix}"
76
+ else
77
+ return text[0, max_chars - 3] + '...' if max_chars > 3
78
+ return ''
79
+ end
80
+ end
81
+
82
+ # Smart truncate for different data types
83
+ def smart_truncate(value, max_tokens: nil)
84
+ max_tokens ||= [remaining / 2, 50].min # Use half remaining or 50, whichever is smaller
85
+ max_chars = [max_tokens.to_i * 4, 0].max
86
+
87
+ case value
88
+ when String
89
+ return value if value.length <= max_chars
90
+ return '...' if max_chars <= 3
91
+ "#{value[0, max_chars - 3]}..."
92
+ when Array
93
+ if estimate_tokens(value.inspect) <= max_tokens
94
+ value.inspect
95
+ else
96
+ # Show first few elements
97
+ truncated = []
98
+ char_count = 2 # for "[]"
99
+
100
+ value.each do |item|
101
+ item_str = item.inspect
102
+ if char_count + item_str.length + 2 <= max_chars - 10 # 10 chars for ", ..."
103
+ truncated << item
104
+ char_count += item_str.length + 2 # +2 for ", "
105
+ else
106
+ break
107
+ end
108
+ end
109
+
110
+ "[#{truncated.map(&:inspect).join(', ')}, ...#{value.size - truncated.size} more]"
111
+ end
112
+ when Hash
113
+ if estimate_tokens(value.inspect) <= max_tokens
114
+ value.inspect
115
+ else
116
+ # Show first few key-value pairs
117
+ truncated = {}
118
+ char_count = 2 # for "{}"
119
+
120
+ value.each do |key, val|
121
+ pair_str = "#{key.inspect}=>#{val.inspect}"
122
+ if char_count + pair_str.length + 2 <= max_chars - 10
123
+ truncated[key] = val
124
+ char_count += pair_str.length + 2
125
+ else
126
+ break
127
+ end
128
+ end
129
+
130
+ "{#{truncated.map { |k, v| "#{k.inspect}=>#{v.inspect}" }.join(', ')}, ...#{value.size - truncated.size} more}"
131
+ end
132
+ else
133
+ smart_truncate(value.to_s, max_tokens: max_tokens)
134
+ end
135
+ end
136
+
137
+ # Distribution strategy for budget allocation
138
+ def allocate_budget
139
+ {
140
+ summary: (@limit * 0.20).to_i, # 20% for file summaries
141
+ failures: (@limit * 0.60).to_i, # 60% for failure details
142
+ context: (@limit * 0.15).to_i, # 15% for additional context
143
+ buffer: (@limit * 0.05).to_i # 5% buffer
144
+ }
145
+ end
146
+
147
+ # Reset budget
148
+ def reset
149
+ @used = 0
150
+ end
151
+
152
+ def to_s
153
+ "TokenBudget[#{@used}/#{@limit} tokens (#{utilization}%)]"
154
+ end
155
+ end
156
+ end
157
+ end
@@ -12,6 +12,7 @@ class Tryouts
12
12
  @show_passed = options.fetch(:show_passed, true)
13
13
  @show_debug = options.fetch(:debug, false)
14
14
  @show_trace = options.fetch(:trace, false)
15
+ @show_stack_traces = options.fetch(:stack_traces, false) || options.fetch(:debug, false)
15
16
  end
16
17
 
17
18
  # Phase-level output
@@ -244,11 +245,11 @@ class Tryouts
244
245
  error_msg = Console.color(:red, "ERROR: #{message}")
245
246
  puts indent_text(error_msg, 1)
246
247
 
247
- return unless backtrace && @show_debug
248
+ return unless backtrace && @show_stack_traces
248
249
 
249
250
  puts indent_text('Details:', 2)
250
251
  # Show first 10 lines of backtrace to avoid overwhelming output
251
- backtrace.first(10).each do |line|
252
+ Console.pretty_backtrace(backtrace, limit: 10).each do |line|
252
253
  puts indent_text(line, 3)
253
254
  end
254
255
  puts indent_text("... (#{backtrace.length - 10} more lines)", 3) if backtrace.length > 10
@@ -6,4 +6,6 @@ require_relative 'formatters/quiet'
6
6
  require_relative 'formatters/verbose'
7
7
  require_relative 'formatters/test_run_state'
8
8
  require_relative 'formatters/tty_status_display'
9
+ require_relative 'formatters/token_budget'
10
+ require_relative 'formatters/agent'
9
11
  require_relative 'formatters/factory'
@@ -15,19 +15,72 @@ class Tryouts
15
15
  try --direct --shared-context test_try.rb # Explicit shared context
16
16
  try --generate-rspec test_try.rb # Output RSpec code only
17
17
  try --inspect test_try.rb # Inspect file structure and validation
18
-
19
- File Format:
20
- ## Test description # Test case marker
21
- code_to_test # Ruby code
22
- #=> expected_result # Expectation (various types available)
18
+ try --agent test_try.rb # Agent-optimized structured output
19
+ try --agent --agent-limit 10000 tests/ # Agent mode with 10K token limit
20
+
21
+ Agent Output Modes:
22
+ --agent # Structured, token-efficient output
23
+ --agent-focus summary # Show counts and problem files only
24
+ --agent-focus first-failure # Show first failure per file
25
+ --agent-focus critical # Show errors/exceptions only
26
+ --agent-limit 1000 # Limit output to 1000 tokens
27
+
28
+ File Naming & Organization:
29
+ Files must end with '_try.rb' or '.try.rb' (e.g., auth_service_try.rb, user_model.try.rb)
30
+ Auto-discovery searches: ./try/, ./tryouts/, ./*_try.rb, ./*.try.rb patterns
31
+ Organize by feature/module: try/models/, try/services/, try/api/
32
+
33
+ Testcase Structure (3 required parts)
34
+ ## This is the description
35
+ echo 'This is ruby code under test'
36
+ true
37
+ #=> true # this is the expected result
38
+
39
+ File Structure (3 sections):
40
+ # Setup section (optional) - runs once before all tests
41
+ @shared_var = "available to all test cases"
42
+
43
+ ## TEST: Feature description
44
+ # Test case body with plain Ruby code
45
+ result = some_operation()
46
+ #=> expected_value
47
+
48
+ # Teardown section (optional) - runs once after all tests
49
+
50
+ Context Guidelines:
51
+ Shared Context (default): Instance variables persist across test cases
52
+ - Use for: Integration testing, stateful scenarios, realistic workflows
53
+ - Caution: Test order matters, state accumulates
54
+
55
+ Fresh Context (--rspec/--minitest): Each test gets isolated environment
56
+ - Use for: Unit testing, independent test cases
57
+ - Setup variables copied to each test, but changes don't persist
58
+
59
+ Writing Quality Tryouts:
60
+ - Use realistic, plain Ruby code (avoid mocks, test harnesses)
61
+ - Test descriptions start with ##, be specific about what's being tested
62
+ - One result per test case (last expression is the result)
63
+ - Use appropriate expectation types for clarity (#==> for boolean, #=:> for types)
64
+ - Keep tests focused and readable - they serve as documentation
23
65
 
24
66
  Great Expectations System:
25
- Multiple expectation types are supported for different testing needs.
26
-
27
67
  #=> Value equality #==> Must be true #=/=> Must be false
28
68
  #=|> True OR false #=!> Must raise error #=:> Type matching
29
- #=~> Regex matching #=%> Time constraints #=1> STDOUT content
30
- #=2> STDERR content #=<> Intentional failure
69
+ #=~> Regex matching #=%> Time constraints #=*> Non-nil result
70
+ #=1> STDOUT content #=2> STDERR content #=<> Intentional failure
71
+
72
+ Exception Testing:
73
+ # Method 1: Rescue and test exception
74
+ begin
75
+ risky_operation
76
+ rescue StandardError => e
77
+ e.class
78
+ end
79
+ #=> StandardError
80
+
81
+ # Method 2: Let it raise and test with #=!>
82
+ risky_operation
83
+ #=!> error.is_a?(StandardError)
31
84
  HELP
32
85
 
33
86
  class << self
@@ -65,6 +118,24 @@ class Tryouts
65
118
  opts.on('-q', '--quiet', 'Minimal output (dots and summary only)') { options[:quiet] = true }
66
119
  opts.on('-c', '--compact', 'Compact single-line output') { options[:compact] = true }
67
120
  opts.on('-l', '--live', 'Live status display') { options[:live_status] = true }
121
+ opts.on('-j', '--parallel [THREADS]', 'Run test files in parallel (optional thread count)') do |threads|
122
+ options[:parallel] = true
123
+ options[:parallel_threads] = threads.to_i if threads && threads.to_i > 0
124
+ end
125
+
126
+ opts.separator "\nAgent-Optimized Output:"
127
+ opts.on('-a', '--agent', 'Agent-optimized structured output for LLM context management') do
128
+ options[:agent] = true
129
+ end
130
+ opts.on('--agent-limit TOKENS', Integer, 'Limit total output to token budget (default: 5000)') do |limit|
131
+ options[:agent] = true
132
+ options[:agent_limit] = limit
133
+ end
134
+ opts.on('--agent-focus TYPE', %w[failures first-failure summary critical],
135
+ 'Focus mode: failures, first-failure, summary, critical (default: failures)') do |focus|
136
+ options[:agent] = true
137
+ options[:agent_focus] = focus.to_sym
138
+ end
68
139
 
69
140
  opts.separator "\nParser Options:"
70
141
  opts.on('--enhanced-parser', 'Use enhanced parser with inhouse comment extraction (default)') { options[:parser] = :enhanced }
@@ -74,10 +145,16 @@ class Tryouts
74
145
  opts.on('-i', '--inspect', 'Inspect file structure without running tests') { options[:inspect] = true }
75
146
 
76
147
  opts.separator "\nGeneral Options:"
148
+ opts.on('-s', '--stack-traces', 'Show stack traces for exceptions') do
149
+ options[:stack_traces] = true
150
+ Tryouts.stack_traces = true
151
+ end
77
152
  opts.on('-V', '--version', 'Show version') { options[:version] = true }
78
153
  opts.on('-D', '--debug', 'Enable debug mode') do
79
154
  options[:debug] = true
155
+ options[:stack_traces] = true # Debug mode auto-enables stack traces
80
156
  Tryouts.debug = true
157
+ Tryouts.stack_traces = true
81
158
  end
82
159
  opts.on('-h', '--help', 'Show this help') do
83
160
  puts opts
@@ -161,12 +161,40 @@ class Tryouts
161
161
  # directory. This simplifies logging and error reporting by showing
162
162
  # only the relevant parts of file paths instead of lengthy absolute paths.
163
163
  #
164
- def pretty_path(file)
165
- return nil if file.nil?
164
+ def pretty_path(filepath)
165
+ return nil if filepath.nil? || filepath.empty?
166
166
 
167
- file = File.expand_path(file) # be absolutely sure
168
167
  basepath = Dir.pwd
169
- Pathname.new(file).relative_path_from(basepath).to_s
168
+ begin
169
+ relative_path = Pathname.new(filepath).relative_path_from(basepath)
170
+ if relative_path.to_s.start_with?('..')
171
+ File.basename(filepath)
172
+ else
173
+ relative_path.to_s
174
+ end
175
+ rescue ArgumentError
176
+ # Handle cases where filepath cannot be relativized (e.g., empty paths, different roots)
177
+ File.basename(filepath)
178
+ end
179
+ end
180
+
181
+ # Format backtrace entries with pretty file paths
182
+ def pretty_backtrace(backtrace, limit: 10)
183
+ return [] unless backtrace&.any?
184
+
185
+ backtrace.first(limit).map do |frame|
186
+ # Split the frame to get file path and line info
187
+ # Use non-greedy match and more specific pattern to prevent ReDoS
188
+ if frame.match(/^([^:]+(?::[^:0-9][^:]*)*):(\d+):(.*)$/)
189
+ file_part = $1
190
+ line_part = $2
191
+ method_part = $3
192
+ pretty_file = pretty_path(file_part) || File.basename(file_part)
193
+ "#{pretty_file}:#{line_part}#{method_part}"
194
+ else
195
+ frame
196
+ end
197
+ end
170
198
  end
171
199
  end
172
200
  end
@@ -9,8 +9,14 @@ class Tryouts
9
9
  expectation_type == :exception
10
10
  end
11
11
 
12
- def evaluate(_actual_result = nil)
13
- execute_test_code_and_evaluate_exception
12
+ def evaluate(_actual_result = nil, caught_exception: nil)
13
+ if caught_exception
14
+ # Use the pre-caught exception to avoid double execution
15
+ evaluate_exception_condition(caught_exception)
16
+ else
17
+ # Fallback for direct calls - shouldn't happen in normal flow
18
+ execute_test_code_and_evaluate_exception
19
+ end
14
20
  end
15
21
 
16
22
  private
@@ -0,0 +1,77 @@
1
+ # lib/tryouts/expectation_evaluators/non_nil.rb
2
+
3
+ require_relative 'base'
4
+
5
+ class Tryouts
6
+ module ExpectationEvaluators
7
+ # Evaluator for non-nil expectations using syntax: #=*>
8
+ #
9
+ # PURPOSE:
10
+ # - Validates that the test result is not nil and no exception occurred
11
+ # - Provides a simple "anything goes" expectation for existence checks
12
+ # - Useful for API responses, object creation, method return values
13
+ #
14
+ # SYNTAX: #=*>
15
+ # Examples:
16
+ # user = User.create(name: "test")
17
+ # #=*> # Pass: user object exists (not nil)
18
+ #
19
+ # response = api_call()
20
+ # #=*> # Pass: got some response (not nil)
21
+ #
22
+ # nil
23
+ # #=*> # Fail: result is nil
24
+ #
25
+ # raise StandardError.new("error")
26
+ # #=*> # Fail: exception occurred
27
+ #
28
+ # VALIDATION LOGIC:
29
+ # - Passes when result is not nil AND no exception was raised during execution
30
+ # - Fails when result is nil OR an exception occurred
31
+ # - Does not evaluate any additional expression (unlike other expectation types)
32
+ #
33
+ # IMPLEMENTATION DETAILS:
34
+ # - Simple existence check without complex evaluation
35
+ # - No expression parsing needed - syntax is just #=*>
36
+ # - Expected display shows "non-nil result with no exception"
37
+ # - Actual display shows the actual result value or exception
38
+ #
39
+ # DESIGN DECISIONS:
40
+ # - Uses #=*> syntax where * represents "anything"
41
+ # - Part of unified #= prefix convention for all expectation types
42
+ # - Complements existing boolean and equality expectations
43
+ # - Provides simple alternative to complex conditional expressions
44
+ # - Useful for integration tests where exact values are unpredictable
45
+ #
46
+ # VARIABLE ACCESS:
47
+ # - No special variables needed since no expression is evaluated
48
+ # - Works purely on the actual test result value
49
+ class NonNil < Base
50
+ def self.handles?(expectation_type)
51
+ expectation_type == :non_nil
52
+ end
53
+
54
+ def evaluate(actual_result = nil, caught_exception: nil)
55
+ # Check if an exception occurred during test execution
56
+ if caught_exception
57
+ return build_result(
58
+ passed: false,
59
+ actual: "(#{caught_exception.class}) #{caught_exception.message}",
60
+ expected: 'non-nil result with no exception',
61
+ )
62
+ end
63
+
64
+ # Check if result is nil
65
+ passed = !actual_result.nil?
66
+
67
+ build_result(
68
+ passed: passed,
69
+ actual: actual_result,
70
+ expected: 'non-nil result',
71
+ )
72
+ rescue StandardError => ex
73
+ handle_evaluation_error(ex, actual_result)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -12,6 +12,7 @@ require_relative 'regex_match'
12
12
  require_relative 'performance_time'
13
13
  require_relative 'intentional_failure'
14
14
  require_relative 'output'
15
+ require_relative 'non_nil'
15
16
 
16
17
  class Tryouts
17
18
  module ExpectationEvaluators
@@ -61,6 +62,7 @@ class Tryouts
61
62
  register(PerformanceTime)
62
63
  register(IntentionalFailure)
63
64
  register(Output)
65
+ register(NonNil)
64
66
  end
65
67
  end
66
68
  end
@@ -20,7 +20,7 @@ class Tryouts
20
20
 
21
21
  def process
22
22
  testrun = create_parser(@file, @options).parse
23
- @global_tally[:file_count] += 1
23
+ @global_tally[:aggregator].increment_total_files
24
24
  @output_manager.file_parsed(@file, testrun.total_tests)
25
25
 
26
26
  if @options[:inspect]
@@ -76,7 +76,11 @@ class Tryouts
76
76
  end
77
77
 
78
78
  def handle_general_error(ex)
79
- @global_tally[:total_errors] += 1 if @global_tally
79
+ if @global_tally
80
+ @global_tally[:aggregator].add_infrastructure_failure(
81
+ :file_processing, @file, ex.message, ex
82
+ )
83
+ end
80
84
  @output_manager.file_failure(@file, ex.message, ex.backtrace)
81
85
  1
82
86
  end
@@ -86,6 +86,8 @@ class Tryouts
86
86
  { type: :false_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
87
87
  when /^#\s*=\|>\s*(.*)$/
88
88
  { type: :boolean_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
89
+ when /^#\s*=\*>\s*(.*)$/
90
+ { type: :non_nil_expectation, content: $1.strip, line: line_number - 1 }
89
91
  when /^#\s*=:>\s*(.*)$/
90
92
  { type: :result_type_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
91
93
  when /^#\s*=~>\s*(.*)$/
@@ -39,6 +39,8 @@ class Tryouts
39
39
  { type: :false_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
40
40
  in /^#\s*=\|>\s*(.*)$/ # Boolean (true or false) expectation
41
41
  { type: :boolean_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
42
+ in /^#\s*=\*>\s*(.*)$/ # Non-nil expectation
43
+ { type: :non_nil_expectation, content: $1.strip, line: index }
42
44
  in /^#\s*=:>\s*(.*)$/ # Result type expectation
43
45
  { type: :result_type_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
44
46
  in /^#\s*=~>\s*(.*)$/ # Regex match expectation
@@ -86,6 +86,9 @@ class Tryouts
86
86
  in [_, { type: :boolean_expectation }]
87
87
  current_block[:expectations] << token
88
88
 
89
+ in [_, { type: :non_nil_expectation }]
90
+ current_block[:expectations] << token
91
+
89
92
  in [_, { type: :result_type_expectation }]
90
93
  current_block[:expectations] << token
91
94
 
@@ -192,7 +195,7 @@ class Tryouts
192
195
  :expectation, :exception_expectation, :intentional_failure_expectation,
193
196
  :true_expectation, :false_expectation, :boolean_expectation,
194
197
  :result_type_expectation, :regex_match_expectation,
195
- :performance_time_expectation, :output_expectation
198
+ :performance_time_expectation, :output_expectation, :non_nil_expectation
196
199
  ].include?(type)
197
200
  end
198
201
 
@@ -368,6 +371,7 @@ class Tryouts
368
371
  when :regex_match_expectation then :regex_match
369
372
  when :performance_time_expectation then :performance_time
370
373
  when :output_expectation then :output
374
+ when :non_nil_expectation then :non_nil
371
375
  else :regular
372
376
  end
373
377
 
@@ -244,7 +244,7 @@ class Tryouts
244
244
  end
245
245
 
246
246
  expectations_result = execute_with_timeout(test_timeout, test_case) do
247
- evaluate_expectations(test_case, caught_exception, container)
247
+ evaluate_expectations(test_case, caught_exception, container, nil, nil, nil, caught_exception)
248
248
  end
249
249
  build_test_result(test_case, caught_exception, expectations_result)
250
250
  else
@@ -330,7 +330,7 @@ class Tryouts
330
330
  end
331
331
 
332
332
  # Evaluate expectations using new object-oriented evaluation system
333
- def evaluate_expectations(test_case, actual_result, context, execution_time_ns = nil, stdout_content = nil, stderr_content = nil)
333
+ def evaluate_expectations(test_case, actual_result, context, execution_time_ns = nil, stdout_content = nil, stderr_content = nil, caught_exception = nil)
334
334
  return { passed: true, actual_results: [], expected_results: [] } if test_case.expectations.empty?
335
335
 
336
336
  evaluation_results = test_case.expectations.map do |expectation|
@@ -341,6 +341,9 @@ class Tryouts
341
341
  evaluator.evaluate(actual_result, execution_time_ns)
342
342
  elsif expectation.output? && (stdout_content || stderr_content)
343
343
  evaluator.evaluate(actual_result, stdout_content, stderr_content)
344
+ elsif expectation.exception? && caught_exception
345
+ # Pass caught exception to avoid double execution
346
+ evaluator.evaluate(actual_result, caught_exception: caught_exception)
344
347
  else
345
348
  evaluator.evaluate(actual_result)
346
349
  end
@@ -385,13 +388,14 @@ class Tryouts
385
388
  def process_test_result(result)
386
389
  @results << result
387
390
 
391
+ # Add all test results to the aggregator for centralized counting
392
+ if @global_tally && @global_tally[:aggregator]
393
+ @global_tally[:aggregator].add_test_result(@testrun.source_file, result)
394
+ end
395
+
396
+ # Update local batch counters for batch-level logic
388
397
  if result.failed? || result.error?
389
398
  @failed_count += 1
390
-
391
- # Collect failure details for end-of-run summary
392
- if @global_tally && @global_tally[:failure_collector]
393
- @global_tally[:failure_collector].add_failure(@testrun.source_file, result)
394
- end
395
399
  end
396
400
 
397
401
  show_test_result(result)
@@ -418,7 +422,11 @@ class Tryouts
418
422
  end
419
423
  rescue StandardError => ex
420
424
  @setup_failed = true
421
- @global_tally[:total_errors] += 1 if @global_tally
425
+ if @global_tally && @global_tally[:aggregator]
426
+ @global_tally[:aggregator].add_infrastructure_failure(
427
+ :setup, @testrun.source_file, ex.message, ex
428
+ )
429
+ end
422
430
 
423
431
  # Classify error and handle appropriately
424
432
  error_type = Tryouts.classify_error(ex)
@@ -455,7 +463,11 @@ class Tryouts
455
463
  end
456
464
  rescue StandardError => ex
457
465
  @setup_failed = true
458
- @global_tally[:total_errors] += 1 if @global_tally
466
+ if @global_tally && @global_tally[:aggregator]
467
+ @global_tally[:aggregator].add_infrastructure_failure(
468
+ :setup, @testrun.source_file, ex.message, ex
469
+ )
470
+ end
459
471
 
460
472
  # Classify error and handle appropriately
461
473
  error_type = Tryouts.classify_error(ex)
@@ -488,7 +500,11 @@ class Tryouts
488
500
  @output_manager&.teardown_output(captured_output) if captured_output && !captured_output.empty?
489
501
  end
490
502
  rescue StandardError => ex
491
- @global_tally[:total_errors] += 1 if @global_tally
503
+ if @global_tally && @global_tally[:aggregator]
504
+ @global_tally[:aggregator].add_infrastructure_failure(
505
+ :teardown, @testrun.source_file, ex.message, ex
506
+ )
507
+ end
492
508
 
493
509
  # Classify error and handle appropriately
494
510
  error_type = Tryouts.classify_error(ex)
@@ -154,9 +154,9 @@ class Tryouts
154
154
  def self.from_error(test_case, error, captured_output: nil, elapsed_time: nil, metadata: {})
155
155
  error_message = error ? error.message : '<exception is nil>'
156
156
 
157
- # Include backtrace in error message when in debug/verbose mode
158
- error_display = if error && Tryouts.debug?
159
- backtrace_preview = error.backtrace&.first(3)&.join("\n ")
157
+ # Include backtrace in error message when stack traces are enabled
158
+ error_display = if error && Tryouts.stack_traces?
159
+ backtrace_preview = Console.pretty_backtrace(error.backtrace, limit: 3).join("\n ")
160
160
  "(#{error.class}) #{error_message}\n #{backtrace_preview}"
161
161
  else
162
162
  "(#{error.class}) #{error_message}"
@@ -53,10 +53,12 @@ class Tryouts
53
53
  file_failed_count = test_results.count { |r| r.failed? }
54
54
  file_error_count = test_results.count { |r| r.error? }
55
55
  executed_test_count = test_results.size
56
- @global_tally[:total_tests] += executed_test_count
57
- @global_tally[:total_failed] += file_failed_count
58
- @global_tally[:total_errors] += file_error_count
59
- @global_tally[:successful_files] += 1 if success
56
+
57
+ # Note: Individual test results are added to the aggregator in TestBatch
58
+ # Here we just update the file success count atomically
59
+ if success
60
+ @global_tally[:aggregator].increment_successful_files
61
+ end
60
62
 
61
63
  duration = Time.now.to_f - @file_start.to_f
62
64
  @output_manager.file_success(@file, executed_test_count, file_failed_count, file_error_count, duration)