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.
- checksums.yaml +4 -4
- data/README.md +15 -4
- data/lib/tryouts/cli/formatters/agent.rb +450 -0
- data/lib/tryouts/cli/formatters/compact.rb +4 -3
- data/lib/tryouts/cli/formatters/factory.rb +5 -0
- data/lib/tryouts/cli/formatters/quiet.rb +4 -3
- data/lib/tryouts/cli/formatters/token_budget.rb +157 -0
- data/lib/tryouts/cli/formatters/verbose.rb +3 -2
- data/lib/tryouts/cli/formatters.rb +2 -0
- data/lib/tryouts/cli/opts.rb +86 -9
- data/lib/tryouts/console.rb +32 -4
- data/lib/tryouts/expectation_evaluators/exception.rb +8 -2
- data/lib/tryouts/expectation_evaluators/non_nil.rb +77 -0
- data/lib/tryouts/expectation_evaluators/registry.rb +2 -0
- data/lib/tryouts/file_processor.rb +6 -2
- data/lib/tryouts/parsers/enhanced_parser.rb +2 -0
- data/lib/tryouts/parsers/prism_parser.rb +2 -0
- data/lib/tryouts/parsers/shared_methods.rb +5 -1
- data/lib/tryouts/test_batch.rb +26 -10
- data/lib/tryouts/test_case.rb +3 -3
- data/lib/tryouts/test_executor.rb +6 -4
- data/lib/tryouts/test_result_aggregator.rb +138 -0
- data/lib/tryouts/test_runner.rb +81 -20
- data/lib/tryouts/version.rb +1 -1
- data/lib/tryouts.rb +5 -1
- metadata +19 -1
@@ -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 && @
|
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
|
-
|
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'
|
data/lib/tryouts/cli/opts.rb
CHANGED
@@ -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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
30
|
-
#=2> STDERR content
|
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
|
data/lib/tryouts/console.rb
CHANGED
@@ -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(
|
165
|
-
return nil if
|
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
|
-
|
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
|
-
|
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[:
|
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
|
-
|
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
|
|
data/lib/tryouts/test_batch.rb
CHANGED
@@ -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
|
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
|
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
|
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)
|
data/lib/tryouts/test_case.rb
CHANGED
@@ -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
|
158
|
-
error_display = if error && Tryouts.
|
159
|
-
backtrace_preview = error.backtrace
|
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
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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)
|