tryouts 3.3.1 → 3.3.2
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/lib/tryouts/cli/formatters/compact.rb +4 -3
- data/lib/tryouts/cli/formatters/quiet.rb +4 -3
- data/lib/tryouts/cli/formatters/verbose.rb +3 -2
- data/lib/tryouts/cli/opts.rb +12 -2
- 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 +74 -19
- data/lib/tryouts/version.rb +1 -1
- data/lib/tryouts.rb +5 -1
- metadata +17 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b8d7c33ad6377a7fb1c64c83e24e643c434643aa09e5451ad42bbe80c65b9c9d
|
4
|
+
data.tar.gz: 89cfe371c0fd575614a56702c904d0c5140db2a3a2af41d22777e459de1f4d8a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 174645930ab03bc8332e415e9dde67e6c261a66ec2aa4f5572a31841a339cb244d15da75f6d314e3e2ec2d987fb5439e03c34d35d244b37d8b81a7e5b4dbc55d
|
7
|
+
data.tar.gz: 524ded2d4d670ed5fce810ef363faaba2459e56979fc13e61cdd7fbad6a3cffe2cadef7e7c8f1be0901ed1d26b44185f05f8be524c0ebe60a76e3bb841be5b37
|
@@ -11,6 +11,7 @@ class Tryouts
|
|
11
11
|
@show_debug = options.fetch(:debug, false)
|
12
12
|
@show_trace = options.fetch(:trace, false)
|
13
13
|
@show_passed = options.fetch(:show_passed, true)
|
14
|
+
@show_stack_traces = options.fetch(:stack_traces, false) || options.fetch(:debug, false)
|
14
15
|
end
|
15
16
|
|
16
17
|
# Phase-level output - minimal for compact mode
|
@@ -238,10 +239,10 @@ class Tryouts
|
|
238
239
|
def error_message(message, backtrace: nil)
|
239
240
|
@stderr.puts Console.color(:red, "ERROR: #{message}")
|
240
241
|
|
241
|
-
return unless backtrace && @
|
242
|
+
return unless backtrace && @show_stack_traces
|
242
243
|
|
243
|
-
|
244
|
-
@stderr.puts indent_text(line
|
244
|
+
Console.pretty_backtrace(backtrace, limit: 3).each do |line|
|
245
|
+
@stderr.puts indent_text(line, 1)
|
245
246
|
end
|
246
247
|
end
|
247
248
|
|
@@ -10,6 +10,7 @@ class Tryouts
|
|
10
10
|
super
|
11
11
|
@show_errors = options.fetch(:show_errors, true)
|
12
12
|
@show_final_summary = options.fetch(:show_final_summary, true)
|
13
|
+
@show_stack_traces = options.fetch(:stack_traces, false) || options.fetch(:debug, false)
|
13
14
|
@current_file = nil
|
14
15
|
end
|
15
16
|
|
@@ -80,10 +81,10 @@ class Tryouts
|
|
80
81
|
@stderr.puts
|
81
82
|
@stderr.puts Console.color(:red, "ERROR: #{message}")
|
82
83
|
|
83
|
-
return unless backtrace && @
|
84
|
+
return unless backtrace && @show_stack_traces
|
84
85
|
|
85
|
-
|
86
|
-
@stderr.puts " #{line
|
86
|
+
Console.pretty_backtrace(backtrace, limit: 3).each do |line|
|
87
|
+
@stderr.puts " #{line}"
|
87
88
|
end
|
88
89
|
end
|
89
90
|
|
@@ -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
|
data/lib/tryouts/cli/opts.rb
CHANGED
@@ -26,8 +26,8 @@ class Tryouts
|
|
26
26
|
|
27
27
|
#=> Value equality #==> Must be true #=/=> Must be false
|
28
28
|
#=|> True OR false #=!> Must raise error #=:> Type matching
|
29
|
-
#=~> Regex matching #=%> Time constraints
|
30
|
-
#=2> STDERR content
|
29
|
+
#=~> Regex matching #=%> Time constraints #=*> Non-nil result
|
30
|
+
#=1> STDOUT content #=2> STDERR content #=<> Intentional failure
|
31
31
|
HELP
|
32
32
|
|
33
33
|
class << self
|
@@ -65,6 +65,10 @@ class Tryouts
|
|
65
65
|
opts.on('-q', '--quiet', 'Minimal output (dots and summary only)') { options[:quiet] = true }
|
66
66
|
opts.on('-c', '--compact', 'Compact single-line output') { options[:compact] = true }
|
67
67
|
opts.on('-l', '--live', 'Live status display') { options[:live_status] = true }
|
68
|
+
opts.on('-j', '--parallel [THREADS]', 'Run test files in parallel (optional thread count)') do |threads|
|
69
|
+
options[:parallel] = true
|
70
|
+
options[:parallel_threads] = threads.to_i if threads && threads.to_i > 0
|
71
|
+
end
|
68
72
|
|
69
73
|
opts.separator "\nParser Options:"
|
70
74
|
opts.on('--enhanced-parser', 'Use enhanced parser with inhouse comment extraction (default)') { options[:parser] = :enhanced }
|
@@ -74,10 +78,16 @@ class Tryouts
|
|
74
78
|
opts.on('-i', '--inspect', 'Inspect file structure without running tests') { options[:inspect] = true }
|
75
79
|
|
76
80
|
opts.separator "\nGeneral Options:"
|
81
|
+
opts.on('-s', '--stack-traces', 'Show stack traces for exceptions') do
|
82
|
+
options[:stack_traces] = true
|
83
|
+
Tryouts.stack_traces = true
|
84
|
+
end
|
77
85
|
opts.on('-V', '--version', 'Show version') { options[:version] = true }
|
78
86
|
opts.on('-D', '--debug', 'Enable debug mode') do
|
79
87
|
options[:debug] = true
|
88
|
+
options[:stack_traces] = true # Debug mode auto-enables stack traces
|
80
89
|
Tryouts.debug = true
|
90
|
+
Tryouts.stack_traces = true
|
81
91
|
end
|
82
92
|
opts.on('-h', '--help', 'Show this help') do
|
83
93
|
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)
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# lib/tryouts/test_result_aggregator.rb
|
2
|
+
|
3
|
+
require_relative 'failure_collector'
|
4
|
+
require 'concurrent'
|
5
|
+
|
6
|
+
class Tryouts
|
7
|
+
# Centralized test result aggregation to ensure counting consistency
|
8
|
+
# across all formatters and eliminate counting discrepancies
|
9
|
+
class TestResultAggregator
|
10
|
+
def initialize
|
11
|
+
@failure_collector = FailureCollector.new
|
12
|
+
# Use thread-safe atomic counters
|
13
|
+
@test_counts = {
|
14
|
+
total_tests: Concurrent::AtomicFixnum.new(0),
|
15
|
+
passed: Concurrent::AtomicFixnum.new(0),
|
16
|
+
failed: Concurrent::AtomicFixnum.new(0),
|
17
|
+
errors: Concurrent::AtomicFixnum.new(0)
|
18
|
+
}
|
19
|
+
@infrastructure_failures = Concurrent::Array.new
|
20
|
+
@file_counts = {
|
21
|
+
total: Concurrent::AtomicFixnum.new(0),
|
22
|
+
successful: Concurrent::AtomicFixnum.new(0)
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :failure_collector
|
27
|
+
|
28
|
+
# Add a test-level result (from individual test execution)
|
29
|
+
def add_test_result(file_path, result_packet)
|
30
|
+
@test_counts[:total_tests].increment
|
31
|
+
|
32
|
+
if result_packet.passed?
|
33
|
+
@test_counts[:passed].increment
|
34
|
+
elsif result_packet.failed?
|
35
|
+
@test_counts[:failed].increment
|
36
|
+
@failure_collector.add_failure(file_path, result_packet)
|
37
|
+
elsif result_packet.error?
|
38
|
+
@test_counts[:errors].increment
|
39
|
+
@failure_collector.add_failure(file_path, result_packet)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Add an infrastructure-level failure (setup, teardown, file-level)
|
44
|
+
def add_infrastructure_failure(type, file_path, error_message, exception = nil)
|
45
|
+
@infrastructure_failures << {
|
46
|
+
type: type, # :setup, :teardown, :file_processing
|
47
|
+
file_path: file_path,
|
48
|
+
error_message: error_message,
|
49
|
+
exception: exception
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
# Atomic increment methods for file-level operations
|
54
|
+
def increment_total_files
|
55
|
+
@file_counts[:total].increment
|
56
|
+
end
|
57
|
+
|
58
|
+
def increment_successful_files
|
59
|
+
@file_counts[:successful].increment
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
# Get counts that should be displayed in numbered failure lists
|
64
|
+
# These match what actually appears in the failure summary
|
65
|
+
def get_display_counts
|
66
|
+
{
|
67
|
+
total_tests: @test_counts[:total_tests].value,
|
68
|
+
passed: @test_counts[:passed].value,
|
69
|
+
failed: @failure_collector.failure_count,
|
70
|
+
errors: @failure_collector.error_count,
|
71
|
+
total_issues: @failure_collector.total_issues
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
# Get total counts including infrastructure failures
|
76
|
+
# These represent all issues that occurred during test execution
|
77
|
+
def get_total_counts
|
78
|
+
display = get_display_counts
|
79
|
+
{
|
80
|
+
total_tests: display[:total_tests],
|
81
|
+
passed: display[:passed],
|
82
|
+
failed: display[:failed],
|
83
|
+
errors: display[:errors],
|
84
|
+
infrastructure_failures: @infrastructure_failures.size,
|
85
|
+
total_issues: display[:total_issues] + @infrastructure_failures.size
|
86
|
+
}
|
87
|
+
end
|
88
|
+
|
89
|
+
# Get file-level statistics
|
90
|
+
def get_file_counts
|
91
|
+
{
|
92
|
+
total: @file_counts[:total].value,
|
93
|
+
successful: @file_counts[:successful].value
|
94
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
# Get infrastructure failures for detailed reporting
|
98
|
+
def get_infrastructure_failures
|
99
|
+
@infrastructure_failures.dup
|
100
|
+
end
|
101
|
+
|
102
|
+
# Check if there are any failures at all
|
103
|
+
def any_failures?
|
104
|
+
@failure_collector.any_failures? || !@infrastructure_failures.empty?
|
105
|
+
end
|
106
|
+
|
107
|
+
# Check if there are displayable failures (for numbered lists)
|
108
|
+
def any_display_failures?
|
109
|
+
@failure_collector.any_failures?
|
110
|
+
end
|
111
|
+
|
112
|
+
# Reset for testing purposes
|
113
|
+
def clear
|
114
|
+
@failure_collector.clear
|
115
|
+
@test_counts[:total_tests].update { |_| 0 }
|
116
|
+
@test_counts[:passed].update { |_| 0 }
|
117
|
+
@test_counts[:failed].update { |_| 0 }
|
118
|
+
@test_counts[:errors].update { |_| 0 }
|
119
|
+
@infrastructure_failures.clear
|
120
|
+
@file_counts[:total].update { |_| 0 }
|
121
|
+
@file_counts[:successful].update { |_| 0 }
|
122
|
+
end
|
123
|
+
|
124
|
+
# Provide a summary string for debugging
|
125
|
+
def summary
|
126
|
+
display = get_display_counts
|
127
|
+
total = get_total_counts
|
128
|
+
|
129
|
+
parts = []
|
130
|
+
parts << "#{display[:passed]} passed" if display[:passed] > 0
|
131
|
+
parts << "#{display[:failed]} failed" if display[:failed] > 0
|
132
|
+
parts << "#{display[:errors]} errors" if display[:errors] > 0
|
133
|
+
parts << "#{total[:infrastructure_failures]} infrastructure failures" if total[:infrastructure_failures] > 0
|
134
|
+
|
135
|
+
parts.empty? ? "All tests passed" : parts.join(', ')
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
data/lib/tryouts/test_runner.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# lib/tryouts/test_runner.rb
|
2
2
|
|
3
|
+
require 'concurrent'
|
3
4
|
require_relative 'parsers/prism_parser'
|
4
5
|
require_relative 'parsers/enhanced_parser'
|
5
6
|
require_relative 'test_batch'
|
@@ -7,6 +8,7 @@ require_relative 'translators/rspec_translator'
|
|
7
8
|
require_relative 'translators/minitest_translator'
|
8
9
|
require_relative 'file_processor'
|
9
10
|
require_relative 'failure_collector'
|
11
|
+
require_relative 'test_result_aggregator'
|
10
12
|
|
11
13
|
class Tryouts
|
12
14
|
class TestRunner
|
@@ -35,7 +37,7 @@ class Tryouts
|
|
35
37
|
|
36
38
|
result = process_files
|
37
39
|
show_failure_summary
|
38
|
-
show_grand_total if @global_tally[:
|
40
|
+
show_grand_total if @global_tally[:aggregator].get_file_counts[:total] > 1
|
39
41
|
result
|
40
42
|
end
|
41
43
|
|
@@ -70,17 +72,20 @@ class Tryouts
|
|
70
72
|
|
71
73
|
def initialize_global_tally
|
72
74
|
{
|
73
|
-
total_tests: 0,
|
74
|
-
total_failed: 0,
|
75
|
-
total_errors: 0,
|
76
|
-
file_count: 0,
|
77
75
|
start_time: Time.now,
|
78
|
-
|
79
|
-
failure_collector: FailureCollector.new,
|
76
|
+
aggregator: TestResultAggregator.new,
|
80
77
|
}
|
81
78
|
end
|
82
79
|
|
83
80
|
def process_files
|
81
|
+
if @options[:parallel] && @files.length > 1
|
82
|
+
process_files_parallel
|
83
|
+
else
|
84
|
+
process_files_sequential
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def process_files_sequential
|
84
89
|
failure_count = 0
|
85
90
|
|
86
91
|
@files.each_with_index do |file, _idx|
|
@@ -93,37 +98,87 @@ class Tryouts
|
|
93
98
|
failure_count
|
94
99
|
end
|
95
100
|
|
96
|
-
def
|
97
|
-
|
98
|
-
|
101
|
+
def process_files_parallel
|
102
|
+
# Determine thread pool size
|
103
|
+
pool_size = @options[:parallel_threads] || Concurrent.processor_count
|
104
|
+
@output_manager.info "Running #{@files.length} files in parallel (#{pool_size} threads)", 1
|
105
|
+
|
106
|
+
# Create thread pool executor
|
107
|
+
executor = Concurrent::ThreadPoolExecutor.new(
|
108
|
+
min_threads: 1,
|
109
|
+
max_threads: pool_size,
|
110
|
+
max_queue: pool_size * 2, # Reasonable queue size
|
111
|
+
fallback_policy: :abort # Raise exception if pool and queue are exhausted
|
112
|
+
)
|
113
|
+
|
114
|
+
# Submit all file processing tasks to the thread pool
|
115
|
+
futures = @files.map do |file|
|
116
|
+
Concurrent::Future.execute(executor: executor) do
|
117
|
+
process_file(file)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Wait for all tasks to complete and collect results
|
122
|
+
failure_count = 0
|
123
|
+
futures.each_with_index do |future, idx|
|
124
|
+
begin
|
125
|
+
result = future.value # This blocks until the future completes
|
126
|
+
failure_count += result unless result.zero?
|
127
|
+
|
128
|
+
status = result.zero? ? Console.color(:green, 'PASS') : Console.color(:red, 'FAIL')
|
129
|
+
file = @files[idx]
|
130
|
+
@output_manager.info "#{status} #{Console.pretty_path(file)} (#{result} failures)", 1
|
131
|
+
rescue StandardError => ex
|
132
|
+
failure_count += 1
|
133
|
+
file = @files[idx]
|
134
|
+
@output_manager.info "#{Console.color(:red, 'ERROR')} #{Console.pretty_path(file)} (#{ex.message})", 1
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Shutdown the thread pool
|
139
|
+
executor.shutdown
|
140
|
+
executor.wait_for_termination(10) # Wait up to 10 seconds for clean shutdown
|
141
|
+
|
142
|
+
failure_count
|
143
|
+
end
|
144
|
+
|
145
|
+
def process_file(file_path)
|
146
|
+
processor = FileProcessor.new(
|
147
|
+
file: file_path,
|
99
148
|
options: @options,
|
100
149
|
output_manager: @output_manager,
|
101
150
|
translator: @translator,
|
102
151
|
global_tally: @global_tally,
|
103
152
|
)
|
104
|
-
|
153
|
+
processor.process
|
105
154
|
rescue StandardError => ex
|
106
155
|
handle_file_error(ex)
|
107
|
-
@global_tally[:
|
156
|
+
@global_tally[:aggregator].add_infrastructure_failure(
|
157
|
+
:file_processing, file_path, ex.message, ex
|
158
|
+
)
|
108
159
|
1
|
109
160
|
end
|
110
161
|
|
111
162
|
def show_failure_summary
|
112
163
|
# Show failure summary if any failures exist
|
113
|
-
|
114
|
-
|
164
|
+
aggregator = @global_tally[:aggregator]
|
165
|
+
if aggregator.any_display_failures?
|
166
|
+
@output_manager.batch_summary(aggregator.failure_collector)
|
115
167
|
end
|
116
168
|
end
|
117
169
|
|
118
170
|
def show_grand_total
|
119
171
|
elapsed_time = Time.now - @global_tally[:start_time]
|
172
|
+
aggregator = @global_tally[:aggregator]
|
173
|
+
display_counts = aggregator.get_display_counts
|
174
|
+
file_counts = aggregator.get_file_counts
|
120
175
|
|
121
176
|
@output_manager.grand_total(
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
177
|
+
display_counts[:total_tests],
|
178
|
+
display_counts[:failed],
|
179
|
+
display_counts[:errors],
|
180
|
+
file_counts[:successful],
|
181
|
+
file_counts[:total],
|
127
182
|
elapsed_time,
|
128
183
|
)
|
129
184
|
end
|
data/lib/tryouts/version.rb
CHANGED
data/lib/tryouts.rb
CHANGED
@@ -23,13 +23,17 @@ class Tryouts
|
|
23
23
|
|
24
24
|
module ClassMethods
|
25
25
|
attr_accessor :container, :quiet, :noisy, :fails
|
26
|
-
attr_writer :debug
|
26
|
+
attr_writer :debug, :stack_traces
|
27
27
|
attr_reader :cases, :testcase_io
|
28
28
|
|
29
29
|
def debug?
|
30
30
|
@debug == true
|
31
31
|
end
|
32
32
|
|
33
|
+
def stack_traces?
|
34
|
+
@stack_traces == true || debug? # Debug mode auto-enables stack traces
|
35
|
+
end
|
36
|
+
|
33
37
|
def update_load_path(lib_glob)
|
34
38
|
Dir.glob(lib_glob).each { |dir| $LOAD_PATH.unshift(dir) }
|
35
39
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tryouts
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.3.
|
4
|
+
version: 3.3.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Delano Mandelbaum
|
@@ -37,6 +37,20 @@ dependencies:
|
|
37
37
|
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
39
|
version: '1.0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: concurrent-ruby
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '1.0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '1.0'
|
40
54
|
- !ruby/object:Gem::Dependency
|
41
55
|
name: minitest
|
42
56
|
requirement: !ruby/object:Gem::Requirement
|
@@ -143,6 +157,7 @@ files:
|
|
143
157
|
- lib/tryouts/expectation_evaluators/expectation_result.rb
|
144
158
|
- lib/tryouts/expectation_evaluators/false.rb
|
145
159
|
- lib/tryouts/expectation_evaluators/intentional_failure.rb
|
160
|
+
- lib/tryouts/expectation_evaluators/non_nil.rb
|
146
161
|
- lib/tryouts/expectation_evaluators/output.rb
|
147
162
|
- lib/tryouts/expectation_evaluators/performance_time.rb
|
148
163
|
- lib/tryouts/expectation_evaluators/regex_match.rb
|
@@ -159,6 +174,7 @@ files:
|
|
159
174
|
- lib/tryouts/test_batch.rb
|
160
175
|
- lib/tryouts/test_case.rb
|
161
176
|
- lib/tryouts/test_executor.rb
|
177
|
+
- lib/tryouts/test_result_aggregator.rb
|
162
178
|
- lib/tryouts/test_runner.rb
|
163
179
|
- lib/tryouts/translators/minitest_translator.rb
|
164
180
|
- lib/tryouts/translators/rspec_translator.rb
|