tryouts 3.2.1 → 3.3.0
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 +1 -1
- data/exe/try +1 -1
- data/lib/tryouts/cli/formatters/compact.rb +2 -2
- data/lib/tryouts/cli/formatters/live_status_manager.rb +2 -2
- data/lib/tryouts/cli/formatters/verbose.rb +2 -1
- data/lib/tryouts/cli/opts.rb +4 -1
- data/lib/tryouts/enhanced_parser.rb +461 -0
- data/lib/tryouts/failure_collector.rb +2 -2
- data/lib/tryouts/file_processor.rb +19 -1
- data/lib/tryouts/prism_parser.rb +2 -1
- data/lib/tryouts/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a7ef307bf73a8bd6c70f52c47337d550ae7715e0b64d15c7f708c9fd407f3df2
|
4
|
+
data.tar.gz: ccd166f03573bee1d31e3843ff95964cf5065ab4a1b2386e9769ec065a6958f5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 22b0d458cfadd632495c4df95f41cca3c06c6e6b3e219b18afb0fd21bfc14d1d9a872b165fcd2dd1c26052a9f57b84dad77c4a5a14c3efa08f62d74f0ab34467
|
7
|
+
data.tar.gz: 8cd6db263235736e26163b84568ad870de5835da3002e6f71226d961fc21e19d9eaef3cf2e02598a3907cb2701665e72a7e21ecea05b540f10ba37daa530a391
|
data/README.md
CHANGED
@@ -134,7 +134,7 @@ try -D # debug mode
|
|
134
134
|
|
135
135
|
### Core Components
|
136
136
|
|
137
|
-
- **Prism Parser**:
|
137
|
+
- **Prism Parser**: Inhouse Ruby parsing with pattern matching for line classification
|
138
138
|
- **Data Structures**: Immutable `Data.define` classes for test representation
|
139
139
|
- **Framework Translators**: Convert tryouts to RSpec/Minitest format
|
140
140
|
- **CLI**: Modern command-line interface with framework selection
|
data/exe/try
CHANGED
@@ -63,10 +63,10 @@ class Tryouts
|
|
63
63
|
|
64
64
|
# Include line number with file path for easy copying/clicking
|
65
65
|
location = if failure.line_number > 0
|
66
|
-
"#{pretty_path}:#{failure.line_number}"
|
66
|
+
"#{pretty_path}:#{failure.line_number + 1}"
|
67
67
|
else
|
68
68
|
pretty_path
|
69
|
-
|
69
|
+
end
|
70
70
|
|
71
71
|
puts " #{location}"
|
72
72
|
puts " #{Console.color(:red, '✗')} #{failure.description}"
|
@@ -6,7 +6,7 @@ require_relative 'tty_status_display'
|
|
6
6
|
class Tryouts
|
7
7
|
class CLI
|
8
8
|
# Centralized manager for live status display across all formatters
|
9
|
-
# Replaces the decorator pattern with
|
9
|
+
# Replaces the decorator pattern with inhouse integration
|
10
10
|
class LiveStatusManager
|
11
11
|
def initialize(formatter, options = {})
|
12
12
|
@formatter = formatter
|
@@ -20,7 +20,7 @@ class Tryouts
|
|
20
20
|
@display = TTYStatusDisplay.new(@formatter.stdout, options)
|
21
21
|
@status_reserved = false
|
22
22
|
|
23
|
-
debug_log('LiveStatusManager: Enabled with
|
23
|
+
debug_log('LiveStatusManager: Enabled with inhouse integration')
|
24
24
|
end
|
25
25
|
|
26
26
|
def enabled?
|
@@ -53,6 +53,7 @@ class Tryouts
|
|
53
53
|
|
54
54
|
puts
|
55
55
|
write '=' * 50
|
56
|
+
puts
|
56
57
|
puts Console.color(:red, 'Failed Tests:')
|
57
58
|
|
58
59
|
failure_collector.failures_by_file.each do |file_path, failures|
|
@@ -61,7 +62,7 @@ class Tryouts
|
|
61
62
|
|
62
63
|
# Include line number with file path for easy copying/clicking
|
63
64
|
if failure.line_number > 0
|
64
|
-
location = "#{pretty_path}:#{failure.line_number}"
|
65
|
+
location = "#{pretty_path}:#{failure.line_number + 1}"
|
65
66
|
else
|
66
67
|
location = pretty_path
|
67
68
|
end
|
data/lib/tryouts/cli/opts.rb
CHANGED
@@ -66,6 +66,10 @@ class Tryouts
|
|
66
66
|
opts.on('-c', '--compact', 'Compact single-line output') { options[:compact] = true }
|
67
67
|
opts.on('-l', '--live', 'Live status display') { options[:live_status] = true }
|
68
68
|
|
69
|
+
opts.separator "\nParser Options:"
|
70
|
+
opts.on('--enhanced-parser', 'Use enhanced parser with inhouse comment extraction') { options[:parser] = :enhanced }
|
71
|
+
opts.on('--legacy-parser', 'Use legacy parser (current default)') { options[:parser] = :prism }
|
72
|
+
|
69
73
|
opts.separator "\nInspection Options:"
|
70
74
|
opts.on('-i', '--inspect', 'Inspect file structure without running tests') { options[:inspect] = true }
|
71
75
|
|
@@ -87,7 +91,6 @@ class Tryouts
|
|
87
91
|
Tryouts.trace "Parsed files: #{files.inspect}, options: #{options.inspect}"
|
88
92
|
[files, options]
|
89
93
|
rescue OptionParser::InvalidOption => ex
|
90
|
-
Tryouts.info Console.color(:red, "Invalid option error: #{ex.message}")
|
91
94
|
warn "Error: #{ex.message}"
|
92
95
|
warn "Try 'try --help' for more information."
|
93
96
|
exit 1
|
@@ -0,0 +1,461 @@
|
|
1
|
+
# Enhanced parser using Prism's inhouse comment extraction capabilities
|
2
|
+
# Drop-in replacement for PrismParser that eliminates HEREDOC parsing issues
|
3
|
+
|
4
|
+
require 'prism'
|
5
|
+
require_relative 'test_case'
|
6
|
+
|
7
|
+
class Tryouts
|
8
|
+
# Enhanced parser that replaces manual line-by-line parsing with inhouse Prism APIs
|
9
|
+
# while maintaining full compatibility with the original parser's logic structure
|
10
|
+
class EnhancedParser
|
11
|
+
def initialize(source_path)
|
12
|
+
@source_path = source_path
|
13
|
+
@source = File.read(source_path)
|
14
|
+
@lines = @source.lines.map(&:chomp)
|
15
|
+
@prism_result = Prism.parse(@source)
|
16
|
+
@parsed_at = Time.now
|
17
|
+
end
|
18
|
+
|
19
|
+
def parse
|
20
|
+
return handle_syntax_errors if @prism_result.failure?
|
21
|
+
|
22
|
+
# Use inhouse comment extraction instead of line-by-line regex parsing
|
23
|
+
# This automatically excludes HEREDOC content!
|
24
|
+
tokens = tokenize_content_with_inhouse_extraction
|
25
|
+
test_boundaries = find_test_case_boundaries(tokens)
|
26
|
+
tokens = classify_potential_descriptions_with_boundaries(tokens, test_boundaries)
|
27
|
+
test_blocks = group_into_test_blocks(tokens)
|
28
|
+
process_test_blocks(test_blocks)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# Inhouse comment extraction - replaces the manual regex parsing
|
34
|
+
def tokenize_content_with_inhouse_extraction
|
35
|
+
tokens = []
|
36
|
+
|
37
|
+
# Get all comments using inhouse Prism extraction
|
38
|
+
comments = Prism.parse_comments(@source)
|
39
|
+
comment_by_line = comments.group_by { |comment| comment.location.start_line }
|
40
|
+
|
41
|
+
# Process each line, handling multiple comments per line
|
42
|
+
@lines.each_with_index do |line, index|
|
43
|
+
line_number = index + 1
|
44
|
+
|
45
|
+
if (comments_for_line = comment_by_line[line_number]) && !comments_for_line.empty?
|
46
|
+
emitted_code = false
|
47
|
+
comments_for_line.sort_by! { |c| c.location.start_column }
|
48
|
+
comments_for_line.each do |comment|
|
49
|
+
comment_content = comment.slice.strip
|
50
|
+
if comment.location.start_column > 0
|
51
|
+
unless emitted_code
|
52
|
+
tokens << { type: :code, content: line, line: index, ast: parse_ruby_line(line) }
|
53
|
+
emitted_code = true
|
54
|
+
end
|
55
|
+
# Inline comment may carry expectations; classify it too
|
56
|
+
tokens << classify_comment_inhousely(comment_content, line_number)
|
57
|
+
else
|
58
|
+
tokens << classify_comment_inhousely(comment_content, line_number)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
next
|
62
|
+
end
|
63
|
+
|
64
|
+
# Handle non-comment lines (blank lines and code)
|
65
|
+
token = case line
|
66
|
+
when /^\s*$/
|
67
|
+
{ type: :blank, line: index }
|
68
|
+
else
|
69
|
+
{ type: :code, content: line, line: index, ast: parse_ruby_line(line) }
|
70
|
+
end
|
71
|
+
tokens << token
|
72
|
+
end
|
73
|
+
|
74
|
+
tokens
|
75
|
+
end
|
76
|
+
|
77
|
+
# Inhouse comment classification - replaces complex regex patterns
|
78
|
+
def classify_comment_inhousely(content, line_number)
|
79
|
+
case content
|
80
|
+
when /^##\s*(.*)$/
|
81
|
+
{ type: :description, content: $1.strip, line: line_number - 1 }
|
82
|
+
when /^#\s*TEST\s*\d*:\s*(.*)$/
|
83
|
+
{ type: :description, content: $1.strip, line: line_number - 1 }
|
84
|
+
when /^#\s*=!>\s*(.*)$/
|
85
|
+
{ type: :exception_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
|
86
|
+
when /^#\s*=<>\s*(.*)$/
|
87
|
+
{ type: :intentional_failure_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
|
88
|
+
when /^#\s*==>\s*(.*)$/
|
89
|
+
{ type: :true_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
|
90
|
+
when %r{^#\s*=/=>\s*(.*)$}
|
91
|
+
{ type: :false_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
|
92
|
+
when /^#\s*=\|>\s*(.*)$/
|
93
|
+
{ type: :boolean_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
|
94
|
+
when /^#\s*=:>\s*(.*)$/
|
95
|
+
{ type: :result_type_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
|
96
|
+
when /^#\s*=~>\s*(.*)$/
|
97
|
+
{ type: :regex_match_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
|
98
|
+
when /^#\s*=%>\s*(.*)$/
|
99
|
+
{ type: :performance_time_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
|
100
|
+
when /^#\s*=(\d+)>\s*(.*)$/
|
101
|
+
{ type: :output_expectation, content: $2.strip, pipe: $1.to_i, line: line_number - 1, ast: parse_expectation($2.strip) }
|
102
|
+
when /^#\s*=>\s*(.*)$/
|
103
|
+
{ type: :expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
|
104
|
+
when /^##\s*=>\s*(.*)$/
|
105
|
+
{ type: :comment, content: '=>' + $1.strip, line: line_number - 1 }
|
106
|
+
when /^#\s*(.*)$/
|
107
|
+
{ type: :potential_description, content: $1.strip, line: line_number - 1 }
|
108
|
+
else
|
109
|
+
{ type: :comment, content: content.sub(/^#\s*/, ''), line: line_number - 1 }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Copy the rest of the methods from PrismParser to maintain identical behavior
|
114
|
+
|
115
|
+
def find_test_case_boundaries(tokens)
|
116
|
+
boundaries = []
|
117
|
+
|
118
|
+
tokens.each_with_index do |token, index|
|
119
|
+
if token[:type] == :description
|
120
|
+
start_line = token[:line]
|
121
|
+
end_line = find_test_case_end(tokens, index)
|
122
|
+
boundaries << { start: start_line, end: end_line } if end_line
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
boundaries
|
127
|
+
end
|
128
|
+
|
129
|
+
def find_test_case_end(tokens, start_index)
|
130
|
+
last_expectation_line = nil
|
131
|
+
|
132
|
+
(start_index + 1).upto(tokens.length - 1) do |i|
|
133
|
+
token = tokens[i]
|
134
|
+
break if token[:type] == :description
|
135
|
+
|
136
|
+
if is_expectation_type?(token[:type])
|
137
|
+
last_expectation_line = token[:line]
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
last_expectation_line
|
142
|
+
end
|
143
|
+
|
144
|
+
def classify_potential_descriptions_with_boundaries(tokens, test_boundaries)
|
145
|
+
tokens.map.with_index do |token, index|
|
146
|
+
if token[:type] == :potential_description
|
147
|
+
line_num = token[:line]
|
148
|
+
within_test_case = test_boundaries.any? do |boundary|
|
149
|
+
line_num >= boundary[:start] && line_num <= boundary[:end]
|
150
|
+
end
|
151
|
+
|
152
|
+
if within_test_case
|
153
|
+
token.merge(type: :comment)
|
154
|
+
else
|
155
|
+
content = token[:content].strip
|
156
|
+
|
157
|
+
looks_like_test_description = content.match?(/test|example|demonstrate|show|should|when|given/i) &&
|
158
|
+
content.length > 10
|
159
|
+
|
160
|
+
prev_token = index > 0 ? tokens[index - 1] : nil
|
161
|
+
has_code_before = prev_token && prev_token[:type] == :code
|
162
|
+
|
163
|
+
if has_code_before || !looks_like_test_description
|
164
|
+
token.merge(type: :comment)
|
165
|
+
else
|
166
|
+
following_tokens = tokens[(index + 1)..]
|
167
|
+
meaningful_following = following_tokens.reject { |t| [:blank, :comment].include?(t[:type]) }
|
168
|
+
test_window = meaningful_following.first(5)
|
169
|
+
has_code = test_window.any? { |t| t[:type] == :code }
|
170
|
+
has_expectation = test_window.any? { |t| is_expectation_type?(t[:type]) }
|
171
|
+
|
172
|
+
if has_code && has_expectation && looks_like_test_description
|
173
|
+
token.merge(type: :description)
|
174
|
+
else
|
175
|
+
token.merge(type: :comment)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
else
|
180
|
+
token
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def is_expectation_type?(type)
|
186
|
+
[
|
187
|
+
:expectation, :exception_expectation, :intentional_failure_expectation,
|
188
|
+
:true_expectation, :false_expectation, :boolean_expectation,
|
189
|
+
:result_type_expectation, :regex_match_expectation,
|
190
|
+
:performance_time_expectation, :output_expectation
|
191
|
+
].include?(type)
|
192
|
+
end
|
193
|
+
|
194
|
+
def group_into_test_blocks(tokens)
|
195
|
+
blocks = []
|
196
|
+
current_block = new_test_block
|
197
|
+
|
198
|
+
tokens.each do |token|
|
199
|
+
case [current_block, token]
|
200
|
+
in [_, { type: :description, content: String => desc, line: Integer => line_num }]
|
201
|
+
if !current_block[:description].empty? && current_block[:code].empty? && current_block[:expectations].empty?
|
202
|
+
current_block[:description] = [current_block[:description], desc].join(' ').strip
|
203
|
+
else
|
204
|
+
blocks << current_block if block_has_content?(current_block)
|
205
|
+
current_block = new_test_block.merge(description: desc, start_line: line_num)
|
206
|
+
end
|
207
|
+
|
208
|
+
in [{ expectations: [], start_line: nil }, { type: :code, content: String => code, line: Integer => line_num }]
|
209
|
+
current_block[:code] << token
|
210
|
+
current_block[:start_line] = line_num
|
211
|
+
|
212
|
+
in [{ expectations: [] }, { type: :code, content: String => code }]
|
213
|
+
current_block[:code] << token
|
214
|
+
|
215
|
+
in [{ expectations: Array => exps }, { type: :code }] if !exps.empty?
|
216
|
+
blocks << current_block
|
217
|
+
current_block = new_test_block.merge(code: [token], start_line: token[:line])
|
218
|
+
|
219
|
+
in [_, { type: :expectation }]
|
220
|
+
current_block[:expectations] << token
|
221
|
+
|
222
|
+
in [_, { type: :exception_expectation }]
|
223
|
+
current_block[:expectations] << token
|
224
|
+
|
225
|
+
in [_, { type: :intentional_failure_expectation }]
|
226
|
+
current_block[:expectations] << token
|
227
|
+
|
228
|
+
in [_, { type: :true_expectation }]
|
229
|
+
current_block[:expectations] << token
|
230
|
+
|
231
|
+
in [_, { type: :false_expectation }]
|
232
|
+
current_block[:expectations] << token
|
233
|
+
|
234
|
+
in [_, { type: :boolean_expectation }]
|
235
|
+
current_block[:expectations] << token
|
236
|
+
|
237
|
+
in [_, { type: :result_type_expectation }]
|
238
|
+
current_block[:expectations] << token
|
239
|
+
|
240
|
+
in [_, { type: :regex_match_expectation }]
|
241
|
+
current_block[:expectations] << token
|
242
|
+
|
243
|
+
in [_, { type: :performance_time_expectation }]
|
244
|
+
current_block[:expectations] << token
|
245
|
+
|
246
|
+
in [_, { type: :output_expectation }]
|
247
|
+
current_block[:expectations] << token
|
248
|
+
|
249
|
+
in [_, { type: :comment | :blank }]
|
250
|
+
add_context_to_block(current_block, token)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
blocks << current_block if block_has_content?(current_block)
|
255
|
+
classify_blocks(blocks)
|
256
|
+
end
|
257
|
+
|
258
|
+
def process_test_blocks(classified_blocks)
|
259
|
+
setup_blocks = classified_blocks.filter { |block| block[:type] == :setup }
|
260
|
+
test_blocks = classified_blocks.filter { |block| block[:type] == :test }
|
261
|
+
teardown_blocks = classified_blocks.filter { |block| block[:type] == :teardown }
|
262
|
+
|
263
|
+
Testrun.new(
|
264
|
+
setup: build_setup(setup_blocks),
|
265
|
+
test_cases: test_blocks.map { |block| build_test_case(block) },
|
266
|
+
teardown: build_teardown(teardown_blocks),
|
267
|
+
source_file: @source_path,
|
268
|
+
metadata: { parsed_at: @parsed_at, parser: :enhanced },
|
269
|
+
)
|
270
|
+
end
|
271
|
+
|
272
|
+
def build_setup(setup_blocks)
|
273
|
+
return Setup.new(code: '', line_range: 0..0, path: @source_path) if setup_blocks.empty?
|
274
|
+
|
275
|
+
Setup.new(
|
276
|
+
code: extract_pure_code_from_blocks(setup_blocks),
|
277
|
+
line_range: calculate_block_range(setup_blocks),
|
278
|
+
path: @source_path,
|
279
|
+
)
|
280
|
+
end
|
281
|
+
|
282
|
+
def build_teardown(teardown_blocks)
|
283
|
+
return Teardown.new(code: '', line_range: 0..0, path: @source_path) if teardown_blocks.empty?
|
284
|
+
|
285
|
+
Teardown.new(
|
286
|
+
code: extract_pure_code_from_blocks(teardown_blocks),
|
287
|
+
line_range: calculate_block_range(teardown_blocks),
|
288
|
+
path: @source_path,
|
289
|
+
)
|
290
|
+
end
|
291
|
+
|
292
|
+
def extract_pure_code_from_blocks(blocks)
|
293
|
+
blocks
|
294
|
+
.flat_map { |block| block[:code] }
|
295
|
+
.filter_map do |token|
|
296
|
+
case token
|
297
|
+
in { type: :code, content: String => content }
|
298
|
+
content
|
299
|
+
else
|
300
|
+
nil
|
301
|
+
end
|
302
|
+
end
|
303
|
+
.join("\n")
|
304
|
+
end
|
305
|
+
|
306
|
+
def calculate_block_range(blocks)
|
307
|
+
return 0..0 if blocks.empty?
|
308
|
+
|
309
|
+
valid_blocks = blocks.filter { |block| block[:start_line] && block[:end_line] }
|
310
|
+
return 0..0 if valid_blocks.empty?
|
311
|
+
|
312
|
+
line_ranges = valid_blocks.map { |block| block[:start_line]..block[:end_line] }
|
313
|
+
line_ranges.first.first..line_ranges.last.last
|
314
|
+
end
|
315
|
+
|
316
|
+
def extract_code_content(code_tokens)
|
317
|
+
code_tokens
|
318
|
+
.filter_map do |token|
|
319
|
+
case token
|
320
|
+
in { type: :code, content: String => content }
|
321
|
+
content
|
322
|
+
else
|
323
|
+
nil
|
324
|
+
end
|
325
|
+
end
|
326
|
+
.join("\n")
|
327
|
+
end
|
328
|
+
|
329
|
+
def parse_ruby_line(line)
|
330
|
+
return nil if line.strip.empty?
|
331
|
+
|
332
|
+
result = Prism.parse(line.strip)
|
333
|
+
case result
|
334
|
+
in { errors: [] => errors, value: { body: { body: [ast] } } }
|
335
|
+
ast
|
336
|
+
in { errors: Array => errors } if errors.any?
|
337
|
+
{ type: :parse_error, errors: errors, raw: line }
|
338
|
+
else
|
339
|
+
nil
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
def parse_expectation(expr)
|
344
|
+
parse_ruby_line(expr)
|
345
|
+
end
|
346
|
+
|
347
|
+
def new_test_block
|
348
|
+
{
|
349
|
+
description: '',
|
350
|
+
code: [],
|
351
|
+
expectations: [],
|
352
|
+
comments: [],
|
353
|
+
start_line: nil,
|
354
|
+
end_line: nil,
|
355
|
+
}
|
356
|
+
end
|
357
|
+
|
358
|
+
def block_has_content?(block)
|
359
|
+
case block
|
360
|
+
in { description: String => desc, code: Array => code, expectations: Array => exps }
|
361
|
+
!desc.empty? || !code.empty? || !exps.empty?
|
362
|
+
else
|
363
|
+
false
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
def add_context_to_block(block, token)
|
368
|
+
case [block[:expectations].empty?, token]
|
369
|
+
in [true, { type: :comment | :blank }]
|
370
|
+
block[:code] << token
|
371
|
+
in [false, { type: :comment | :blank }]
|
372
|
+
block[:comments] << token
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
def classify_blocks(blocks)
|
377
|
+
blocks.map.with_index do |block, index|
|
378
|
+
block_type = case block
|
379
|
+
in { expectations: [] } if index == 0
|
380
|
+
:setup
|
381
|
+
in { expectations: [] } if index == blocks.size - 1
|
382
|
+
:teardown
|
383
|
+
in { expectations: Array => exps } if !exps.empty?
|
384
|
+
:test
|
385
|
+
else
|
386
|
+
:preamble
|
387
|
+
end
|
388
|
+
|
389
|
+
block.merge(type: block_type, end_line: calculate_end_line(block))
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
def calculate_end_line(block)
|
394
|
+
content_tokens = [*block[:code], *block[:expectations]]
|
395
|
+
return block[:start_line] if content_tokens.empty?
|
396
|
+
|
397
|
+
content_tokens.map { |token| token[:line] }.max || block[:start_line]
|
398
|
+
end
|
399
|
+
|
400
|
+
def build_test_case(block)
|
401
|
+
case block
|
402
|
+
in {
|
403
|
+
type: :test,
|
404
|
+
description: String => desc,
|
405
|
+
code: Array => code_tokens,
|
406
|
+
expectations: Array => exp_tokens,
|
407
|
+
start_line: Integer => start_line,
|
408
|
+
end_line: Integer => end_line
|
409
|
+
}
|
410
|
+
source_lines = @lines[start_line..end_line]
|
411
|
+
first_expectation_line = exp_tokens.empty? ? start_line : exp_tokens.first[:line]
|
412
|
+
|
413
|
+
TestCase.new(
|
414
|
+
description: desc,
|
415
|
+
code: extract_code_content(code_tokens),
|
416
|
+
expectations: exp_tokens.map do |token|
|
417
|
+
type = case token[:type]
|
418
|
+
when :exception_expectation then :exception
|
419
|
+
when :intentional_failure_expectation then :intentional_failure
|
420
|
+
when :true_expectation then :true # rubocop:disable Lint/BooleanSymbol
|
421
|
+
when :false_expectation then :false # rubocop:disable Lint/BooleanSymbol
|
422
|
+
when :boolean_expectation then :boolean
|
423
|
+
when :result_type_expectation then :result_type
|
424
|
+
when :regex_match_expectation then :regex_match
|
425
|
+
when :performance_time_expectation then :performance_time
|
426
|
+
when :output_expectation then :output
|
427
|
+
else :regular
|
428
|
+
end
|
429
|
+
|
430
|
+
if token[:type] == :output_expectation
|
431
|
+
OutputExpectation.new(content: token[:content], type: type, pipe: token[:pipe])
|
432
|
+
else
|
433
|
+
Expectation.new(content: token[:content], type: type)
|
434
|
+
end
|
435
|
+
end,
|
436
|
+
line_range: start_line..end_line,
|
437
|
+
path: @source_path,
|
438
|
+
source_lines: source_lines,
|
439
|
+
first_expectation_line: first_expectation_line,
|
440
|
+
)
|
441
|
+
else
|
442
|
+
raise "Invalid test block structure: #{block}"
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
def handle_syntax_errors
|
447
|
+
errors = @prism_result.errors.map do |error|
|
448
|
+
line_context = @lines[error.location.start_line - 1] || ''
|
449
|
+
|
450
|
+
TryoutSyntaxError.new(
|
451
|
+
error.message,
|
452
|
+
line_number: error.location.start_line,
|
453
|
+
context: line_context,
|
454
|
+
source_file: @source_path,
|
455
|
+
)
|
456
|
+
end
|
457
|
+
|
458
|
+
raise errors.first if errors.any?
|
459
|
+
end
|
460
|
+
end
|
461
|
+
end
|
@@ -9,8 +9,8 @@ class Tryouts
|
|
9
9
|
# Data structure for a single failure entry
|
10
10
|
FailureEntry = Data.define(:file_path, :test_case, :result_packet) do
|
11
11
|
def line_number
|
12
|
-
# Use
|
13
|
-
test_case.
|
12
|
+
# Use first expectation line for consistency with main error display
|
13
|
+
test_case.first_expectation_line || test_case.line_range&.first || 0
|
14
14
|
end
|
15
15
|
|
16
16
|
def description
|
@@ -1,12 +1,15 @@
|
|
1
1
|
# lib/tryouts/file_processor.rb
|
2
2
|
|
3
3
|
require_relative 'prism_parser'
|
4
|
+
require_relative 'enhanced_parser'
|
4
5
|
require_relative 'test_executor'
|
5
6
|
require_relative 'cli/modes/inspect'
|
6
7
|
require_relative 'cli/modes/generate'
|
7
8
|
|
8
9
|
class Tryouts
|
9
10
|
class FileProcessor
|
11
|
+
# Supported parser types for validation and documentation
|
12
|
+
PARSER_TYPES = [:enhanced, :prism].freeze
|
10
13
|
def initialize(file:, options:, output_manager:, translator:, global_tally:)
|
11
14
|
@file = file
|
12
15
|
@options = options
|
@@ -16,7 +19,7 @@ class Tryouts
|
|
16
19
|
end
|
17
20
|
|
18
21
|
def process
|
19
|
-
testrun =
|
22
|
+
testrun = create_parser(@file, @options).parse
|
20
23
|
@global_tally[:file_count] += 1
|
21
24
|
@output_manager.file_parsed(@file, testrun.total_tests)
|
22
25
|
|
@@ -35,6 +38,21 @@ class Tryouts
|
|
35
38
|
|
36
39
|
private
|
37
40
|
|
41
|
+
def create_parser(file, options)
|
42
|
+
parser_type = options[:parser] || :prism # default to legacy for safe rollout
|
43
|
+
|
44
|
+
unless PARSER_TYPES.include?(parser_type)
|
45
|
+
raise ArgumentError, "Unknown parser: #{parser_type}. Allowed types: #{PARSER_TYPES.join(', ')}"
|
46
|
+
end
|
47
|
+
|
48
|
+
case parser_type
|
49
|
+
when :enhanced
|
50
|
+
EnhancedParser.new(file)
|
51
|
+
when :prism
|
52
|
+
PrismParser.new(file)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
38
56
|
def handle_inspect_mode(testrun)
|
39
57
|
mode = Tryouts::CLI::InspectMode.new(@file, testrun, @options, @output_manager, @translator)
|
40
58
|
mode.handle
|
data/lib/tryouts/prism_parser.rb
CHANGED
@@ -11,6 +11,7 @@ class Tryouts
|
|
11
11
|
@source = File.read(source_path)
|
12
12
|
@lines = @source.lines.map(&:chomp)
|
13
13
|
@prism_result = Prism.parse(@source)
|
14
|
+
@parsed_at = Time.now
|
14
15
|
end
|
15
16
|
|
16
17
|
def parse
|
@@ -308,7 +309,7 @@ class Tryouts
|
|
308
309
|
test_cases: test_blocks.map { |block| build_test_case(block) },
|
309
310
|
teardown: build_teardown(teardown_blocks),
|
310
311
|
source_file: @source_path,
|
311
|
-
metadata: { parsed_at:
|
312
|
+
metadata: { parsed_at: @parsed_at, parser: :prism_v2_fixed },
|
312
313
|
)
|
313
314
|
end
|
314
315
|
|
data/lib/tryouts/version.rb
CHANGED
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.
|
4
|
+
version: 3.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Delano Mandelbaum
|
@@ -137,6 +137,7 @@ files:
|
|
137
137
|
- lib/tryouts/cli/opts.rb
|
138
138
|
- lib/tryouts/cli/tty_detector.rb
|
139
139
|
- lib/tryouts/console.rb
|
140
|
+
- lib/tryouts/enhanced_parser.rb
|
140
141
|
- lib/tryouts/expectation_evaluators/base.rb
|
141
142
|
- lib/tryouts/expectation_evaluators/boolean.rb
|
142
143
|
- lib/tryouts/expectation_evaluators/exception.rb
|