tryouts 3.4.0 → 3.5.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 +18 -3
- data/exe/try +8 -5
- data/lib/tryouts/cli/formatters/agent.rb +176 -50
- 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/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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7b692cc3a63ac86e060b52e77c246305be0f209dbf06b31b1b08deefbc06434a
|
4
|
+
data.tar.gz: 156e86aa4bde377aa2158f3a059983a6fe5f58796f71545c9895a9b57668b126
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d289a9b5ccd6694bd63fe4ca2a0c8aaf610d702e74380bf4975ae78ffaf377d0f82a9ec481dbc51105de9f3b07681c32eaa8a1c3566a1eebd0a861796eaff1e3
|
7
|
+
data.tar.gz: a0db3ebf76b5a291e6ea9d81f1f6e268a95320d28040a10f2225171b221f04bb2ed525bae3f79785d50f4269eb7c9be1dec242012bbca0a6d4588ac9e7fdd362
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Tryouts
|
1
|
+
# Tryouts - A Ruby Testing Framework
|
2
2
|
|
3
3
|
**Ruby tests that read like documentation.**
|
4
4
|
|
@@ -130,6 +130,21 @@ try --agent --agent-focus critical # show only errors/exceptions
|
|
130
130
|
try --agent --agent-limit 1000 # limit output to 1000 tokens
|
131
131
|
```
|
132
132
|
|
133
|
+
#### Why Not Pipe Test Output Directly to AI?
|
134
|
+
|
135
|
+
Raw test output creates several problems when working with AI assistants:
|
136
|
+
|
137
|
+
- **Token bloat**: Verbose formatting wastes 60-80% of your context window on styling
|
138
|
+
- **Signal vs noise**: Important failures get buried in passing test details and framework boilerplate
|
139
|
+
- **Inconsistent parsing**: AI struggles with varying output formats across different test runs
|
140
|
+
- **Context overflow**: Large test suites exceed AI token limits, truncating critical information
|
141
|
+
|
142
|
+
#### TOPA: A Better Approach
|
143
|
+
|
144
|
+
Tryouts' `--agent` mode inspired the development of **TOPA (Test Output Protocol for AI)** - a standardized format optimized for AI analysis. The [tpane](https://github.com/delano/tpane) tool implements this protocol, transforming any test framework's output into structured, token-efficient formats.
|
145
|
+
|
146
|
+
Instead of overwhelming AI with raw output, TOPA provides clean semantic data focusing on what actually needs attention - failures, errors, and actionable context.
|
147
|
+
|
133
148
|
### Exit Codes
|
134
149
|
|
135
150
|
- `0`: All tests pass
|
@@ -162,8 +177,8 @@ For real-world usage examples, see:
|
|
162
177
|
|
163
178
|
This version of Tryouts was developed with assistance from AI tools. The following tools provided significant help with architecture design, code generation, and documentation:
|
164
179
|
|
165
|
-
- **Claude Sonnet 4** - Architecture design, code generation, and documentation
|
166
|
-
- **Claude Desktop & Claude Code** - Interactive development sessions and debugging
|
180
|
+
- **Claude Sonnet 4, Opus 4.1** - Architecture design, code generation, and documentation
|
181
|
+
- **Claude Desktop & Claude Code (Max plan)** - Interactive development sessions and debugging
|
167
182
|
- **GitHub Copilot** - Code completion and refactoring assistance
|
168
183
|
- **Qodo Merge Pro** - Code review and quality improvements
|
169
184
|
|
data/exe/try
CHANGED
@@ -42,15 +42,18 @@ Tryouts.update_load_path(lib_glob) if Tryouts.respond_to?(:update_load_path)
|
|
42
42
|
begin
|
43
43
|
files, options = Tryouts::CLI.parse_args(ARGV)
|
44
44
|
|
45
|
-
# Expand files if directories are given
|
45
|
+
# Expand files if directories are given, preserving line specs
|
46
46
|
expanded_files = []
|
47
47
|
files.each do |file_or_dir|
|
48
|
-
|
48
|
+
# Parse line spec from the argument
|
49
|
+
path_part, line_spec = Tryouts::CLI::LineSpecParser.parse(file_or_dir)
|
50
|
+
|
51
|
+
if File.directory?(path_part)
|
49
52
|
# If it's a directory, find all *_try.rb and *.try.rb files within it
|
50
|
-
dir_files = Dir.glob(['**/*_try.rb', '**/*.try.rb'], base:
|
51
|
-
expanded_files.concat(dir_files.map { |f| File.join(
|
53
|
+
dir_files = Dir.glob(['**/*_try.rb', '**/*.try.rb'], base: path_part)
|
54
|
+
expanded_files.concat(dir_files.map { |f| File.join(path_part, f) })
|
52
55
|
else
|
53
|
-
# If it's a file, add it as-is
|
56
|
+
# If it's a file, add it as-is (with line spec if present)
|
54
57
|
expanded_files << file_or_dir
|
55
58
|
end
|
56
59
|
end
|
@@ -22,6 +22,9 @@ class Tryouts
|
|
22
22
|
@current_file_data = nil
|
23
23
|
@total_stats = { files: 0, tests: 0, failures: 0, errors: 0, elapsed: 0 }
|
24
24
|
@output_rendered = false
|
25
|
+
@options = options # Store all options for execution context display
|
26
|
+
@all_warnings = [] # Store warnings globally for execution details
|
27
|
+
@syntax_errors = [] # Store syntax errors for execution details
|
25
28
|
|
26
29
|
# No colors in agent mode for cleaner parsing
|
27
30
|
@use_colors = false
|
@@ -42,7 +45,8 @@ class Tryouts
|
|
42
45
|
tests: 0,
|
43
46
|
failures: [],
|
44
47
|
errors: [],
|
45
|
-
passed: 0
|
48
|
+
passed: 0,
|
49
|
+
context_info: context_info # Store context info for later display
|
46
50
|
}
|
47
51
|
end
|
48
52
|
|
@@ -56,19 +60,47 @@ class Tryouts
|
|
56
60
|
end
|
57
61
|
|
58
62
|
def file_parsed(_file_path, test_count:, setup_present: false, teardown_present: false)
|
59
|
-
|
63
|
+
if @current_file_data
|
64
|
+
@current_file_data[:tests] = test_count
|
65
|
+
end
|
60
66
|
@total_stats[:tests] += test_count
|
61
67
|
end
|
62
68
|
|
63
|
-
def
|
69
|
+
def parser_warnings(file_path, warnings:)
|
70
|
+
return if warnings.empty? || !@options.fetch(:warnings, true)
|
71
|
+
|
72
|
+
# Store warnings globally for execution details and per-file
|
73
|
+
warnings.each do |warning|
|
74
|
+
warning_data = {
|
75
|
+
type: warning.type.to_s,
|
76
|
+
message: warning.message,
|
77
|
+
line: warning.line_number,
|
78
|
+
suggestion: warning.suggestion,
|
79
|
+
file: relative_path(file_path)
|
80
|
+
}
|
81
|
+
@all_warnings << warning_data
|
82
|
+
end
|
83
|
+
|
84
|
+
# Also store in current file data for potential future use
|
85
|
+
if @current_file_data
|
86
|
+
@current_file_data[:warnings] = @all_warnings.select { |w| w[:file] == relative_path(file_path) }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def file_result(file_path, total_tests:, failed_count:, error_count:, elapsed_time: nil)
|
64
91
|
# Always update global totals
|
65
92
|
@total_stats[:failures] += failed_count
|
66
93
|
@total_stats[:errors] += error_count
|
67
94
|
@total_stats[:elapsed] += elapsed_time if elapsed_time
|
68
95
|
|
69
|
-
# Update per-file data
|
70
|
-
|
71
|
-
|
96
|
+
# Update per-file data - file_result is called AFTER file_end, so data is in @collected_files
|
97
|
+
relative_file_path = relative_path(file_path)
|
98
|
+
file_data = @collected_files.find { |f| f[:path] == relative_file_path }
|
99
|
+
|
100
|
+
if file_data
|
101
|
+
file_data[:passed] = total_tests - failed_count - error_count
|
102
|
+
# Also ensure tests count is correct if it wasn't set properly earlier
|
103
|
+
file_data[:tests] ||= total_tests
|
72
104
|
end
|
73
105
|
end
|
74
106
|
|
@@ -135,6 +167,14 @@ class Tryouts
|
|
135
167
|
@output_rendered = true
|
136
168
|
end
|
137
169
|
|
170
|
+
def error_message(message, backtrace: nil)
|
171
|
+
# Store syntax errors for display in execution details
|
172
|
+
@syntax_errors << {
|
173
|
+
message: message,
|
174
|
+
backtrace: backtrace
|
175
|
+
}
|
176
|
+
end
|
177
|
+
|
138
178
|
# Override live status - not needed for agent mode
|
139
179
|
def live_status_capabilities
|
140
180
|
{
|
@@ -209,50 +249,71 @@ class Tryouts
|
|
209
249
|
def render_summary_only
|
210
250
|
output = []
|
211
251
|
|
252
|
+
# Add execution context header for agent clarity
|
253
|
+
output << render_execution_context
|
254
|
+
output << ""
|
255
|
+
|
212
256
|
# Count failures manually from collected file data (same as other render methods)
|
213
257
|
failed_count = @collected_files.sum { |f| f[:failures].size }
|
214
258
|
error_count = @collected_files.sum { |f| f[:errors].size }
|
215
259
|
issues_count = failed_count + error_count
|
216
260
|
passed_count = [@total_stats[:tests] - issues_count, 0].max
|
217
261
|
|
262
|
+
status_parts = []
|
218
263
|
if issues_count > 0
|
219
|
-
status = "FAIL: #{issues_count}/#{@total_stats[:tests]} tests"
|
220
264
|
details = []
|
221
265
|
details << "#{failed_count} failed" if failed_count > 0
|
222
266
|
details << "#{error_count} errors" if error_count > 0
|
223
|
-
|
267
|
+
status_parts << "FAIL: #{issues_count}/#{@total_stats[:tests]} tests (#{details.join(', ')}, #{passed_count} passed)"
|
224
268
|
else
|
225
|
-
|
269
|
+
# Agent doesn't need output in the positive case (i.e. for passing
|
270
|
+
# tests). It just fills out the context window.
|
226
271
|
end
|
227
272
|
|
228
|
-
|
273
|
+
status_parts << "(#{format_time(@total_stats[:elapsed])})" if @total_stats[:elapsed]
|
229
274
|
|
230
|
-
output <<
|
275
|
+
output << status_parts.join(" ")
|
276
|
+
|
277
|
+
# Always show file information for agent context
|
278
|
+
output << ""
|
231
279
|
|
232
|
-
# Show which files had failures
|
233
280
|
files_with_issues = @collected_files.select { |f| f[:failures].any? || f[:errors].any? }
|
234
281
|
if files_with_issues.any?
|
235
|
-
output << ""
|
236
|
-
output << "Files with issues:"
|
282
|
+
output << "Files:"
|
237
283
|
files_with_issues.each do |file_data|
|
238
284
|
issue_count = file_data[:failures].size + file_data[:errors].size
|
239
285
|
output << " #{file_data[:path]}: #{issue_count} issue#{'s' if issue_count != 1}"
|
240
286
|
end
|
287
|
+
elsif @collected_files.any?
|
288
|
+
# Show files that were processed successfully
|
289
|
+
output << "Files:"
|
290
|
+
@collected_files.each do |file_data|
|
291
|
+
# Use the passed count from file_result if available, otherwise calculate
|
292
|
+
passed_tests = file_data[:passed] ||
|
293
|
+
((file_data[:tests] || 0) - file_data[:failures].size - file_data[:errors].size)
|
294
|
+
output << " #{file_data[:path]}: #{passed_tests} test#{'s' if passed_tests != 1} passed"
|
295
|
+
end
|
241
296
|
end
|
242
297
|
|
243
|
-
puts output.join("\n")
|
298
|
+
puts output.join("\n") if output.any?
|
244
299
|
end
|
245
300
|
|
246
301
|
def render_critical_only
|
247
302
|
# Only show errors (exceptions), skip assertion failures
|
248
303
|
critical_files = @collected_files.select { |f| f[:errors].any? }
|
249
304
|
|
305
|
+
output = []
|
306
|
+
|
307
|
+
# Add execution context header for agent clarity
|
308
|
+
output << render_execution_context
|
309
|
+
output << ""
|
310
|
+
|
250
311
|
if critical_files.empty?
|
251
|
-
|
312
|
+
output << "No critical errors found"
|
313
|
+
puts output.join("\n")
|
252
314
|
return
|
253
315
|
end
|
254
316
|
|
255
|
-
output = []
|
256
317
|
output << "CRITICAL: #{critical_files.size} file#{'s' if critical_files.size != 1} with errors"
|
257
318
|
output << ""
|
258
319
|
|
@@ -283,39 +344,21 @@ class Tryouts
|
|
283
344
|
def render_full_structured
|
284
345
|
output = []
|
285
346
|
|
286
|
-
#
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
files_count = if @total_stats[:files].to_i > 0
|
291
|
-
@total_stats[:files]
|
292
|
-
else
|
293
|
-
@total_stats[:total_files] || @collected_files.size
|
294
|
-
end
|
295
|
-
|
296
|
-
if issues_count > 0
|
297
|
-
status_line = "FAIL: #{issues_count}/#{@total_stats[:tests]} tests (#{files_count} files, #{format_time(@total_stats[:elapsed])})"
|
298
|
-
else
|
299
|
-
status_line = "PASS: #{@total_stats[:tests]} tests (#{files_count} files, #{format_time(@total_stats[:elapsed])})"
|
300
|
-
end
|
301
|
-
|
302
|
-
# Always include status line
|
303
|
-
output << status_line
|
304
|
-
@budget.force_consume(status_line)
|
347
|
+
# Add execution context header for agent clarity
|
348
|
+
output << render_execution_context
|
349
|
+
output << ""
|
305
350
|
|
306
|
-
#
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
@collected_files.select { |f| f[:failures].any? || f[:errors].any? }
|
312
|
-
end
|
351
|
+
# Count actual failures from collected data
|
352
|
+
failed_count = @collected_files.sum { |f| f[:failures].size }
|
353
|
+
error_count = @collected_files.sum { |f| f[:errors].size }
|
354
|
+
issues_count = failed_count + error_count
|
355
|
+
passed_count = [@total_stats[:tests] - issues_count, 0].max
|
313
356
|
|
314
|
-
|
315
|
-
|
316
|
-
@budget.consume("\n")
|
357
|
+
# Show files with issues only
|
358
|
+
files_with_issues = @collected_files.select { |f| f[:failures].any? || f[:errors].any? }
|
317
359
|
|
318
|
-
|
360
|
+
if files_with_issues.any?
|
361
|
+
files_with_issues.each do |file_data|
|
319
362
|
break unless @budget.has_budget?
|
320
363
|
|
321
364
|
file_section = render_file_section(file_data)
|
@@ -329,14 +372,15 @@ class Tryouts
|
|
329
372
|
@budget.consume(file_section)
|
330
373
|
end
|
331
374
|
end
|
375
|
+
output << ""
|
332
376
|
end
|
333
377
|
|
334
378
|
# Final summary line
|
335
|
-
summary = "Summary:
|
336
|
-
summary += ", #{
|
379
|
+
summary = "Summary: \n"
|
380
|
+
summary += "#{passed_count} testcases passed, #{failed_count} failed"
|
381
|
+
summary += ", #{error_count} errors" if error_count > 0
|
337
382
|
summary += " in #{@total_stats[:files]} files"
|
338
383
|
|
339
|
-
output << ""
|
340
384
|
output << summary
|
341
385
|
|
342
386
|
puts output.join("\n")
|
@@ -348,6 +392,20 @@ class Tryouts
|
|
348
392
|
# File header
|
349
393
|
lines << "#{file_data[:path]}:"
|
350
394
|
|
395
|
+
# Check if file has any issues
|
396
|
+
has_issues = file_data[:failures].any? || file_data[:errors].any?
|
397
|
+
|
398
|
+
# If no issues, show success summary
|
399
|
+
if !has_issues
|
400
|
+
# Use the passed count from file_result if available, otherwise calculate
|
401
|
+
passed_tests = file_data[:passed] ||
|
402
|
+
((file_data[:tests] || 0) - file_data[:failures].size - file_data[:errors].size)
|
403
|
+
|
404
|
+
|
405
|
+
lines << " ✓ #{passed_tests} test#{'s' if passed_tests != 1} passed"
|
406
|
+
return lines.join("\n")
|
407
|
+
end
|
408
|
+
|
351
409
|
# For first-failure mode, only show first error or failure
|
352
410
|
if @focus_mode == :first_failure || @focus_mode == :'first-failure'
|
353
411
|
shown_count = 0
|
@@ -445,6 +503,74 @@ class Tryouts
|
|
445
503
|
"#{seconds.round(2)}s"
|
446
504
|
end
|
447
505
|
end
|
506
|
+
|
507
|
+
def render_execution_context
|
508
|
+
context_lines = []
|
509
|
+
context_lines << "EXECUTION DETAILS:"
|
510
|
+
|
511
|
+
# Framework and context mode
|
512
|
+
framework = @options[:framework] || :direct
|
513
|
+
shared_context = if @options.key?(:shared_context)
|
514
|
+
@options[:shared_context]
|
515
|
+
else
|
516
|
+
# Apply framework defaults
|
517
|
+
case framework
|
518
|
+
when :rspec, :minitest
|
519
|
+
false
|
520
|
+
else
|
521
|
+
true # direct/tryouts defaults to shared
|
522
|
+
end
|
523
|
+
end
|
524
|
+
|
525
|
+
context_lines << " Framework: #{framework}"
|
526
|
+
context_lines << " Context mode: #{shared_context ? 'shared (variables persist across test cases)' : 'fresh (each test case isolated)'}"
|
527
|
+
|
528
|
+
# Parser type
|
529
|
+
parser = @options[:parser] || :enhanced
|
530
|
+
context_lines << " Parser: #{parser}"
|
531
|
+
|
532
|
+
# Other relevant flags
|
533
|
+
flags = []
|
534
|
+
flags << "verbose" if @options[:verbose]
|
535
|
+
flags << "fails-only" if @options[:fails_only]
|
536
|
+
flags << "debug" if @options[:debug]
|
537
|
+
flags << "stack-traces" if @options[:stack_traces]
|
538
|
+
flags << "parallel(#{@options[:parallel_threads] || 'auto'})" if @options[:parallel]
|
539
|
+
flags << "line-spec" if @options[:line_spec]
|
540
|
+
|
541
|
+
context_lines << " Flags: #{flags.any? ? flags.join(', ') : 'none'}" if flags.any?
|
542
|
+
|
543
|
+
# Agent-specific settings
|
544
|
+
context_lines << " Agent mode: focus=#{@focus_mode}, limit=#{@budget.limit} tokens"
|
545
|
+
|
546
|
+
# Add syntax errors if any (these prevent test execution)
|
547
|
+
if @syntax_errors.any?
|
548
|
+
context_lines << ""
|
549
|
+
context_lines << "Syntax Errors:"
|
550
|
+
@syntax_errors.each do |error|
|
551
|
+
# Clean up the error message to remove redundant prefixes
|
552
|
+
clean_message = error[:message].gsub(/^ERROR:\s*/i, '').strip
|
553
|
+
context_lines << " #{clean_message}"
|
554
|
+
if error[:backtrace] && @options[:debug]
|
555
|
+
error[:backtrace].first(3).each do |trace|
|
556
|
+
context_lines << " #{trace}"
|
557
|
+
end
|
558
|
+
end
|
559
|
+
end
|
560
|
+
end
|
561
|
+
|
562
|
+
# Add warnings if any
|
563
|
+
if @all_warnings.any? && @options.fetch(:warnings, true)
|
564
|
+
context_lines << ""
|
565
|
+
context_lines << "Parser Warnings:"
|
566
|
+
@all_warnings.each do |warning|
|
567
|
+
context_lines << " #{warning[:file]}:#{warning[:line]}: #{warning[:message]}"
|
568
|
+
context_lines << " #{warning[:suggestion]}" if warning[:suggestion]
|
569
|
+
end
|
570
|
+
end
|
571
|
+
|
572
|
+
context_lines.join("\n")
|
573
|
+
end
|
448
574
|
end
|
449
575
|
end
|
450
576
|
end
|
@@ -33,6 +33,10 @@ class Tryouts
|
|
33
33
|
# Default: no output
|
34
34
|
end
|
35
35
|
|
36
|
+
def parser_warnings(file_path, warnings:)
|
37
|
+
# Default: no output - override in specific formatters
|
38
|
+
end
|
39
|
+
|
36
40
|
def file_execution_start(file_path, test_count:, context_mode:)
|
37
41
|
# Default: no output
|
38
42
|
end
|
@@ -151,7 +155,7 @@ class Tryouts
|
|
151
155
|
end
|
152
156
|
|
153
157
|
def separator(style = :light)
|
154
|
-
width = @options.fetch(:line_width,
|
158
|
+
width = @options.fetch(:line_width, 60)
|
155
159
|
case style
|
156
160
|
when :heavy
|
157
161
|
'=' * width
|
@@ -44,9 +44,20 @@ class Tryouts
|
|
44
44
|
@stderr.puts indent_text("Parsed #{test_count} tests#{suffix}", 1)
|
45
45
|
end
|
46
46
|
|
47
|
+
def parser_warnings(file_path, warnings:)
|
48
|
+
return if warnings.empty? || !@options.fetch(:warnings, true)
|
49
|
+
|
50
|
+
@stderr.puts
|
51
|
+
@stderr.puts Console.color(:yellow, "Warnings:")
|
52
|
+
warnings.each do |warning|
|
53
|
+
@stderr.puts " #{Console.pretty_path(file_path)}:#{warning.line_number}: #{warning.message}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
47
57
|
def file_execution_start(file_path, test_count:, context_mode:)
|
48
58
|
pretty_path = Console.pretty_path(file_path)
|
49
|
-
|
59
|
+
puts
|
60
|
+
puts "#{pretty_path}: #{test_count} tests"
|
50
61
|
end
|
51
62
|
|
52
63
|
# Summary operations - show failure summary
|
@@ -204,7 +215,6 @@ class Tryouts
|
|
204
215
|
|
205
216
|
def grand_total(total_tests:, failed_count:, error_count:, successful_files:, total_files:, elapsed_time:)
|
206
217
|
@stderr.puts
|
207
|
-
@stderr.puts '=' * 50
|
208
218
|
|
209
219
|
issues_count = failed_count + error_count
|
210
220
|
if issues_count > 0
|
@@ -219,8 +229,8 @@ class Tryouts
|
|
219
229
|
|
220
230
|
time_str = format_timing(elapsed_time)
|
221
231
|
|
222
|
-
@stderr.puts "
|
223
|
-
@stderr.puts "
|
232
|
+
@stderr.puts "#{result}#{time_str}"
|
233
|
+
@stderr.puts "#{successful_files} of #{total_files} files passed"
|
224
234
|
end
|
225
235
|
|
226
236
|
# Debug and diagnostic output - minimal in compact mode
|
@@ -57,6 +57,10 @@ class Tryouts
|
|
57
57
|
)
|
58
58
|
end
|
59
59
|
|
60
|
+
def parser_warnings(file_path, warnings:)
|
61
|
+
@formatter.parser_warnings(file_path, warnings: warnings)
|
62
|
+
end
|
63
|
+
|
60
64
|
def file_execution_start(file_path, test_count, context_mode)
|
61
65
|
@formatter.file_execution_start(
|
62
66
|
file_path,
|
@@ -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
|
@@ -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
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# lib/tryouts/parsers/shared_methods.rb
|
2
2
|
|
3
|
+
require_relative '../parser_warning'
|
4
|
+
|
3
5
|
class Tryouts
|
4
6
|
module Parsers
|
5
7
|
module SharedMethods
|
@@ -205,13 +207,19 @@ class Tryouts
|
|
205
207
|
test_blocks = classified_blocks.filter { |block| block[:type] == :test }
|
206
208
|
teardown_blocks = classified_blocks.filter { |block| block[:type] == :teardown }
|
207
209
|
|
208
|
-
Testrun.new(
|
210
|
+
testrun = Testrun.new(
|
209
211
|
setup: build_setup(setup_blocks),
|
210
212
|
test_cases: test_blocks.map { |block| build_test_case(block) },
|
211
213
|
teardown: build_teardown(teardown_blocks),
|
212
214
|
source_file: @source_path,
|
213
215
|
metadata: { parsed_at: @parsed_at, parser: parser_type },
|
216
|
+
warnings: warnings
|
214
217
|
)
|
218
|
+
|
219
|
+
# Validate strict mode after collecting all warnings
|
220
|
+
validate_strict_mode(testrun)
|
221
|
+
|
222
|
+
testrun
|
215
223
|
end
|
216
224
|
|
217
225
|
def build_setup(setup_blocks)
|
@@ -354,6 +362,9 @@ class Tryouts
|
|
354
362
|
start_line: Integer => start_line,
|
355
363
|
end_line: Integer => end_line
|
356
364
|
}
|
365
|
+
# Collect warning for unnamed test
|
366
|
+
collect_unnamed_test_warning(block)
|
367
|
+
|
357
368
|
source_lines = @lines[start_line..end_line]
|
358
369
|
first_expectation_line = exp_tokens.empty? ? start_line : exp_tokens.first[:line]
|
359
370
|
|
@@ -411,6 +422,44 @@ class Tryouts
|
|
411
422
|
:shared
|
412
423
|
end
|
413
424
|
|
425
|
+
# Warning collection methods
|
426
|
+
def add_warning(warning)
|
427
|
+
@warnings ||= []
|
428
|
+
@warnings << warning
|
429
|
+
end
|
430
|
+
|
431
|
+
def warnings
|
432
|
+
@warnings ||= []
|
433
|
+
end
|
434
|
+
|
435
|
+
def collect_unnamed_test_warning(block)
|
436
|
+
return unless block[:type] == :test && block[:description].empty?
|
437
|
+
|
438
|
+
line_number = block[:start_line] + 1
|
439
|
+
context = @lines[block[:start_line]] || ''
|
440
|
+
|
441
|
+
add_warning(ParserWarning.unnamed_test(
|
442
|
+
line_number: line_number,
|
443
|
+
context: context.strip
|
444
|
+
))
|
445
|
+
end
|
446
|
+
|
447
|
+
def validate_strict_mode(testrun)
|
448
|
+
return unless @options[:strict]
|
449
|
+
|
450
|
+
unnamed_test_warnings = warnings.select { |w| w.type == :unnamed_test }
|
451
|
+
return if unnamed_test_warnings.empty?
|
452
|
+
|
453
|
+
# In strict mode, fail with first unnamed test error
|
454
|
+
first_warning = unnamed_test_warnings.first
|
455
|
+
raise TryoutSyntaxError.new(
|
456
|
+
"Strict mode: #{first_warning.message} at line #{first_warning.line_number}. #{first_warning.suggestion}",
|
457
|
+
line_number: first_warning.line_number,
|
458
|
+
context: first_warning.context,
|
459
|
+
source_file: @source_path
|
460
|
+
)
|
461
|
+
end
|
462
|
+
|
414
463
|
end
|
415
464
|
end
|
416
465
|
end
|
data/lib/tryouts/test_case.rb
CHANGED
@@ -11,6 +11,7 @@ class Tryouts
|
|
11
11
|
@output_manager = output_manager
|
12
12
|
@translator = translator
|
13
13
|
@global_tally = global_tally
|
14
|
+
|
14
15
|
end
|
15
16
|
|
16
17
|
def execute
|
@@ -28,6 +29,7 @@ class Tryouts
|
|
28
29
|
|
29
30
|
private
|
30
31
|
|
32
|
+
|
31
33
|
def execute_direct_mode
|
32
34
|
batch = TestBatch.new(
|
33
35
|
@testrun,
|
data/lib/tryouts/test_runner.rb
CHANGED
@@ -29,6 +29,7 @@ class Tryouts
|
|
29
29
|
@output_manager = output_manager
|
30
30
|
@translator = initialize_translator
|
31
31
|
@global_tally = initialize_global_tally
|
32
|
+
@file_line_specs = options[:file_line_specs] || {}
|
32
33
|
end
|
33
34
|
|
34
35
|
def run
|
@@ -37,7 +38,10 @@ class Tryouts
|
|
37
38
|
|
38
39
|
result = process_files
|
39
40
|
show_failure_summary
|
40
|
-
|
41
|
+
# Always show grand total for agent mode to ensure output, otherwise only for multiple files
|
42
|
+
if @options[:agent] || @global_tally[:aggregator].get_file_counts[:total] > 1
|
43
|
+
show_grand_total
|
44
|
+
end
|
41
45
|
|
42
46
|
# For agent critical mode, only count errors as failures
|
43
47
|
if @options[:agent] && (@options[:agent_focus] == :critical || @options[:agent_focus] == 'critical')
|
@@ -148,10 +152,16 @@ class Tryouts
|
|
148
152
|
failure_count
|
149
153
|
end
|
150
154
|
|
151
|
-
def process_file(
|
155
|
+
def process_file(file)
|
156
|
+
# Pass line spec for this file if available
|
157
|
+
file_options = @options.dup
|
158
|
+
if @file_line_specs && @file_line_specs[file]
|
159
|
+
file_options[:line_spec] = @file_line_specs[file]
|
160
|
+
end
|
161
|
+
|
152
162
|
processor = FileProcessor.new(
|
153
|
-
file:
|
154
|
-
options:
|
163
|
+
file: file,
|
164
|
+
options: file_options,
|
155
165
|
output_manager: @output_manager,
|
156
166
|
translator: @translator,
|
157
167
|
global_tally: @global_tally,
|
@@ -160,7 +170,7 @@ class Tryouts
|
|
160
170
|
rescue StandardError => ex
|
161
171
|
handle_file_error(ex)
|
162
172
|
@global_tally[:aggregator].add_infrastructure_failure(
|
163
|
-
:file_processing,
|
173
|
+
:file_processing, file, ex.message, ex
|
164
174
|
)
|
165
175
|
1
|
166
176
|
end
|
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.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Delano Mandelbaum
|
@@ -148,6 +148,7 @@ files:
|
|
148
148
|
- lib/tryouts/cli/formatters/token_budget.rb
|
149
149
|
- lib/tryouts/cli/formatters/tty_status_display.rb
|
150
150
|
- lib/tryouts/cli/formatters/verbose.rb
|
151
|
+
- lib/tryouts/cli/line_spec_parser.rb
|
151
152
|
- lib/tryouts/cli/modes/generate.rb
|
152
153
|
- lib/tryouts/cli/modes/inspect.rb
|
153
154
|
- lib/tryouts/cli/opts.rb
|
@@ -169,6 +170,7 @@ files:
|
|
169
170
|
- lib/tryouts/expectation_evaluators/true.rb
|
170
171
|
- lib/tryouts/failure_collector.rb
|
171
172
|
- lib/tryouts/file_processor.rb
|
173
|
+
- lib/tryouts/parser_warning.rb
|
172
174
|
- lib/tryouts/parsers/base_parser.rb
|
173
175
|
- lib/tryouts/parsers/enhanced_parser.rb
|
174
176
|
- lib/tryouts/parsers/prism_parser.rb
|