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.
- checksums.yaml +4 -4
- data/README.md +5 -8
- data/exe/try +6 -0
- data/lib/tryouts/cli/formatters/agent.rb +206 -34
- data/lib/tryouts/cli/opts.rb +15 -15
- data/lib/tryouts/expectation_evaluators/result_type.rb +15 -0
- 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
@@ -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
|
data/lib/tryouts/test_runner.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# lib/tryouts/test_runner.rb
|
2
2
|
|
3
3
|
require 'concurrent'
|
4
|
-
require_relative 'parsers/
|
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
|
28
|
-
@options
|
29
|
-
@output_manager
|
30
|
-
@translator
|
31
|
-
@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] && (
|
48
|
-
|
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
|
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
|
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
|
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
|
188
|
-
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
|
193
|
+
file_counts = aggregator.get_file_counts
|
191
194
|
|
192
195
|
@output_manager.grand_total(
|
193
196
|
display_counts[:total_tests],
|
data/lib/tryouts/version.rb
CHANGED
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/
|
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.
|
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/
|
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
|