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.
- checksums.yaml +4 -4
- data/README.md +5 -8
- data/lib/tryouts/cli/formatters/agent.rb +33 -15
- data/lib/tryouts/cli/opts.rb +15 -15
- data/lib/tryouts/file_processor.rb +2 -2
- data/lib/tryouts/parser_warning.rb +10 -0
- data/lib/tryouts/parsers/CLAUDE.md +178 -0
- data/lib/tryouts/parsers/base_parser.rb +101 -1
- data/lib/tryouts/parsers/enhanced_parser.rb +177 -25
- data/lib/tryouts/parsers/legacy_parser.rb +254 -0
- data/lib/tryouts/parsers/shared_methods.rb +5 -1
- data/lib/tryouts/test_batch.rb +1 -1
- data/lib/tryouts/test_executor.rb +12 -4
- data/lib/tryouts/test_result_aggregator.rb +15 -11
- data/lib/tryouts/test_runner.rb +18 -15
- data/lib/tryouts/version.rb +1 -1
- data/lib/tryouts.rb +1 -1
- metadata +3 -2
- data/lib/tryouts/parsers/prism_parser.rb +0 -122
@@ -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
|
11
|
-
#
|
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
|
-
#
|
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
|
51
|
-
tokens <<
|
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
|
-
#
|
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
|
-
|
167
|
+
in /^##\s*(.*)$/ # Test description format: ## description
|
76
168
|
{ type: :description, content: $1.strip, line: line_number - 1 }
|
77
|
-
|
169
|
+
in /^#\s*TEST\s*\d*:\s*(.*)$/ # rubocop:disable Lint/DuplicateBranch
|
78
170
|
{ type: :description, content: $1.strip, line: line_number - 1 }
|
79
|
-
|
171
|
+
in /^#\s*=!>\s*(.*)$/ # Exception expectation
|
80
172
|
{ type: :exception_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
|
81
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
181
|
+
in /^#\s*=\*>\s*(.*)$/ # Non-nil expectation
|
90
182
|
{ type: :non_nil_expectation, content: $1.strip, line: line_number - 1 }
|
91
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
191
|
+
in /^#\s*=>\s*(.*)$/ # Regular expectation
|
100
192
|
{ type: :expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
|
101
|
-
|
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
|
-
|
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
|
|
data/lib/tryouts/test_batch.rb
CHANGED
@@ -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
|
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
|
-
#
|
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
|
-
|
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
|
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
|
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? ?
|
139
|
+
parts.empty? ? 'All tests passed' : parts.join(', ')
|
136
140
|
end
|
137
141
|
end
|
138
142
|
end
|