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,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,8 +37,14 @@ class Tryouts
35
37
 
36
38
  result = process_files
37
39
  show_failure_summary
38
- show_grand_total if @global_tally[:file_count] > 1
39
- result
40
+ show_grand_total if @global_tally[:aggregator].get_file_counts[:total] > 1
41
+
42
+ # For agent critical mode, only count errors as failures
43
+ if @options[:agent] && (@options[:agent_focus] == :critical || @options[:agent_focus] == 'critical')
44
+ @global_tally[:aggregator].get_display_counts[:errors]
45
+ else
46
+ result
47
+ end
40
48
  end
41
49
 
42
50
  private
@@ -70,17 +78,20 @@ class Tryouts
70
78
 
71
79
  def initialize_global_tally
72
80
  {
73
- total_tests: 0,
74
- total_failed: 0,
75
- total_errors: 0,
76
- file_count: 0,
77
81
  start_time: Time.now,
78
- successful_files: 0,
79
- failure_collector: FailureCollector.new,
82
+ aggregator: TestResultAggregator.new,
80
83
  }
81
84
  end
82
85
 
83
86
  def process_files
87
+ if @options[:parallel] && @files.length > 1
88
+ process_files_parallel
89
+ else
90
+ process_files_sequential
91
+ end
92
+ end
93
+
94
+ def process_files_sequential
84
95
  failure_count = 0
85
96
 
86
97
  @files.each_with_index do |file, _idx|
@@ -93,37 +104,87 @@ class Tryouts
93
104
  failure_count
94
105
  end
95
106
 
96
- def process_file(file)
97
- file = FileProcessor.new(
98
- file: file,
107
+ def process_files_parallel
108
+ # Determine thread pool size
109
+ pool_size = @options[:parallel_threads] || Concurrent.processor_count
110
+ @output_manager.info "Running #{@files.length} files in parallel (#{pool_size} threads)", 1
111
+
112
+ # Create thread pool executor
113
+ executor = Concurrent::ThreadPoolExecutor.new(
114
+ min_threads: 1,
115
+ max_threads: pool_size,
116
+ max_queue: @files.length, # Queue size must accommodate all files
117
+ fallback_policy: :abort # Raise exception if pool and queue are exhausted
118
+ )
119
+
120
+ # Submit all file processing tasks to the thread pool
121
+ futures = @files.map do |file|
122
+ Concurrent::Future.execute(executor: executor) do
123
+ process_file(file)
124
+ end
125
+ end
126
+
127
+ # Wait for all tasks to complete and collect results
128
+ failure_count = 0
129
+ futures.each_with_index do |future, idx|
130
+ begin
131
+ result = future.value # This blocks until the future completes
132
+ failure_count += result unless result.zero?
133
+
134
+ status = result.zero? ? Console.color(:green, 'PASS') : Console.color(:red, 'FAIL')
135
+ file = @files[idx]
136
+ @output_manager.info "#{status} #{Console.pretty_path(file)} (#{result} failures)", 1
137
+ rescue StandardError => ex
138
+ failure_count += 1
139
+ file = @files[idx]
140
+ @output_manager.info "#{Console.color(:red, 'ERROR')} #{Console.pretty_path(file)} (#{ex.message})", 1
141
+ end
142
+ end
143
+
144
+ # Shutdown the thread pool
145
+ executor.shutdown
146
+ executor.wait_for_termination(10) # Wait up to 10 seconds for clean shutdown
147
+
148
+ failure_count
149
+ end
150
+
151
+ def process_file(file_path)
152
+ processor = FileProcessor.new(
153
+ file: file_path,
99
154
  options: @options,
100
155
  output_manager: @output_manager,
101
156
  translator: @translator,
102
157
  global_tally: @global_tally,
103
158
  )
104
- file.process
159
+ processor.process
105
160
  rescue StandardError => ex
106
161
  handle_file_error(ex)
107
- @global_tally[:total_errors] += 1
162
+ @global_tally[:aggregator].add_infrastructure_failure(
163
+ :file_processing, file_path, ex.message, ex
164
+ )
108
165
  1
109
166
  end
110
167
 
111
168
  def show_failure_summary
112
169
  # Show failure summary if any failures exist
113
- if @global_tally[:failure_collector].any_failures?
114
- @output_manager.batch_summary(@global_tally[:failure_collector])
170
+ aggregator = @global_tally[:aggregator]
171
+ if aggregator.any_display_failures?
172
+ @output_manager.batch_summary(aggregator.failure_collector)
115
173
  end
116
174
  end
117
175
 
118
176
  def show_grand_total
119
177
  elapsed_time = Time.now - @global_tally[:start_time]
178
+ aggregator = @global_tally[:aggregator]
179
+ display_counts = aggregator.get_display_counts
180
+ file_counts = aggregator.get_file_counts
120
181
 
121
182
  @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],
183
+ display_counts[:total_tests],
184
+ display_counts[:failed],
185
+ display_counts[:errors],
186
+ file_counts[:successful],
187
+ file_counts[:total],
127
188
  elapsed_time,
128
189
  )
129
190
  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.4.0'
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.4.0
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
@@ -123,6 +137,7 @@ files:
123
137
  - lib/tryouts.rb
124
138
  - lib/tryouts/cli.rb
125
139
  - lib/tryouts/cli/formatters.rb
140
+ - lib/tryouts/cli/formatters/agent.rb
126
141
  - lib/tryouts/cli/formatters/base.rb
127
142
  - lib/tryouts/cli/formatters/compact.rb
128
143
  - lib/tryouts/cli/formatters/factory.rb
@@ -130,6 +145,7 @@ files:
130
145
  - lib/tryouts/cli/formatters/output_manager.rb
131
146
  - lib/tryouts/cli/formatters/quiet.rb
132
147
  - lib/tryouts/cli/formatters/test_run_state.rb
148
+ - lib/tryouts/cli/formatters/token_budget.rb
133
149
  - lib/tryouts/cli/formatters/tty_status_display.rb
134
150
  - lib/tryouts/cli/formatters/verbose.rb
135
151
  - lib/tryouts/cli/modes/generate.rb
@@ -143,6 +159,7 @@ files:
143
159
  - lib/tryouts/expectation_evaluators/expectation_result.rb
144
160
  - lib/tryouts/expectation_evaluators/false.rb
145
161
  - lib/tryouts/expectation_evaluators/intentional_failure.rb
162
+ - lib/tryouts/expectation_evaluators/non_nil.rb
146
163
  - lib/tryouts/expectation_evaluators/output.rb
147
164
  - lib/tryouts/expectation_evaluators/performance_time.rb
148
165
  - lib/tryouts/expectation_evaluators/regex_match.rb
@@ -159,6 +176,7 @@ files:
159
176
  - lib/tryouts/test_batch.rb
160
177
  - lib/tryouts/test_case.rb
161
178
  - lib/tryouts/test_executor.rb
179
+ - lib/tryouts/test_result_aggregator.rb
162
180
  - lib/tryouts/test_runner.rb
163
181
  - lib/tryouts/translators/minitest_translator.rb
164
182
  - lib/tryouts/translators/rspec_translator.rb