tryouts 3.5.1 → 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.
@@ -1,16 +1,74 @@
1
1
  # lib/tryouts/parsers/enhanced_parser.rb
2
2
 
3
- # Enhanced parser using Prism's inhouse comment extraction capabilities
4
- # Drop-in replacement for PrismParser that eliminates HEREDOC parsing issues
5
-
6
3
  require_relative '../test_case'
7
4
  require_relative 'base_parser'
8
5
 
9
6
  class Tryouts
10
- # Enhanced parser that replaces manual line-by-line parsing with inhouse Prism APIs
11
- # while maintaining full compatibility with the original parser's logic structure
7
+ # Enhanced parser using Prism's native comment extraction for robust parsing
8
+ #
9
+ # The EnhancedParser is the default parser that provides syntax-aware comment
10
+ # extraction by leveraging Ruby's official Prism parser. This approach eliminates
11
+ # common parsing issues found in regex-based parsers, particularly with complex
12
+ # Ruby syntax.
13
+ #
14
+ # @example Basic usage
15
+ # parser = Tryouts::EnhancedParser.new(source_code, file_path)
16
+ # testrun = parser.parse
17
+ # puts testrun.test_cases.length
18
+ #
19
+ # @example Problematic code that EnhancedParser handles correctly
20
+ # source = <<~RUBY
21
+ # ## Test HEREDOC handling
22
+ # sql = <<~SQL
23
+ # SELECT * FROM users
24
+ # -- This is NOT a tryout comment
25
+ # #=> This is NOT a tryout expectation
26
+ # SQL
27
+ # puts sql.length
28
+ # #=> Integer # This IS a real expectation
29
+ # RUBY
30
+ #
31
+ # @!attribute [r] parser_type
32
+ # @return [Symbol] Returns :enhanced to identify parser type
33
+ #
34
+ # ## Key Benefits over LegacyParser
35
+ #
36
+ # ### 1. HEREDOC Safety
37
+ # - Uses Prism's `parse_comments()` to extract only actual Ruby comments
38
+ # - Automatically excludes content inside string literals, HEREDOCs, and interpolation
39
+ # - Prevents false positive expectation detection
40
+ #
41
+ # ### 2. Inline Comment Handling
42
+ # - Correctly handles lines with both code and comments
43
+ # - Supports multiple comments per line with proper positioning
44
+ # - Emits separate tokens for code and comment content
45
+ #
46
+ # ### 3. Syntax Awareness
47
+ # - Leverages Ruby's official parser for accurate code understanding
48
+ # - Handles complex Ruby syntax edge cases reliably
49
+ # - More robust than regex-based parsing approaches
50
+ #
51
+ # ### 4. Performance
52
+ # - Uses optimized C-based Prism parsing for comment extraction
53
+ # - Efficient handling of large files with complex syntax
54
+ #
55
+ # ## Pattern Matching
56
+ # Uses Ruby 3.4+ pattern matching (`case/in`) for token classification,
57
+ # providing clean, modern syntax for expectation type detection.
58
+ #
59
+ # @see LegacyParser For simpler regex-based parsing (legacy compatibility)
60
+ # @see BaseParser For shared parsing functionality
61
+ # @since 3.2.0
12
62
  class EnhancedParser < Tryouts::Parsers::BaseParser
13
63
 
64
+ # Parse source code into a Testrun using Prism-based comment extraction
65
+ #
66
+ # This method provides the main parsing logic that converts raw Ruby source
67
+ # code containing tryout syntax into structured test cases. Uses Prism's
68
+ # native comment extraction to avoid HEREDOC parsing issues.
69
+ #
70
+ # @return [Tryouts::Testrun] Structured test data with setup, test cases, teardown, and warnings
71
+ # @raise [Tryouts::TryoutSyntaxError] If source contains syntax errors or strict mode violations
14
72
  def parse
15
73
  return handle_syntax_errors if @prism_result.failure?
16
74
 
@@ -25,7 +83,20 @@ class Tryouts
25
83
 
26
84
  private
27
85
 
28
- # Inhouse comment extraction - replaces the manual regex parsing
86
+ # Extract and tokenize comments using Prism's native comment extraction
87
+ #
88
+ # This method replaces manual line-by-line regex parsing with Prism's
89
+ # built-in comment extraction capabilities. The key benefit is that
90
+ # `Prism.parse_comments()` only returns actual Ruby comments, automatically
91
+ # excluding content inside string literals, HEREDOCs, and interpolations.
92
+ #
93
+ # @return [Array<Hash>] Array of token hashes with keys :type, :content, :line, etc.
94
+ # @example Token structure
95
+ # [
96
+ # { type: :description, content: "Test case description", line: 5 },
97
+ # { type: :code, content: "result = calculate(x)", line: 6 },
98
+ # { type: :expectation, content: "42", line: 7, ast: <Prism::Node> }
99
+ # ]
29
100
  def tokenize_content_with_inhouse_extraction
30
101
  tokens = []
31
102
 
@@ -47,8 +118,8 @@ class Tryouts
47
118
  tokens << { type: :code, content: line, line: index, ast: parse_ruby_line(line) }
48
119
  emitted_code = true
49
120
  end
50
- # Inline comment may carry expectations; classify it too
51
- tokens << classify_comment_inhousely(comment_content, line_number)
121
+ # Inline comment (after code) - treat as regular comment, not expectation
122
+ tokens << { type: :comment, content: comment_content.sub(/^#\s*/, ''), line: line_number - 1 }
52
123
  else
53
124
  tokens << classify_comment_inhousely(comment_content, line_number)
54
125
  end
@@ -69,44 +140,125 @@ class Tryouts
69
140
  tokens
70
141
  end
71
142
 
72
- # Inhouse comment classification - replaces complex regex patterns
143
+ # Classify comment content into specific token types using pattern matching
144
+ #
145
+ # Takes a raw comment string and determines what type of tryout token it
146
+ # represents (description, expectation, etc.). Uses Ruby 3.4+ pattern matching
147
+ # for clean, maintainable classification logic.
148
+ #
149
+ # @param content [String] The comment content (including # prefix)
150
+ # @param line_number [Integer] 1-based line number for error reporting
151
+ # @return [Hash] Token hash with :type, :content, :line and other type-specific keys
152
+ #
153
+ # @example Valid expectation
154
+ # classify_comment_inhousely("#=> 42", 10)
155
+ # # => { type: :expectation, content: "42", line: 9, ast: <Prism::Node> }
156
+ #
157
+ # @example Malformed expectation (triggers warning)
158
+ # classify_comment_inhousely("#=INVALID> 42", 10)
159
+ # # => { type: :malformed_expectation, syntax: "INVALID", content: "42", line: 9 }
160
+ # # Also adds warning to parser's warning collection
161
+ #
162
+ # @example Test description
163
+ # classify_comment_inhousely("## Test basic math", 5)
164
+ # # => { type: :description, content: "Test basic math", line: 4 }
73
165
  def classify_comment_inhousely(content, line_number)
74
166
  case content
75
- when /^##\s*(.*)$/
167
+ in /^##\s*(.*)$/ # Test description format: ## description
76
168
  { type: :description, content: $1.strip, line: line_number - 1 }
77
- when /^#\s*TEST\s*\d*:\s*(.*)$/
169
+ in /^#\s*TEST\s*\d*:\s*(.*)$/ # rubocop:disable Lint/DuplicateBranch
78
170
  { type: :description, content: $1.strip, line: line_number - 1 }
79
- when /^#\s*=!>\s*(.*)$/
171
+ in /^#\s*=!>\s*(.*)$/ # Exception expectation
80
172
  { type: :exception_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
81
- when /^#\s*=<>\s*(.*)$/
173
+ in /^#\s*=<>\s*(.*)$/ # Intentional failure expectation
82
174
  { type: :intentional_failure_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
83
- when /^#\s*==>\s*(.*)$/
175
+ in /^#\s*==>\s*(.*)$/ # Boolean true expectation
84
176
  { type: :true_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
85
- when %r{^#\s*=/=>\s*(.*)$}
177
+ in %r{^#\s*=/=>\s*(.*)$} # Boolean false expectation
86
178
  { type: :false_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
87
- when /^#\s*=\|>\s*(.*)$/
179
+ in /^#\s*=\|>\s*(.*)$/ # Boolean (true or false) expectation
88
180
  { type: :boolean_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
89
- when /^#\s*=\*>\s*(.*)$/
181
+ in /^#\s*=\*>\s*(.*)$/ # Non-nil expectation
90
182
  { type: :non_nil_expectation, content: $1.strip, line: line_number - 1 }
91
- when /^#\s*=:>\s*(.*)$/
183
+ in /^#\s*=:>\s*(.*)$/ # Result type expectation
92
184
  { type: :result_type_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
93
- when /^#\s*=~>\s*(.*)$/
185
+ in /^#\s*=~>\s*(.*)$/ # Regex match expectation
94
186
  { type: :regex_match_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
95
- when /^#\s*=%>\s*(.*)$/
187
+ in /^#\s*=%>\s*(.*)$/ # Performance time expectation
96
188
  { type: :performance_time_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
97
- when /^#\s*=(\d+)>\s*(.*)$/
189
+ in /^#\s*=(\d+)>\s*(.*)$/ # Output expectation (stdout/stderr with pipe number)
98
190
  { type: :output_expectation, content: $2.strip, pipe: $1.to_i, line: line_number - 1, ast: parse_expectation($2.strip) }
99
- when /^#\s*=>\s*(.*)$/
191
+ in /^#\s*=>\s*(.*)$/ # Regular expectation
100
192
  { type: :expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
101
- when /^##\s*=>\s*(.*)$/
193
+ in /^#\s*=([^>=:!~%*|\/\s]+)>\s*(.*)$/ # Malformed expectation - invalid characters between = and >
194
+ syntax = $1
195
+ content_part = $2.strip
196
+ add_warning(ParserWarning.malformed_expectation(
197
+ line_number: line_number,
198
+ syntax: syntax,
199
+ context: content.strip
200
+ ))
201
+ { type: :malformed_expectation, syntax: syntax, content: content_part, line: line_number - 1 }
202
+ in /^##\s*=>\s*(.*)$/ # Commented out expectation (should be ignored)
102
203
  { type: :comment, content: '=>' + $1.strip, line: line_number - 1 }
103
- when /^#\s*(.*)$/
204
+ in content if looks_like_malformed_expectation?(content) # Comprehensive malformed expectation detection
205
+ detected_syntax = extract_malformed_syntax(content)
206
+ add_warning(ParserWarning.malformed_expectation(
207
+ line_number: line_number,
208
+ syntax: detected_syntax,
209
+ context: content.strip
210
+ ))
211
+ { type: :malformed_expectation, syntax: detected_syntax, content: content.strip, line: line_number - 1 }
212
+ in /^#\s*(.*)$/ # Single hash comment - potential description
104
213
  { type: :potential_description, content: $1.strip, line: line_number - 1 }
105
- else
214
+ else # Unknown comment format
106
215
  { type: :comment, content: content.sub(/^#\s*/, ''), line: line_number - 1 }
107
216
  end
108
217
  end
109
218
 
219
+ # Detect if a comment looks like a malformed expectation attempt
220
+ # This catches patterns that suggest someone tried to write an expectation
221
+ # but got the syntax wrong (missing parts, wrong spacing, extra characters, etc.)
222
+ #
223
+ # Only flags as malformed if it starts with patterns that look like expectation syntax,
224
+ # not just any comment that happens to contain equals signs in natural language.
225
+ def looks_like_malformed_expectation?(content)
226
+ # Skip if it's already handled by specific patterns above
227
+ return false if content.match?(/^##\s*/) # Description
228
+ return false if content.match?(/^#\s*TEST\s*\d*:\s*/) # TEST format
229
+ return false if content.match?(/^##\s*=>\s*/) # Commented out expectation
230
+
231
+ # Only flag as malformed if it looks like an expectation attempt at the start
232
+ # Patterns that suggest malformed expectation syntax:
233
+ # - Starts with #= but doesn't match valid patterns
234
+ # - Has multiple = or > characters in suspicious positions near the start
235
+ # - Looks like broken expectation syntax (not natural language)
236
+
237
+ return true if content.match?(/^#\s*=\s*>/) # "# = >" (spaces in wrong places)
238
+ return true if content.match?(/^#\s*==+>/) # "# ==> " (wrong number of =)
239
+ return true if content.match?(/^#\s*=[^=:!~%*|\/>\s][^>]*>/) # "# =X> " (invalid chars between = and >)
240
+ return true if content.match?(/^#\s*>[^=]/) # "# >something" (starts with >)
241
+ return true if content.match?(/^#\s*<[^=]/) # "# <something" (starts with <)
242
+
243
+ false # Regular comments with = signs in natural language are OK
244
+ end
245
+
246
+ # Extract the malformed syntax portion for warning display
247
+ def extract_malformed_syntax(content)
248
+ # Try to identify what the user was attempting to write
249
+ case content
250
+ when /^#\s*([=><][^=><]*[=><].*?)(\s|$)/ # Pattern with expectation chars
251
+ $1.strip
252
+ when /^#\s*([=><].*?)(\s|$)/ # Simple pattern starting with expectation char
253
+ $1.strip
254
+ when /^#\s*(.*?[=><].*?)(\s|$)/ # Pattern containing expectation chars
255
+ $1.strip
256
+ else
257
+ # Fallback: show the part after #
258
+ content.sub(/^#\s*/, '').split(/\s/).first || 'unknown'
259
+ end
260
+ end
261
+
110
262
  # Parser type identification for metadata
111
263
  def parser_type
112
264
  :enhanced
@@ -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