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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: adb9f46213aee10ed4b9d2a1b9b8d5c9e385ea23f996445debc7458b43351512
4
- data.tar.gz: 8d6abfed42e73bb340e13a62991b4855b8c0c18efa3092b4e3244a822a1612c1
3
+ metadata.gz: b8d7c33ad6377a7fb1c64c83e24e643c434643aa09e5451ad42bbe80c65b9c9d
4
+ data.tar.gz: 89cfe371c0fd575614a56702c904d0c5140db2a3a2af41d22777e459de1f4d8a
5
5
  SHA512:
6
- metadata.gz: 571410ed483879bd035b14c053a5c3496d51ad2248cff2534cf40fc06a72ece1aea13a7e61ae2c29b1e1f1768f19d3c5ede8c4e4a30a977c29aacb502c616988
7
- data.tar.gz: 6b5186131242edf826ba44303daaee716ce1ec364683e521df5224818d796067209a9a0ae66cacc85d15796cfee92df2349372fc5df762ced52b8753e1196c0e
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 && @show_debug
242
+ return unless backtrace && @show_stack_traces
242
243
 
243
- backtrace.first(3).each do |line|
244
- @stderr.puts indent_text(line.chomp, 1)
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 && @show_debug
84
+ return unless backtrace && @show_stack_traces
84
85
 
85
- backtrace.first(3).each do |line|
86
- @stderr.puts " #{line.chomp}"
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 && @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
@@ -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 #=1> STDOUT content
30
- #=2> STDERR content #=<> Intentional failure
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
@@ -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)
@@ -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
@@ -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[:file_count] > 1
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
- successful_files: 0,
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 process_file(file)
97
- file = FileProcessor.new(
98
- file: file,
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
- file.process
153
+ processor.process
105
154
  rescue StandardError => ex
106
155
  handle_file_error(ex)
107
- @global_tally[:total_errors] += 1
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
- if @global_tally[:failure_collector].any_failures?
114
- @output_manager.batch_summary(@global_tally[:failure_collector])
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
- @global_tally[:total_tests],
123
- @global_tally[:total_failed],
124
- @global_tally[:total_errors],
125
- @global_tally[:successful_files],
126
- @global_tally[:file_count],
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
@@ -1,5 +1,5 @@
1
1
  # lib/tryouts/version.rb
2
2
 
3
3
  class Tryouts
4
- VERSION = '3.3.1'
4
+ VERSION = '3.3.2'
5
5
  end
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.1
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