tryouts 3.3.0 → 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.
@@ -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,11 +1,14 @@
1
1
  # lib/tryouts/test_runner.rb
2
2
 
3
- require_relative 'prism_parser'
3
+ require 'concurrent'
4
+ require_relative 'parsers/prism_parser'
5
+ require_relative 'parsers/enhanced_parser'
4
6
  require_relative 'test_batch'
5
7
  require_relative 'translators/rspec_translator'
6
8
  require_relative 'translators/minitest_translator'
7
9
  require_relative 'file_processor'
8
10
  require_relative 'failure_collector'
11
+ require_relative 'test_result_aggregator'
9
12
 
10
13
  class Tryouts
11
14
  class TestRunner
@@ -34,7 +37,7 @@ class Tryouts
34
37
 
35
38
  result = process_files
36
39
  show_failure_summary
37
- show_grand_total if @global_tally[:file_count] > 1
40
+ show_grand_total if @global_tally[:aggregator].get_file_counts[:total] > 1
38
41
  result
39
42
  end
40
43
 
@@ -69,17 +72,20 @@ class Tryouts
69
72
 
70
73
  def initialize_global_tally
71
74
  {
72
- total_tests: 0,
73
- total_failed: 0,
74
- total_errors: 0,
75
- file_count: 0,
76
75
  start_time: Time.now,
77
- successful_files: 0,
78
- failure_collector: FailureCollector.new,
76
+ aggregator: TestResultAggregator.new,
79
77
  }
80
78
  end
81
79
 
82
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
83
89
  failure_count = 0
84
90
 
85
91
  @files.each_with_index do |file, _idx|
@@ -92,37 +98,87 @@ class Tryouts
92
98
  failure_count
93
99
  end
94
100
 
95
- def process_file(file)
96
- file = FileProcessor.new(
97
- 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,
98
148
  options: @options,
99
149
  output_manager: @output_manager,
100
150
  translator: @translator,
101
151
  global_tally: @global_tally,
102
152
  )
103
- file.process
153
+ processor.process
104
154
  rescue StandardError => ex
105
155
  handle_file_error(ex)
106
- @global_tally[:total_errors] += 1
156
+ @global_tally[:aggregator].add_infrastructure_failure(
157
+ :file_processing, file_path, ex.message, ex
158
+ )
107
159
  1
108
160
  end
109
161
 
110
162
  def show_failure_summary
111
163
  # Show failure summary if any failures exist
112
- if @global_tally[:failure_collector].any_failures?
113
- @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)
114
167
  end
115
168
  end
116
169
 
117
170
  def show_grand_total
118
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
119
175
 
120
176
  @output_manager.grand_total(
121
- @global_tally[:total_tests],
122
- @global_tally[:total_failed],
123
- @global_tally[:total_errors],
124
- @global_tally[:successful_files],
125
- @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],
126
182
  elapsed_time,
127
183
  )
128
184
  end
@@ -1,5 +1,5 @@
1
1
  # lib/tryouts/version.rb
2
2
 
3
3
  class Tryouts
4
- VERSION = '3.3.0'
4
+ VERSION = '3.3.2'
5
5
  end
data/lib/tryouts.rb CHANGED
@@ -8,7 +8,8 @@ TRYOUTS_LIB_HOME = __dir__ unless defined?(TRYOUTS_LIB_HOME)
8
8
  require_relative 'tryouts/console'
9
9
  require_relative 'tryouts/test_batch'
10
10
  require_relative 'tryouts/version'
11
- require_relative 'tryouts/prism_parser'
11
+ require_relative 'tryouts/parsers/prism_parser'
12
+ require_relative 'tryouts/parsers/enhanced_parser'
12
13
  require_relative 'tryouts/cli'
13
14
 
14
15
  class Tryouts
@@ -22,13 +23,17 @@ class Tryouts
22
23
 
23
24
  module ClassMethods
24
25
  attr_accessor :container, :quiet, :noisy, :fails
25
- attr_writer :debug
26
+ attr_writer :debug, :stack_traces
26
27
  attr_reader :cases, :testcase_io
27
28
 
28
29
  def debug?
29
30
  @debug == true
30
31
  end
31
32
 
33
+ def stack_traces?
34
+ @stack_traces == true || debug? # Debug mode auto-enables stack traces
35
+ end
36
+
32
37
  def update_load_path(lib_glob)
33
38
  Dir.glob(lib_glob).each { |dir| $LOAD_PATH.unshift(dir) }
34
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.0
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
@@ -137,13 +151,13 @@ files:
137
151
  - lib/tryouts/cli/opts.rb
138
152
  - lib/tryouts/cli/tty_detector.rb
139
153
  - lib/tryouts/console.rb
140
- - lib/tryouts/enhanced_parser.rb
141
154
  - lib/tryouts/expectation_evaluators/base.rb
142
155
  - lib/tryouts/expectation_evaluators/boolean.rb
143
156
  - lib/tryouts/expectation_evaluators/exception.rb
144
157
  - lib/tryouts/expectation_evaluators/expectation_result.rb
145
158
  - lib/tryouts/expectation_evaluators/false.rb
146
159
  - lib/tryouts/expectation_evaluators/intentional_failure.rb
160
+ - lib/tryouts/expectation_evaluators/non_nil.rb
147
161
  - lib/tryouts/expectation_evaluators/output.rb
148
162
  - lib/tryouts/expectation_evaluators/performance_time.rb
149
163
  - lib/tryouts/expectation_evaluators/regex_match.rb
@@ -153,10 +167,14 @@ files:
153
167
  - lib/tryouts/expectation_evaluators/true.rb
154
168
  - lib/tryouts/failure_collector.rb
155
169
  - lib/tryouts/file_processor.rb
156
- - lib/tryouts/prism_parser.rb
170
+ - lib/tryouts/parsers/base_parser.rb
171
+ - lib/tryouts/parsers/enhanced_parser.rb
172
+ - lib/tryouts/parsers/prism_parser.rb
173
+ - lib/tryouts/parsers/shared_methods.rb
157
174
  - lib/tryouts/test_batch.rb
158
175
  - lib/tryouts/test_case.rb
159
176
  - lib/tryouts/test_executor.rb
177
+ - lib/tryouts/test_result_aggregator.rb
160
178
  - lib/tryouts/test_runner.rb
161
179
  - lib/tryouts/translators/minitest_translator.rb
162
180
  - lib/tryouts/translators/rspec_translator.rb