tryouts 3.5.0 → 3.5.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.
@@ -0,0 +1,254 @@
1
+ # lib/tryouts/parsers/legacy_parser.rb
2
+
3
+ require_relative '../test_case'
4
+ require_relative 'base_parser'
5
+
6
+ class Tryouts
7
+ # Legacy parser using line-by-line regex parsing for compatibility
8
+ #
9
+ # The LegacyParser provides a simpler, more straightforward approach to parsing
10
+ # tryout files using sequential line-by-line processing with pattern matching.
11
+ # While less sophisticated than the EnhancedParser, it offers predictable behavior
12
+ # and serves as a fallback option for edge cases.
13
+ #
14
+ # @example Basic usage
15
+ # parser = Tryouts::LegacyParser.new(source_code, file_path)
16
+ # testrun = parser.parse
17
+ # puts testrun.test_cases.length
18
+ #
19
+ # @example Using legacy parser explicitly
20
+ # # In CLI: tryouts --legacy-parser my_test.rb
21
+ # # Or programmatically:
22
+ # parser = Tryouts::LegacyParser.new(source, file)
23
+ # result = parser.parse
24
+ #
25
+ # @!attribute [r] parser_type
26
+ # @return [Symbol] Returns :legacy to identify parser type
27
+ #
28
+ # ## Characteristics
29
+ #
30
+ # ### 1. Simple Line-by-Line Processing
31
+ # - Processes each line sequentially with pattern matching
32
+ # - Straightforward regex-based approach
33
+ # - Easy to understand and debug parsing logic
34
+ #
35
+ # ### 2. Pattern Matching Classification
36
+ # - Uses Ruby 3.4+ pattern matching (`case/in`) for token classification
37
+ # - Modern syntax while maintaining simple parsing approach
38
+ # - Consistent with EnhancedParser's classification logic
39
+ #
40
+ # ### 3. Compatibility Focus
41
+ # - Maintains backward compatibility with older tryout files
42
+ # - Provides fallback parsing when EnhancedParser encounters issues
43
+ # - Useful for debugging parser-specific problems
44
+ #
45
+ # ## Limitations
46
+ #
47
+ # ### 1. HEREDOC Vulnerability
48
+ # - Cannot distinguish between real comments and content inside HEREDOCs
49
+ # - May incorrectly parse string content as tryout syntax
50
+ # - Requires careful handling of complex Ruby syntax
51
+ #
52
+ # ### 2. Limited Inline Comment Support
53
+ # - Basic handling of lines with both code and comments
54
+ # - Less sophisticated than EnhancedParser's multi-comment support
55
+ #
56
+ # ## When to Use
57
+ #
58
+ # - **Debugging**: When EnhancedParser produces unexpected results
59
+ # - **Compatibility**: With older Ruby versions or edge cases
60
+ # - **Simplicity**: When predictable line-by-line behavior is preferred
61
+ # - **Fallback**: As a secondary parsing option
62
+ #
63
+ # @see EnhancedParser For robust syntax-aware parsing (recommended default)
64
+ # @see BaseParser For shared parsing functionality
65
+ # @since 3.0.0
66
+ class LegacyParser < Tryouts::Parsers::BaseParser
67
+
68
+ # Parse source code into a Testrun using line-by-line processing
69
+ #
70
+ # This method provides sequential line-by-line parsing that processes each
71
+ # line with pattern matching to classify tokens. While simpler than
72
+ # EnhancedParser, it may be vulnerable to HEREDOC content parsing issues.
73
+ #
74
+ # @return [Tryouts::Testrun] Structured test data with setup, test cases, teardown, and warnings
75
+ # @raise [Tryouts::TryoutSyntaxError] If source contains syntax errors or strict mode violations
76
+ def parse
77
+ return handle_syntax_errors if @prism_result.failure?
78
+
79
+ tokens = tokenize_content
80
+ test_boundaries = find_test_case_boundaries(tokens)
81
+ tokens = classify_potential_descriptions_with_boundaries(tokens, test_boundaries)
82
+ test_blocks = group_into_test_blocks(tokens)
83
+ process_test_blocks(test_blocks)
84
+ end
85
+
86
+ private
87
+
88
+ # Tokenize content using sequential line-by-line pattern matching
89
+ #
90
+ # Processes each line of source code individually, applying pattern matching
91
+ # to classify it as code, comment, expectation, etc. This approach is simple
92
+ # and predictable but cannot distinguish between real comments and content
93
+ # inside string literals or HEREDOCs.
94
+ #
95
+ # @return [Array<Hash>] Array of token hashes with keys :type, :content, :line, etc.
96
+ # @example Token structure
97
+ # [
98
+ # { type: :description, content: "Test case description", line: 5 },
99
+ # { type: :code, content: "result = calculate(x)", line: 6 },
100
+ # { type: :expectation, content: "42", line: 7, ast: <Prism::Node> }
101
+ # ]
102
+ # @note Potential_descriptions are later reclassified based on test boundaries
103
+ def tokenize_content
104
+ tokens = []
105
+
106
+ @lines.each_with_index do |line, index|
107
+ token = case line
108
+ in /^##\s*(.*)$/ # Test description format: ## description
109
+ { type: :description, content: $1.strip, line: index }
110
+ in /^#\s*TEST\s*\d*:\s*(.*)$/ # rubocop:disable Lint/DuplicateBranch
111
+ { type: :description, content: $1.strip, line: index }
112
+ in /^#\s*=!>\s*(.*)$/ # Exception expectation (updated for consistency)
113
+ { type: :exception_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
114
+ in /^#\s*=<>\s*(.*)$/ # Intentional failure expectation
115
+ { type: :intentional_failure_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
116
+ in /^#\s*==>\s*(.*)$/ # Boolean true expectation
117
+ { type: :true_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
118
+ in %r{^#\s*=/=>\s*(.*)$} # Boolean false expectation
119
+ { type: :false_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
120
+ in /^#\s*=\|>\s*(.*)$/ # Boolean (true or false) expectation
121
+ { type: :boolean_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
122
+ in /^#\s*=\*>\s*(.*)$/ # Non-nil expectation
123
+ { type: :non_nil_expectation, content: $1.strip, line: index }
124
+ in /^#\s*=:>\s*(.*)$/ # Result type expectation
125
+ { type: :result_type_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
126
+ in /^#\s*=~>\s*(.*)$/ # Regex match expectation
127
+ { type: :regex_match_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
128
+ in /^#\s*=%>\s*(.*)$/ # Performance time expectation
129
+ { type: :performance_time_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
130
+ in /^#\s*=(\d+)>\s*(.*)$/ # Output expectation (stdout/stderr with pipe number)
131
+ { type: :output_expectation, content: $2.strip, pipe: $1.to_i, line: index, ast: parse_expectation($2.strip) }
132
+ in /^#\s*=>\s*(.*)$/ # Regular expectation
133
+ { type: :expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
134
+ in /^#\s*=([^>=:!~%*|\/\s]+)>\s*(.*)$/ # Malformed expectation - invalid characters between = and >
135
+ syntax = $1
136
+ content_part = $2.strip
137
+ add_warning(ParserWarning.malformed_expectation(
138
+ line_number: index + 1,
139
+ syntax: syntax,
140
+ context: line.strip
141
+ ))
142
+ { type: :malformed_expectation, syntax: syntax, content: content_part, line: index }
143
+ in /^##\s*=>\s*(.*)$/ # Commented out expectation (should be ignored)
144
+ { type: :comment, content: '=>' + $1.strip, line: index }
145
+ in line if looks_like_malformed_expectation?(line) # Comprehensive malformed expectation detection
146
+ detected_syntax = extract_malformed_syntax(line)
147
+ add_warning(ParserWarning.malformed_expectation(
148
+ line_number: index + 1,
149
+ syntax: detected_syntax,
150
+ context: line.strip
151
+ ))
152
+ { type: :malformed_expectation, syntax: detected_syntax, content: line.strip, line: index }
153
+ in /^#\s*(.*)$/ # Single hash comment - potential description
154
+ { type: :potential_description, content: $1.strip, line: index }
155
+ in /^\s*$/ # Blank line
156
+ { type: :blank, line: index }
157
+ else # Ruby code
158
+ { type: :code, content: line, line: index, ast: parse_ruby_line(line) }
159
+ end
160
+
161
+ tokens << token
162
+ end
163
+
164
+ # Return tokens with potential_descriptions - they'll be classified later with test boundaries
165
+ tokens
166
+ end
167
+
168
+
169
+ # Convert potential_descriptions to descriptions or comments based on context
170
+ def classify_potential_descriptions(tokens)
171
+ tokens.map.with_index do |token, index|
172
+ if token[:type] == :potential_description
173
+ # Check if this looks like a test description based on content and context
174
+ content = token[:content].strip
175
+
176
+ # Skip if it's clearly just a regular comment (short, lowercase, etc.)
177
+ # Test descriptions are typically longer and more descriptive
178
+ looks_like_regular_comment = content.length < 20 &&
179
+ content.downcase == content &&
180
+ !content.match?(/test|example|demonstrate|show/i)
181
+
182
+ # Check if there's code immediately before this (suggesting it's mid-test)
183
+ prev_token = index > 0 ? tokens[index - 1] : nil
184
+ has_code_before = prev_token && prev_token[:type] == :code
185
+
186
+ if looks_like_regular_comment || has_code_before
187
+ # Treat as regular comment
188
+ token.merge(type: :comment)
189
+ else
190
+ # Look ahead for test pattern: code + at least one expectation within reasonable distance
191
+ following_tokens = tokens[(index + 1)..]
192
+
193
+ # Skip blanks and comments to find meaningful content
194
+ meaningful_following = following_tokens.reject { |t| [:blank, :comment].include?(t[:type]) }
195
+
196
+ # Look for test pattern: at least one code token followed by at least one expectation
197
+ # within the next 10 meaningful tokens (to avoid matching setup/teardown)
198
+ test_window = meaningful_following.first(10)
199
+ has_code = test_window.any? { |t| t[:type] == :code }
200
+ has_expectation = test_window.any? { |t| is_expectation_type?(t[:type]) }
201
+
202
+ if has_code && has_expectation
203
+ token.merge(type: :description)
204
+ else
205
+ token.merge(type: :comment)
206
+ end
207
+ end
208
+ else
209
+ token
210
+ end
211
+ end
212
+ end
213
+
214
+ # Detect if a comment looks like a malformed expectation attempt
215
+ # This catches patterns that suggest someone tried to write an expectation
216
+ # but got the syntax wrong (missing parts, wrong spacing, extra characters, etc.)
217
+ def looks_like_malformed_expectation?(content)
218
+ # Skip if it's already handled by specific patterns above
219
+ return false if content.match?(/^##\s*/) # Description
220
+ return false if content.match?(/^#\s*TEST\s*\d*:\s*/) # TEST format
221
+ return false if content.match?(/^##\s*=>\s*/) # Commented out expectation
222
+
223
+ # Look for patterns that suggest expectation attempts:
224
+ # 1. Contains = and/or > in suspicious positions
225
+ # 2. Has spaces around = or > suggesting misunderstanding
226
+ # 3. Missing > or = from what looks like expectation syntax
227
+ # 4. Extra characters in expectation-like patterns
228
+
229
+ content.match?(/^#\s*([=><]|.*[=><])/) && # Contains =, >, or < after #
230
+ !content.match?(/^#\s*[^=><]*$/) # Not just a regular comment without expectation chars
231
+ end
232
+
233
+ # Extract the malformed syntax portion for warning display
234
+ def extract_malformed_syntax(content)
235
+ # Try to identify what the user was attempting to write
236
+ case content
237
+ when /^#\s*([=><][^=><]*[=><].*?)(\s|$)/ # Pattern with expectation chars
238
+ $1.strip
239
+ when /^#\s*([=><].*?)(\s|$)/ # Simple pattern starting with expectation char
240
+ $1.strip
241
+ when /^#\s*(.*?[=><].*?)(\s|$)/ # Pattern containing expectation chars
242
+ $1.strip
243
+ else
244
+ # Fallback: show the part after #
245
+ content.sub(/^#\s*/, '').split(/\s/).first || 'unknown'
246
+ end
247
+ end
248
+
249
+ # Parser type identification for metadata
250
+ def parser_type
251
+ :legacy
252
+ end
253
+ end
254
+ end
@@ -103,6 +103,9 @@ class Tryouts
103
103
  in [_, { type: :output_expectation }]
104
104
  current_block[:expectations] << token
105
105
 
106
+ in [_, { type: :malformed_expectation }]
107
+ current_block[:expectations] << token
108
+
106
109
  in [_, { type: :comment | :blank }]
107
110
  add_context_to_block(current_block, token)
108
111
  end
@@ -197,7 +200,8 @@ class Tryouts
197
200
  :expectation, :exception_expectation, :intentional_failure_expectation,
198
201
  :true_expectation, :false_expectation, :boolean_expectation,
199
202
  :result_type_expectation, :regex_match_expectation,
200
- :performance_time_expectation, :output_expectation, :non_nil_expectation
203
+ :performance_time_expectation, :output_expectation, :non_nil_expectation,
204
+ :malformed_expectation
201
205
  ].include?(type)
202
206
  end
203
207
 
@@ -436,7 +436,7 @@ class Tryouts
436
436
 
437
437
  # For non-catastrophic errors, we still stop batch execution
438
438
  unless Tryouts.batch_stopping_error?(ex)
439
- @output_manager&.error("Global setup failed: #{ex.message}")
439
+ @output_manager&.error("Global setup error: #{ex.message}")
440
440
  return
441
441
  end
442
442
 
@@ -11,7 +11,6 @@ class Tryouts
11
11
  @output_manager = output_manager
12
12
  @translator = translator
13
13
  @global_tally = global_tally
14
-
15
14
  end
16
15
 
17
16
  def execute
@@ -29,7 +28,6 @@ class Tryouts
29
28
 
30
29
  private
31
30
 
32
-
33
31
  def execute_direct_mode
34
32
  batch = TestBatch.new(
35
33
  @testrun,
@@ -56,7 +54,7 @@ class Tryouts
56
54
  file_error_count = test_results.count { |r| r.error? }
57
55
  executed_test_count = test_results.size
58
56
 
59
- # Note: Individual test results are added to the aggregator in TestBatch
57
+ # NOTE: Individual test results are added to the aggregator in TestBatch
60
58
  # Here we just update the file success count atomically
61
59
  if success
62
60
  @global_tally[:aggregator].increment_successful_files
@@ -66,7 +64,17 @@ class Tryouts
66
64
  @output_manager.file_success(@file, executed_test_count, file_failed_count, file_error_count, duration)
67
65
 
68
66
  # Combine failures and errors to determine the exit code.
69
- success ? 0 : (file_failed_count + file_error_count)
67
+ # Include infrastructure failures (setup/teardown errors) in exit code calculation
68
+ if success
69
+ 0
70
+ else
71
+ # Check for infrastructure failures when no test cases executed
72
+ infrastructure_failure_count = @global_tally[:aggregator].infrastructure_failure_count
73
+ total_failure_count = file_failed_count + file_error_count
74
+
75
+ # If no tests ran but there are infrastructure failures, count those as failures
76
+ total_failure_count.zero? && infrastructure_failure_count > 0 ? infrastructure_failure_count : total_failure_count
77
+ end
70
78
  end
71
79
 
72
80
  def execute_rspec_mode
@@ -8,18 +8,18 @@ class Tryouts
8
8
  # across all formatters and eliminate counting discrepancies
9
9
  class TestResultAggregator
10
10
  def initialize
11
- @failure_collector = FailureCollector.new
11
+ @failure_collector = FailureCollector.new
12
12
  # Use thread-safe atomic counters
13
- @test_counts = {
13
+ @test_counts = {
14
14
  total_tests: Concurrent::AtomicFixnum.new(0),
15
15
  passed: Concurrent::AtomicFixnum.new(0),
16
16
  failed: Concurrent::AtomicFixnum.new(0),
17
- errors: Concurrent::AtomicFixnum.new(0)
17
+ errors: Concurrent::AtomicFixnum.new(0),
18
18
  }
19
19
  @infrastructure_failures = Concurrent::Array.new
20
- @file_counts = {
20
+ @file_counts = {
21
21
  total: Concurrent::AtomicFixnum.new(0),
22
- successful: Concurrent::AtomicFixnum.new(0)
22
+ successful: Concurrent::AtomicFixnum.new(0),
23
23
  }
24
24
  end
25
25
 
@@ -46,7 +46,7 @@ class Tryouts
46
46
  type: type, # :setup, :teardown, :file_processing
47
47
  file_path: file_path,
48
48
  error_message: error_message,
49
- exception: exception
49
+ exception: exception,
50
50
  }
51
51
  end
52
52
 
@@ -59,6 +59,10 @@ class Tryouts
59
59
  @file_counts[:successful].increment
60
60
  end
61
61
 
62
+ # Get count of infrastructure failures
63
+ def infrastructure_failure_count
64
+ @infrastructure_failures.size
65
+ end
62
66
 
63
67
  # Get counts that should be displayed in numbered failure lists
64
68
  # These match what actually appears in the failure summary
@@ -68,7 +72,7 @@ class Tryouts
68
72
  passed: @test_counts[:passed].value,
69
73
  failed: @failure_collector.failure_count,
70
74
  errors: @failure_collector.error_count,
71
- total_issues: @failure_collector.total_issues
75
+ total_issues: @failure_collector.total_issues,
72
76
  }
73
77
  end
74
78
 
@@ -82,7 +86,7 @@ class Tryouts
82
86
  failed: display[:failed],
83
87
  errors: display[:errors],
84
88
  infrastructure_failures: @infrastructure_failures.size,
85
- total_issues: display[:total_issues] + @infrastructure_failures.size
89
+ total_issues: display[:total_issues] + @infrastructure_failures.size,
86
90
  }
87
91
  end
88
92
 
@@ -90,7 +94,7 @@ class Tryouts
90
94
  def get_file_counts
91
95
  {
92
96
  total: @file_counts[:total].value,
93
- successful: @file_counts[:successful].value
97
+ successful: @file_counts[:successful].value,
94
98
  }
95
99
  end
96
100
 
@@ -124,7 +128,7 @@ class Tryouts
124
128
  # Provide a summary string for debugging
125
129
  def summary
126
130
  display = get_display_counts
127
- total = get_total_counts
131
+ total = get_total_counts
128
132
 
129
133
  parts = []
130
134
  parts << "#{display[:passed]} passed" if display[:passed] > 0
@@ -132,7 +136,7 @@ class Tryouts
132
136
  parts << "#{display[:errors]} errors" if display[:errors] > 0
133
137
  parts << "#{total[:infrastructure_failures]} infrastructure failures" if total[:infrastructure_failures] > 0
134
138
 
135
- parts.empty? ? "All tests passed" : parts.join(', ')
139
+ parts.empty? ? 'All tests passed' : parts.join(', ')
136
140
  end
137
141
  end
138
142
  end
@@ -1,7 +1,7 @@
1
1
  # lib/tryouts/test_runner.rb
2
2
 
3
3
  require 'concurrent'
4
- require_relative 'parsers/prism_parser'
4
+ require_relative 'parsers/legacy_parser'
5
5
  require_relative 'parsers/enhanced_parser'
6
6
  require_relative 'test_batch'
7
7
  require_relative 'translators/rspec_translator'
@@ -24,11 +24,11 @@ class Tryouts
24
24
  }.freeze
25
25
 
26
26
  def initialize(files:, options:, output_manager:)
27
- @files = files
28
- @options = apply_framework_defaults(options)
29
- @output_manager = output_manager
30
- @translator = initialize_translator
31
- @global_tally = initialize_global_tally
27
+ @files = files
28
+ @options = apply_framework_defaults(options)
29
+ @output_manager = output_manager
30
+ @translator = initialize_translator
31
+ @global_tally = initialize_global_tally
32
32
  @file_line_specs = options[:file_line_specs] || {}
33
33
  end
34
34
 
@@ -44,8 +44,11 @@ class Tryouts
44
44
  end
45
45
 
46
46
  # For agent critical mode, only count errors as failures
47
- if @options[:agent] && (@options[:agent_focus] == :critical || @options[:agent_focus] == 'critical')
48
- @global_tally[:aggregator].get_display_counts[:errors]
47
+ if @options[:agent] && ([:critical, 'critical'].include?(@options[:agent_focus]))
48
+ # Include infrastructure failures as errors for agent critical mode
49
+ display_errors = @global_tally[:aggregator].get_display_counts[:errors]
50
+ infrastructure_errors = @global_tally[:aggregator].infrastructure_failure_count
51
+ display_errors + infrastructure_errors
49
52
  else
50
53
  result
51
54
  end
@@ -118,7 +121,7 @@ class Tryouts
118
121
  min_threads: 1,
119
122
  max_threads: pool_size,
120
123
  max_queue: @files.length, # Queue size must accommodate all files
121
- fallback_policy: :abort # Raise exception if pool and queue are exhausted
124
+ fallback_policy: :abort, # Raise exception if pool and queue are exhausted
122
125
  )
123
126
 
124
127
  # Submit all file processing tasks to the thread pool
@@ -132,15 +135,15 @@ class Tryouts
132
135
  failure_count = 0
133
136
  futures.each_with_index do |future, idx|
134
137
  begin
135
- result = future.value # This blocks until the future completes
138
+ result = future.value # This blocks until the future completes
136
139
  failure_count += result unless result.zero?
137
140
 
138
141
  status = result.zero? ? Console.color(:green, 'PASS') : Console.color(:red, 'FAIL')
139
- file = @files[idx]
142
+ file = @files[idx]
140
143
  @output_manager.info "#{status} #{Console.pretty_path(file)} (#{result} failures)", 1
141
144
  rescue StandardError => ex
142
145
  failure_count += 1
143
- file = @files[idx]
146
+ file = @files[idx]
144
147
  @output_manager.info "#{Console.color(:red, 'ERROR')} #{Console.pretty_path(file)} (#{ex.message})", 1
145
148
  end
146
149
  end
@@ -184,10 +187,10 @@ class Tryouts
184
187
  end
185
188
 
186
189
  def show_grand_total
187
- elapsed_time = Time.now - @global_tally[:start_time]
188
- aggregator = @global_tally[:aggregator]
190
+ elapsed_time = Time.now - @global_tally[:start_time]
191
+ aggregator = @global_tally[:aggregator]
189
192
  display_counts = aggregator.get_display_counts
190
- file_counts = aggregator.get_file_counts
193
+ file_counts = aggregator.get_file_counts
191
194
 
192
195
  @output_manager.grand_total(
193
196
  display_counts[:total_tests],
@@ -1,5 +1,5 @@
1
1
  # lib/tryouts/version.rb
2
2
 
3
3
  class Tryouts
4
- VERSION = '3.5.0'
4
+ VERSION = '3.5.2'
5
5
  end
data/lib/tryouts.rb CHANGED
@@ -8,7 +8,7 @@ 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/parsers/prism_parser'
11
+ require_relative 'tryouts/parsers/legacy_parser'
12
12
  require_relative 'tryouts/parsers/enhanced_parser'
13
13
  require_relative 'tryouts/cli'
14
14
 
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.5.0
4
+ version: 3.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Delano Mandelbaum
@@ -171,9 +171,10 @@ files:
171
171
  - lib/tryouts/failure_collector.rb
172
172
  - lib/tryouts/file_processor.rb
173
173
  - lib/tryouts/parser_warning.rb
174
+ - lib/tryouts/parsers/CLAUDE.md
174
175
  - lib/tryouts/parsers/base_parser.rb
175
176
  - lib/tryouts/parsers/enhanced_parser.rb
176
- - lib/tryouts/parsers/prism_parser.rb
177
+ - lib/tryouts/parsers/legacy_parser.rb
177
178
  - lib/tryouts/parsers/shared_methods.rb
178
179
  - lib/tryouts/test_batch.rb
179
180
  - lib/tryouts/test_case.rb
@@ -1,122 +0,0 @@
1
- # lib/tryouts/parsers/prism_parser.rb
2
-
3
- require_relative '../test_case'
4
- require_relative 'base_parser'
5
-
6
- class Tryouts
7
- # Fixed PrismParser with pattern matching for robust token filtering
8
- class PrismParser < Tryouts::Parsers::BaseParser
9
-
10
- def parse
11
- return handle_syntax_errors if @prism_result.failure?
12
-
13
- tokens = tokenize_content
14
- test_boundaries = find_test_case_boundaries(tokens)
15
- tokens = classify_potential_descriptions_with_boundaries(tokens, test_boundaries)
16
- test_blocks = group_into_test_blocks(tokens)
17
- process_test_blocks(test_blocks)
18
- end
19
-
20
- private
21
-
22
- # Tokenize content using pattern matching for clean line classification
23
- def tokenize_content
24
- tokens = []
25
-
26
- @lines.each_with_index do |line, index|
27
- token = case line
28
- in /^##\s*(.*)$/ # Test description format: ## description
29
- { type: :description, content: $1.strip, line: index }
30
- in /^#\s*TEST\s*\d*:\s*(.*)$/ # rubocop:disable Lint/DuplicateBranch
31
- { type: :description, content: $1.strip, line: index }
32
- in /^#\s*=!>\s*(.*)$/ # Exception expectation (updated for consistency)
33
- { type: :exception_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
34
- in /^#\s*=<>\s*(.*)$/ # Intentional failure expectation
35
- { type: :intentional_failure_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
36
- in /^#\s*==>\s*(.*)$/ # Boolean true expectation
37
- { type: :true_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
38
- in %r{^#\s*=/=>\s*(.*)$} # Boolean false expectation
39
- { type: :false_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
40
- in /^#\s*=\|>\s*(.*)$/ # Boolean (true or false) expectation
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 }
44
- in /^#\s*=:>\s*(.*)$/ # Result type expectation
45
- { type: :result_type_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
46
- in /^#\s*=~>\s*(.*)$/ # Regex match expectation
47
- { type: :regex_match_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
48
- in /^#\s*=%>\s*(.*)$/ # Performance time expectation
49
- { type: :performance_time_expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
50
- in /^#\s*=(\d+)>\s*(.*)$/ # Output expectation (stdout/stderr with pipe number)
51
- { type: :output_expectation, content: $2.strip, pipe: $1.to_i, line: index, ast: parse_expectation($2.strip) }
52
- in /^#\s*=>\s*(.*)$/ # Regular expectation
53
- { type: :expectation, content: $1.strip, line: index, ast: parse_expectation($1.strip) }
54
- in /^##\s*=>\s*(.*)$/ # Commented out expectation (should be ignored)
55
- { type: :comment, content: '=>' + $1.strip, line: index }
56
- in /^#\s*(.*)$/ # Single hash comment - potential description
57
- { type: :potential_description, content: $1.strip, line: index }
58
- in /^\s*$/ # Blank line
59
- { type: :blank, line: index }
60
- else # Ruby code
61
- { type: :code, content: line, line: index, ast: parse_ruby_line(line) }
62
- end
63
-
64
- tokens << token
65
- end
66
-
67
- # Return tokens with potential_descriptions - they'll be classified later with test boundaries
68
- tokens
69
- end
70
-
71
-
72
- # Convert potential_descriptions to descriptions or comments based on context
73
- def classify_potential_descriptions(tokens)
74
- tokens.map.with_index do |token, index|
75
- if token[:type] == :potential_description
76
- # Check if this looks like a test description based on content and context
77
- content = token[:content].strip
78
-
79
- # Skip if it's clearly just a regular comment (short, lowercase, etc.)
80
- # Test descriptions are typically longer and more descriptive
81
- looks_like_regular_comment = content.length < 20 &&
82
- content.downcase == content &&
83
- !content.match?(/test|example|demonstrate|show/i)
84
-
85
- # Check if there's code immediately before this (suggesting it's mid-test)
86
- prev_token = index > 0 ? tokens[index - 1] : nil
87
- has_code_before = prev_token && prev_token[:type] == :code
88
-
89
- if looks_like_regular_comment || has_code_before
90
- # Treat as regular comment
91
- token.merge(type: :comment)
92
- else
93
- # Look ahead for test pattern: code + at least one expectation within reasonable distance
94
- following_tokens = tokens[(index + 1)..]
95
-
96
- # Skip blanks and comments to find meaningful content
97
- meaningful_following = following_tokens.reject { |t| [:blank, :comment].include?(t[:type]) }
98
-
99
- # Look for test pattern: at least one code token followed by at least one expectation
100
- # within the next 10 meaningful tokens (to avoid matching setup/teardown)
101
- test_window = meaningful_following.first(10)
102
- has_code = test_window.any? { |t| t[:type] == :code }
103
- has_expectation = test_window.any? { |t| is_expectation_type?(t[:type]) }
104
-
105
- if has_code && has_expectation
106
- token.merge(type: :description)
107
- else
108
- token.merge(type: :comment)
109
- end
110
- end
111
- else
112
- token
113
- end
114
- end
115
- end
116
-
117
- # Parser type identification for metadata
118
- def parser_type
119
- :prism_v2_fixed
120
- end
121
- end
122
- end