tryouts 3.4.0 → 3.5.1
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 +18 -3
- data/exe/try +14 -5
- data/lib/tryouts/cli/formatters/agent.rb +337 -57
- data/lib/tryouts/cli/formatters/base.rb +5 -1
- data/lib/tryouts/cli/formatters/compact.rb +14 -4
- data/lib/tryouts/cli/formatters/output_manager.rb +4 -0
- data/lib/tryouts/cli/formatters/verbose.rb +69 -56
- data/lib/tryouts/cli/line_spec_parser.rb +109 -0
- data/lib/tryouts/cli/opts.rb +6 -0
- data/lib/tryouts/cli.rb +22 -5
- data/lib/tryouts/expectation_evaluators/result_type.rb +15 -0
- data/lib/tryouts/file_processor.rb +37 -2
- data/lib/tryouts/parser_warning.rb +26 -0
- data/lib/tryouts/parsers/base_parser.rb +4 -1
- data/lib/tryouts/parsers/shared_methods.rb +50 -1
- data/lib/tryouts/test_case.rb +1 -1
- data/lib/tryouts/test_executor.rb +2 -0
- data/lib/tryouts/test_runner.rb +15 -5
- data/lib/tryouts/version.rb +1 -1
- metadata +3 -1
@@ -19,12 +19,7 @@ class Tryouts
|
|
19
19
|
def phase_header(message, file_count: nil)
|
20
20
|
return if message.include?('EXECUTING') # Skip execution phase headers
|
21
21
|
|
22
|
-
|
23
|
-
separator_line = '=' * @line_width
|
24
|
-
|
25
|
-
puts(separator_line)
|
26
|
-
puts(header_line)
|
27
|
-
puts(separator_line)
|
22
|
+
puts("=== #{message} ===")
|
28
23
|
end
|
29
24
|
|
30
25
|
# File-level operations
|
@@ -40,12 +35,25 @@ class Tryouts
|
|
40
35
|
extras << 'teardown' if teardown_present
|
41
36
|
message += " (#{extras.join(', ')})" unless extras.empty?
|
42
37
|
|
43
|
-
puts(indent_text(message,
|
38
|
+
puts(indent_text(message, 1))
|
39
|
+
end
|
40
|
+
|
41
|
+
def parser_warnings(file_path, warnings:)
|
42
|
+
return if warnings.empty? || !@options.fetch(:warnings, true)
|
43
|
+
|
44
|
+
puts
|
45
|
+
puts Console.color(:yellow, "Parser Warnings:")
|
46
|
+
warnings.each do |warning|
|
47
|
+
puts " #{Console.color(:yellow, 'WARNING')}: #{warning.message} (line #{warning.line_number})"
|
48
|
+
puts " #{Console.color(:dim, warning.context)}" unless warning.context.empty?
|
49
|
+
puts " #{Console.color(:blue, warning.suggestion)}"
|
50
|
+
end
|
51
|
+
puts
|
44
52
|
end
|
45
53
|
|
46
54
|
def file_execution_start(_file_path, test_count:, context_mode:)
|
47
55
|
message = "Running #{test_count} tests with #{context_mode} context"
|
48
|
-
puts(indent_text(message,
|
56
|
+
puts(indent_text(message, 0))
|
49
57
|
end
|
50
58
|
|
51
59
|
# Summary operations - show detailed failure summary
|
@@ -53,9 +61,7 @@ class Tryouts
|
|
53
61
|
return unless failure_collector.any_failures?
|
54
62
|
|
55
63
|
puts
|
56
|
-
write '
|
57
|
-
puts
|
58
|
-
puts Console.color(:red, 'Failed Tests:')
|
64
|
+
write '=== FAILURES ==='
|
59
65
|
|
60
66
|
# Number failures sequentially across all files instead of per-file
|
61
67
|
failure_number = 1
|
@@ -73,16 +79,14 @@ class Tryouts
|
|
73
79
|
puts
|
74
80
|
puts Console.color(:yellow, location)
|
75
81
|
puts " #{failure_number}) #{failure.description}"
|
76
|
-
puts " #{Console.color(:red,
|
82
|
+
puts " #{Console.color(:red, failure.failure_reason)}"
|
77
83
|
|
78
|
-
# Show source context in
|
84
|
+
# Show source context in compact format
|
79
85
|
if failure.source_context.any?
|
80
|
-
puts " #{Console.color(:cyan, 'Source:')}"
|
81
86
|
failure.source_context.each do |line|
|
82
87
|
puts " #{line.strip}"
|
83
88
|
end
|
84
89
|
end
|
85
|
-
puts
|
86
90
|
failure_number += 1
|
87
91
|
end
|
88
92
|
end
|
@@ -102,25 +106,27 @@ class Tryouts
|
|
102
106
|
|
103
107
|
time_str = elapsed_time ? " (#{elapsed_time.round(2)}s)" : ''
|
104
108
|
message = "✗ Out of #{total_tests} tests: #{details_str}#{time_str}"
|
105
|
-
puts indent_text(Console.color(color, message),
|
109
|
+
puts indent_text(Console.color(color, message), 1)
|
106
110
|
else
|
107
111
|
message = "#{total_tests} tests passed"
|
108
112
|
color = :green
|
109
|
-
puts indent_text(Console.color(color, "✓ #{message}"),
|
113
|
+
puts indent_text(Console.color(color, "✓ #{message}"), 1)
|
110
114
|
end
|
111
115
|
|
112
116
|
return unless elapsed_time
|
113
117
|
|
114
118
|
time_msg = "Completed in #{format_timing(elapsed_time).strip.tr('()', '')}"
|
115
|
-
puts indent_text(Console.color(:dim, time_msg),
|
119
|
+
puts indent_text(Console.color(:dim, time_msg), 1)
|
116
120
|
end
|
117
121
|
|
118
122
|
# Test-level operations
|
119
123
|
def test_start(test_case:, index:, total:)
|
124
|
+
return unless @show_passed
|
125
|
+
|
120
126
|
desc = test_case.description.to_s
|
121
127
|
desc = 'Unnamed test' if desc.empty?
|
122
128
|
message = "Test #{index}/#{total}: #{desc}"
|
123
|
-
puts indent_text(Console.color(:dim, message),
|
129
|
+
puts indent_text(Console.color(:dim, message), 1)
|
124
130
|
end
|
125
131
|
|
126
132
|
def test_result(result_packet)
|
@@ -143,7 +149,7 @@ class Tryouts
|
|
143
149
|
test_case = result_packet.test_case
|
144
150
|
location = "#{Console.pretty_path(test_case.path)}:#{test_case.first_expectation_line + 1}"
|
145
151
|
puts
|
146
|
-
puts indent_text("#{status_line} @ #{location}",
|
152
|
+
puts indent_text("#{status_line} @ #{location}", 1)
|
147
153
|
|
148
154
|
# Show source code for verbose mode
|
149
155
|
show_test_source_code(test_case)
|
@@ -160,21 +166,22 @@ class Tryouts
|
|
160
166
|
def test_output(test_case:, output_text:, result_packet:)
|
161
167
|
return if output_text.nil? || output_text.strip.empty?
|
162
168
|
|
163
|
-
puts indent_text('Test Output:',
|
164
|
-
puts indent_text(Console.color(:dim, '--- BEGIN OUTPUT ---'),
|
169
|
+
puts indent_text('Test Output:', 2)
|
170
|
+
puts indent_text(Console.color(:dim, '--- BEGIN OUTPUT ---'), 2)
|
165
171
|
|
166
172
|
output_text.lines.each do |line|
|
167
|
-
puts indent_text(line.chomp,
|
173
|
+
puts indent_text(line.chomp, 3)
|
168
174
|
end
|
169
175
|
|
170
|
-
puts indent_text(Console.color(:dim, '--- END OUTPUT ---'),
|
176
|
+
puts indent_text(Console.color(:dim, '--- END OUTPUT ---'), 2)
|
171
177
|
puts
|
172
178
|
end
|
173
179
|
|
174
180
|
# Setup/teardown operations
|
175
181
|
def setup_start(line_range:)
|
176
182
|
message = "Executing global setup (lines #{line_range.first}..#{line_range.last})"
|
177
|
-
puts indent_text(Console.color(:cyan, message),
|
183
|
+
puts indent_text(Console.color(:cyan, message), 1)
|
184
|
+
puts
|
178
185
|
end
|
179
186
|
|
180
187
|
def setup_output(output_text)
|
@@ -187,8 +194,7 @@ class Tryouts
|
|
187
194
|
|
188
195
|
def teardown_start(line_range:)
|
189
196
|
message = "Executing teardown (lines #{line_range.first}..#{line_range.last})"
|
190
|
-
puts indent_text(Console.color(:cyan, message),
|
191
|
-
puts
|
197
|
+
puts indent_text(Console.color(:cyan, message), 1)
|
192
198
|
end
|
193
199
|
|
194
200
|
def teardown_output(output_text)
|
@@ -201,8 +207,7 @@ class Tryouts
|
|
201
207
|
|
202
208
|
def grand_total(total_tests:, failed_count:, error_count:, successful_files:, total_files:, elapsed_time:)
|
203
209
|
puts
|
204
|
-
puts '
|
205
|
-
puts 'Grand Total:'
|
210
|
+
puts '=== TOTAL ==='
|
206
211
|
|
207
212
|
issues_count = failed_count + error_count
|
208
213
|
time_str = if elapsed_time < 2.0
|
@@ -216,13 +221,12 @@ class Tryouts
|
|
216
221
|
details = []
|
217
222
|
details << "#{failed_count} failed" if failed_count > 0
|
218
223
|
details << "#{error_count} errors" if error_count > 0
|
219
|
-
|
224
|
+
printf("%-10s %s\n", "Testcases:", "#{details.join(', ')}, #{passed} passed#{time_str}")
|
220
225
|
else
|
221
|
-
|
226
|
+
printf("%-10s %s\n", "Testcases:", "#{total_tests} tests passed#{time_str}")
|
222
227
|
end
|
223
228
|
|
224
|
-
|
225
|
-
puts '=' * @line_width
|
229
|
+
printf("%-10s %s\n", "Files:", "#{successful_files} of #{total_files} passed")
|
226
230
|
end
|
227
231
|
|
228
232
|
# Debug and diagnostic output
|
@@ -243,16 +247,16 @@ class Tryouts
|
|
243
247
|
|
244
248
|
def error_message(message, backtrace: nil)
|
245
249
|
error_msg = Console.color(:red, "ERROR: #{message}")
|
246
|
-
puts indent_text(error_msg,
|
250
|
+
puts indent_text(error_msg, 0)
|
247
251
|
|
248
252
|
return unless backtrace && @show_stack_traces
|
249
253
|
|
250
|
-
puts indent_text('Details:',
|
254
|
+
puts indent_text('Details:', 1)
|
251
255
|
# Show first 10 lines of backtrace to avoid overwhelming output
|
252
256
|
Console.pretty_backtrace(backtrace, limit: 10).each do |line|
|
253
|
-
puts indent_text(line,
|
257
|
+
puts indent_text(line, 2)
|
254
258
|
end
|
255
|
-
puts indent_text("... (#{backtrace.length - 10} more lines)",
|
259
|
+
puts indent_text("... (#{backtrace.length - 10} more lines)", 2) if backtrace.length > 10
|
256
260
|
end
|
257
261
|
|
258
262
|
def live_status_capabilities
|
@@ -272,16 +276,16 @@ class Tryouts
|
|
272
276
|
def show_exception_details(test_case, actual_results, expected_results = [])
|
273
277
|
return if actual_results.empty?
|
274
278
|
|
275
|
-
puts indent_text('Exception Details:',
|
279
|
+
puts indent_text('Exception Details:', 2)
|
276
280
|
|
277
281
|
actual_results.each_with_index do |actual, idx|
|
278
282
|
expected = expected_results[idx] if expected_results && idx < expected_results.length
|
279
283
|
expectation = test_case.expectations[idx] if test_case.expectations
|
280
284
|
|
281
285
|
if expectation&.type == :exception
|
282
|
-
puts indent_text("Caught: #{Console.color(:blue, actual.inspect)}",
|
283
|
-
puts indent_text("Expectation: #{Console.color(:green, expectation.content)}",
|
284
|
-
puts indent_text("Result: #{Console.color(:green, expected.inspect)}",
|
286
|
+
puts indent_text("Caught: #{Console.color(:blue, actual.inspect)}", 3)
|
287
|
+
puts indent_text("Expectation: #{Console.color(:green, expectation.content)}", 3)
|
288
|
+
puts indent_text("Result: #{Console.color(:green, expected.inspect)}", 3) if expected
|
285
289
|
end
|
286
290
|
end
|
287
291
|
puts
|
@@ -300,7 +304,7 @@ class Tryouts
|
|
300
304
|
line_display = Console.color(:yellow, line_display)
|
301
305
|
end
|
302
306
|
|
303
|
-
puts indent_text(line_display,
|
307
|
+
puts indent_text(line_display, 2)
|
304
308
|
end
|
305
309
|
puts
|
306
310
|
end
|
@@ -314,15 +318,15 @@ class Tryouts
|
|
314
318
|
|
315
319
|
if !expected.nil?
|
316
320
|
# Use the evaluated expected value from the evaluator
|
317
|
-
puts indent_text("Expected: #{Console.color(:green, expected.inspect)}",
|
318
|
-
puts indent_text("Actual: #{Console.color(:red, actual.inspect)}",
|
321
|
+
puts indent_text("Expected: #{Console.color(:green, expected.inspect)}", 2)
|
322
|
+
puts indent_text("Actual: #{Console.color(:red, actual.inspect)}", 2)
|
319
323
|
elsif expected_line && !expected_results.empty?
|
320
324
|
# Only show raw expectation content if we have expected_results (non-error case)
|
321
|
-
puts indent_text("Expected: #{Console.color(:green, expected_line.content)}",
|
322
|
-
puts indent_text("Actual: #{Console.color(:red, actual.inspect)}",
|
325
|
+
puts indent_text("Expected: #{Console.color(:green, expected_line.content)}", 2)
|
326
|
+
puts indent_text("Actual: #{Console.color(:red, actual.inspect)}", 2)
|
323
327
|
else
|
324
328
|
# For error cases (empty expected_results), just show the error
|
325
|
-
puts indent_text("Error: #{Console.color(:red, actual.inspect)}",
|
329
|
+
puts indent_text("Error: #{Console.color(:red, actual.inspect)}", 2)
|
326
330
|
end
|
327
331
|
|
328
332
|
# Show difference if both are strings
|
@@ -337,21 +341,16 @@ class Tryouts
|
|
337
341
|
def show_string_diff(expected, actual)
|
338
342
|
return if expected == actual
|
339
343
|
|
340
|
-
puts indent_text('Difference:',
|
341
|
-
puts indent_text("- #{Console.color(:red, actual)}",
|
342
|
-
puts indent_text("+ #{Console.color(:green, expected)}",
|
344
|
+
puts indent_text('Difference:', 2)
|
345
|
+
puts indent_text("- #{Console.color(:red, actual)}", 3)
|
346
|
+
puts indent_text("+ #{Console.color(:green, expected)}", 3)
|
343
347
|
end
|
344
348
|
|
345
349
|
def file_header_visual(file_path)
|
346
350
|
pretty_path = Console.pretty_path(file_path)
|
347
|
-
header_content = ">>>>> #{pretty_path} "
|
348
|
-
padding_length = [@line_width - header_content.length, 0].max
|
349
|
-
padding = '<' * padding_length
|
350
351
|
|
351
352
|
[
|
352
|
-
indent_text(
|
353
|
-
indent_text(header_content + padding, 1),
|
354
|
-
indent_text('-' * @line_width, 1),
|
353
|
+
indent_text("--- #{pretty_path} ---", 0)
|
355
354
|
].join("\n")
|
356
355
|
end
|
357
356
|
end
|
@@ -376,6 +375,20 @@ class Tryouts
|
|
376
375
|
super
|
377
376
|
end
|
378
377
|
|
378
|
+
# Suppress setup/teardown output in fails-only mode
|
379
|
+
def setup_start(line_range:)
|
380
|
+
# No output in fails mode
|
381
|
+
end
|
382
|
+
|
383
|
+
def teardown_start(line_range:)
|
384
|
+
# No output in fails mode
|
385
|
+
end
|
386
|
+
|
387
|
+
# Suppress file result summaries in fails-only mode
|
388
|
+
def file_result(_file_path, total_tests:, failed_count:, error_count:, elapsed_time: nil)
|
389
|
+
# No output in fails mode - let the batch_summary handle failures
|
390
|
+
end
|
391
|
+
|
379
392
|
def live_status_capabilities
|
380
393
|
{
|
381
394
|
supports_coordination: true, # Verbose can work with coordinated output
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# lib/tryouts/cli/line_spec_parser.rb
|
2
|
+
|
3
|
+
class Tryouts
|
4
|
+
class CLI
|
5
|
+
class LineSpecParser
|
6
|
+
# Parse a file path with optional line specification
|
7
|
+
# Supports formats:
|
8
|
+
# - file.rb:19 (single line)
|
9
|
+
# - file.rb:19-45 (range)
|
10
|
+
# - file.rb:L19 (GitHub-style single line)
|
11
|
+
# - file.rb:L19-45 (GitHub-style range)
|
12
|
+
# - file.rb:L19-L45 (GitHub-style range with L on both)
|
13
|
+
#
|
14
|
+
# Returns [filepath, line_spec] where line_spec is nil or a Range/Integer
|
15
|
+
def self.parse(path_with_spec)
|
16
|
+
return [path_with_spec, nil] unless path_with_spec.include?(':')
|
17
|
+
|
18
|
+
# Split on the last colon to handle paths with colons
|
19
|
+
parts = path_with_spec.rpartition(':')
|
20
|
+
filepath = parts[0]
|
21
|
+
line_spec_str = parts[2]
|
22
|
+
|
23
|
+
# If the filepath is empty, it means there was no colon or the input started with colon
|
24
|
+
return [path_with_spec, nil] if filepath.empty?
|
25
|
+
|
26
|
+
# If the "line spec" part looks like a Windows drive letter, this isn't a line spec
|
27
|
+
return [path_with_spec, nil] if line_spec_str =~ /\A[a-zA-Z]\z/
|
28
|
+
|
29
|
+
# Parse the line specification
|
30
|
+
line_spec = parse_line_spec(line_spec_str)
|
31
|
+
|
32
|
+
# If we couldn't parse it, treat the whole thing as a filepath
|
33
|
+
return [path_with_spec, nil] if line_spec.nil?
|
34
|
+
|
35
|
+
[filepath, line_spec]
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def self.parse_line_spec(spec)
|
41
|
+
return nil if spec.nil? || spec.empty?
|
42
|
+
|
43
|
+
# Remove 'L' prefix if present (GitHub style)
|
44
|
+
spec = spec.gsub(/L/i, '')
|
45
|
+
|
46
|
+
# Handle range (e.g., "19-80")
|
47
|
+
if spec.include?('-')
|
48
|
+
parts = spec.split('-', 2)
|
49
|
+
|
50
|
+
# Validate that both parts are numeric
|
51
|
+
return nil unless parts[0] =~ /\A\d+\z/ && parts[1] =~ /\A\d+\z/
|
52
|
+
|
53
|
+
start_line = parts[0].to_i
|
54
|
+
end_line = parts[1].to_i
|
55
|
+
|
56
|
+
# Validate the numbers
|
57
|
+
return nil if start_line <= 0 || end_line <= 0
|
58
|
+
return nil if start_line > end_line
|
59
|
+
|
60
|
+
start_line..end_line
|
61
|
+
else
|
62
|
+
# Single line number - validate it's numeric
|
63
|
+
return nil unless spec =~ /\A\d+\z/
|
64
|
+
|
65
|
+
line = spec.to_i
|
66
|
+
return nil if line <= 0
|
67
|
+
|
68
|
+
line
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Check if a test case at the given line range matches the line specification
|
73
|
+
# Test case line ranges are 0-based, user line specs are 1-based
|
74
|
+
def self.matches?(test_case, line_spec)
|
75
|
+
return true if line_spec.nil?
|
76
|
+
|
77
|
+
# Convert user's 1-based line spec to 0-based for comparison
|
78
|
+
zero_based_spec = case line_spec
|
79
|
+
when Integer
|
80
|
+
line_spec - 1
|
81
|
+
when Range
|
82
|
+
(line_spec.begin - 1)..(line_spec.end - 1)
|
83
|
+
else
|
84
|
+
line_spec
|
85
|
+
end
|
86
|
+
|
87
|
+
# Test case line_range is already 0-based
|
88
|
+
test_range = test_case.line_range
|
89
|
+
|
90
|
+
case zero_based_spec
|
91
|
+
when Integer
|
92
|
+
# Single line - check if it falls within the test's range
|
93
|
+
test_range.cover?(zero_based_spec)
|
94
|
+
when Range
|
95
|
+
# Range - check if there's any overlap
|
96
|
+
spec_start = zero_based_spec.begin
|
97
|
+
spec_end = zero_based_spec.end
|
98
|
+
test_start = test_range.begin
|
99
|
+
test_end = test_range.end
|
100
|
+
|
101
|
+
# Check for overlap: ranges overlap if start of one is before end of other
|
102
|
+
!(test_end < spec_start || spec_end < test_start)
|
103
|
+
else
|
104
|
+
true
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
data/lib/tryouts/cli/opts.rb
CHANGED
@@ -123,6 +123,12 @@ class Tryouts
|
|
123
123
|
options[:parallel_threads] = threads.to_i if threads && threads.to_i > 0
|
124
124
|
end
|
125
125
|
|
126
|
+
opts.separator "\nParser Options:"
|
127
|
+
opts.on('--strict', 'Require explicit test descriptions (fail on unnamed tests)') { options[:strict] = true }
|
128
|
+
opts.on('--no-strict', 'Allow unnamed tests (legacy behavior)') { options[:strict] = false }
|
129
|
+
opts.on('-w', '--warnings', 'Show parser warnings (default: true)') { options[:warnings] = true }
|
130
|
+
opts.on('--no-warnings', 'Suppress parser warnings') { options[:warnings] = false }
|
131
|
+
|
126
132
|
opts.separator "\nAgent-Optimized Output:"
|
127
133
|
opts.on('-a', '--agent', 'Agent-optimized structured output for LLM context management') do
|
128
134
|
options[:agent] = true
|
data/lib/tryouts/cli.rb
CHANGED
@@ -4,6 +4,7 @@ require 'optparse'
|
|
4
4
|
|
5
5
|
require_relative 'cli/opts'
|
6
6
|
require_relative 'cli/formatters'
|
7
|
+
require_relative 'cli/line_spec_parser'
|
7
8
|
require_relative 'test_runner'
|
8
9
|
|
9
10
|
class Tryouts
|
@@ -13,6 +14,8 @@ class Tryouts
|
|
13
14
|
framework: :direct,
|
14
15
|
verbose: false,
|
15
16
|
inspect: false,
|
17
|
+
strict: true, # Default to strict mode for better UX
|
18
|
+
warnings: true, # Default to showing warnings
|
16
19
|
}
|
17
20
|
end
|
18
21
|
|
@@ -22,11 +25,14 @@ class Tryouts
|
|
22
25
|
output_manager = FormatterFactory.create_output_manager(@options)
|
23
26
|
|
24
27
|
handle_version_flag(@options, output_manager)
|
25
|
-
|
28
|
+
|
29
|
+
# Parse line specs from file arguments
|
30
|
+
files_with_specs = parse_file_specs(files)
|
31
|
+
validate_files_exist(files_with_specs, output_manager)
|
26
32
|
|
27
33
|
runner = TestRunner.new(
|
28
|
-
files:
|
29
|
-
options: @options,
|
34
|
+
files: files_with_specs.keys,
|
35
|
+
options: @options.merge(file_line_specs: files_with_specs),
|
30
36
|
output_manager: output_manager,
|
31
37
|
)
|
32
38
|
|
@@ -42,13 +48,24 @@ class Tryouts
|
|
42
48
|
exit 0
|
43
49
|
end
|
44
50
|
|
45
|
-
def validate_files_exist(
|
46
|
-
missing_files =
|
51
|
+
def validate_files_exist(files_with_specs, output_manager)
|
52
|
+
missing_files = files_with_specs.keys.reject { |file| File.exist?(file) }
|
47
53
|
|
48
54
|
unless missing_files.empty?
|
49
55
|
missing_files.each { |file| output_manager.error("File not found: #{file}") }
|
50
56
|
exit 1
|
51
57
|
end
|
52
58
|
end
|
59
|
+
|
60
|
+
def parse_file_specs(files)
|
61
|
+
files_with_specs = {}
|
62
|
+
|
63
|
+
files.each do |file_arg|
|
64
|
+
filepath, line_spec = LineSpecParser.parse(file_arg)
|
65
|
+
files_with_specs[filepath] = line_spec
|
66
|
+
end
|
67
|
+
|
68
|
+
files_with_specs
|
69
|
+
end
|
53
70
|
end
|
54
71
|
end
|
@@ -40,6 +40,21 @@ class Tryouts
|
|
40
40
|
# Try to evaluate in test context first, then fallback to global context for constants
|
41
41
|
begin
|
42
42
|
expected_class = eval_expectation_content(@expectation.content, expectation_result)
|
43
|
+
|
44
|
+
# If the evaluated result is not a class/module (e.g., shadowed constant),
|
45
|
+
# fall back to global context resolution
|
46
|
+
unless expected_class.is_a?(Class) || expected_class.is_a?(Module)
|
47
|
+
# Attempt to get the constant from global context
|
48
|
+
# This handles cases where a local variable/method shadows a class constant
|
49
|
+
constant_name = @expectation.content.strip
|
50
|
+
# Ensure the constant name is valid before attempting const_get
|
51
|
+
if constant_name =~ /\A[A-Z][\w:]*\z/
|
52
|
+
expected_class = Object.const_get(constant_name)
|
53
|
+
else
|
54
|
+
# If it's not a valid constant name, the evaluation failed
|
55
|
+
raise TypeError, "Expected a Class or Module, got #{expected_class.class}"
|
56
|
+
end
|
57
|
+
end
|
43
58
|
rescue NameError => e
|
44
59
|
# If we can't find the constant in test context, try global context
|
45
60
|
# This is common for exception classes like ArgumentError, StandardError, etc.
|
@@ -20,8 +20,15 @@ class Tryouts
|
|
20
20
|
|
21
21
|
def process
|
22
22
|
testrun = create_parser(@file, @options).parse
|
23
|
+
|
24
|
+
# Apply line spec filtering before reporting test counts
|
25
|
+
if @options[:line_spec]
|
26
|
+
testrun = filter_testrun_by_line_spec(testrun)
|
27
|
+
end
|
28
|
+
|
23
29
|
@global_tally[:aggregator].increment_total_files
|
24
30
|
@output_manager.file_parsed(@file, testrun.total_tests)
|
31
|
+
@output_manager.parser_warnings(@file, warnings: testrun.warnings)
|
25
32
|
|
26
33
|
if @options[:inspect]
|
27
34
|
handle_inspect_mode(testrun)
|
@@ -38,6 +45,34 @@ class Tryouts
|
|
38
45
|
|
39
46
|
private
|
40
47
|
|
48
|
+
def filter_testrun_by_line_spec(testrun)
|
49
|
+
require_relative 'cli/line_spec_parser'
|
50
|
+
|
51
|
+
line_spec = @options[:line_spec]
|
52
|
+
|
53
|
+
# Filter test cases to only those that match the line spec
|
54
|
+
filtered_cases = testrun.test_cases.select do |test_case|
|
55
|
+
Tryouts::CLI::LineSpecParser.matches?(test_case, line_spec)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Check if any tests matched the line specification
|
59
|
+
if filtered_cases.empty?
|
60
|
+
@output_manager.file_failure(@file, "No test cases found matching line specification: #{line_spec}")
|
61
|
+
return testrun # Return original testrun to avoid breaking the pipeline
|
62
|
+
end
|
63
|
+
|
64
|
+
# Create a new testrun with filtered cases
|
65
|
+
# We need to preserve the setup and teardown but only include matching tests
|
66
|
+
testrun.class.new(
|
67
|
+
setup: testrun.setup,
|
68
|
+
test_cases: filtered_cases,
|
69
|
+
teardown: testrun.teardown,
|
70
|
+
source_file: testrun.source_file,
|
71
|
+
metadata: testrun.metadata,
|
72
|
+
warnings: testrun.warnings
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
41
76
|
def create_parser(file, options)
|
42
77
|
parser_type = options[:parser] || :enhanced # enhanced parser is now the default
|
43
78
|
|
@@ -47,9 +82,9 @@ class Tryouts
|
|
47
82
|
|
48
83
|
case parser_type
|
49
84
|
when :enhanced
|
50
|
-
EnhancedParser.new(file)
|
85
|
+
EnhancedParser.new(file, options)
|
51
86
|
when :prism
|
52
|
-
PrismParser.new(file)
|
87
|
+
PrismParser.new(file, options)
|
53
88
|
end
|
54
89
|
end
|
55
90
|
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# lib/tryouts/parser_warning.rb
|
2
|
+
|
3
|
+
class Tryouts
|
4
|
+
# Data structure for parser warnings
|
5
|
+
ParserWarning = Data.define(:type, :message, :line_number, :context, :suggestion) do
|
6
|
+
def self.unnamed_test(line_number:, context:)
|
7
|
+
new(
|
8
|
+
type: :unnamed_test,
|
9
|
+
message: "Test case without explicit description",
|
10
|
+
line_number: line_number,
|
11
|
+
context: context,
|
12
|
+
suggestion: "Add a test description using '## Description' prefix"
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.ambiguous_test_boundary(line_number:, context:)
|
17
|
+
new(
|
18
|
+
type: :ambiguous_boundary,
|
19
|
+
message: "Ambiguous test case boundary detected",
|
20
|
+
line_number: line_number,
|
21
|
+
context: context,
|
22
|
+
suggestion: "Use explicit '## Description' to clarify test structure"
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'prism'
|
4
4
|
|
5
5
|
require_relative 'shared_methods'
|
6
|
+
require_relative '../parser_warning'
|
6
7
|
|
7
8
|
class Tryouts
|
8
9
|
# Fixed PrismParser with pattern matching for robust token filtering
|
@@ -10,12 +11,14 @@ class Tryouts
|
|
10
11
|
class BaseParser
|
11
12
|
include Tryouts::Parsers::SharedMethods
|
12
13
|
|
13
|
-
def initialize(source_path)
|
14
|
+
def initialize(source_path, options = {})
|
14
15
|
@source_path = source_path
|
15
16
|
@source = File.read(source_path)
|
16
17
|
@lines = @source.lines.map(&:chomp)
|
17
18
|
@prism_result = Prism.parse(@source)
|
18
19
|
@parsed_at = Time.now
|
20
|
+
@options = options
|
21
|
+
@warnings = []
|
19
22
|
end
|
20
23
|
|
21
24
|
end
|