tryouts 3.3.0 → 3.3.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/exe/try +1 -1
- data/lib/tryouts/cli/formatters/compact.rb +8 -4
- data/lib/tryouts/cli/formatters/quiet.rb +4 -3
- data/lib/tryouts/cli/formatters/verbose.rb +8 -4
- data/lib/tryouts/cli/opts.rb +14 -4
- data/lib/tryouts/console.rb +32 -4
- data/lib/tryouts/expectation_evaluators/exception.rb +8 -2
- data/lib/tryouts/expectation_evaluators/non_nil.rb +77 -0
- data/lib/tryouts/expectation_evaluators/regex_match.rb +11 -3
- data/lib/tryouts/expectation_evaluators/registry.rb +2 -0
- data/lib/tryouts/expectation_evaluators/result_type.rb +9 -1
- data/lib/tryouts/file_processor.rb +9 -5
- data/lib/tryouts/parsers/base_parser.rb +23 -0
- data/lib/tryouts/parsers/enhanced_parser.rb +115 -0
- data/lib/tryouts/parsers/prism_parser.rb +122 -0
- data/lib/tryouts/parsers/shared_methods.rb +416 -0
- data/lib/tryouts/test_batch.rb +54 -13
- data/lib/tryouts/test_case.rb +3 -3
- data/lib/tryouts/test_executor.rb +6 -4
- data/lib/tryouts/test_result_aggregator.rb +138 -0
- data/lib/tryouts/test_runner.rb +76 -20
- data/lib/tryouts/version.rb +1 -1
- data/lib/tryouts.rb +7 -2
- metadata +21 -3
- data/lib/tryouts/enhanced_parser.rb +0 -461
- data/lib/tryouts/prism_parser.rb +0 -516
@@ -1,461 +0,0 @@
|
|
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
|