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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b0f0a0ca7de83a58c4e822d127a963bb1fb08523d9146b24c8b33f8a2c12ff2
4
- data.tar.gz: 2f4e61611e23176da42cc9bbf82a92a2cc1e9d6049943fd4d4827dcd2b34c84f
3
+ metadata.gz: 7b692cc3a63ac86e060b52e77c246305be0f209dbf06b31b1b08deefbc06434a
4
+ data.tar.gz: 156e86aa4bde377aa2158f3a059983a6fe5f58796f71545c9895a9b57668b126
5
5
  SHA512:
6
- metadata.gz: 1505d26c47b4fb3fb75675425e6aaac3e1ba81b6f39ac8ae47622ae43acd0c19a13a16e82486663de0201f21bf7b997281744fc6d69cae6116e67065803b4a39
7
- data.tar.gz: ca57f3c38348023c57f5e382fe8b45e29877393fae155a718c921550b4a2200b0fe37c1c31be4f41dcbc934527d8c6b53e74fbe88fcc574c53cbc3d765bb3534
6
+ metadata.gz: d289a9b5ccd6694bd63fe4ca2a0c8aaf610d702e74380bf4975ae78ffaf377d0f82a9ec481dbc51105de9f3b07681c32eaa8a1c3566a1eebd0a861796eaff1e3
7
+ data.tar.gz: a0db3ebf76b5a291e6ea9d81f1f6e268a95320d28040a10f2225171b221f04bb2ed525bae3f79785d50f4269eb7c9be1dec242012bbca0a6d4588ac9e7fdd362
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Tryouts v3
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
- if File.directory?(file_or_dir)
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: file_or_dir)
51
- expanded_files.concat(dir_files.map { |f| File.join(file_or_dir, f) })
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
- @current_file_data[:tests] = test_count if @current_file_data
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 file_result(_file_path, total_tests:, failed_count:, error_count:, elapsed_time: nil)
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 when available
70
- if @current_file_data
71
- @current_file_data[:passed] = total_tests - failed_count - error_count
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
- status += " (#{details.join(', ')}, #{passed_count} passed)"
267
+ status_parts << "FAIL: #{issues_count}/#{@total_stats[:tests]} tests (#{details.join(', ')}, #{passed_count} passed)"
224
268
  else
225
- status = "PASS: #{@total_stats[:tests]} tests passed"
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
- status += " (#{format_time(@total_stats[:elapsed])})" if @total_stats[:elapsed]
273
+ status_parts << "(#{format_time(@total_stats[:elapsed])})" if @total_stats[:elapsed]
229
274
 
230
- output << status
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
- puts "No critical errors found"
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
- # Header with overall stats
287
- issues_count = @total_stats[:failures] + @total_stats[:errors]
288
- passed_count = [@total_stats[:tests] - issues_count, 0].max
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
- # Only show files with issues (unless focus is different)
307
- files_to_show = case @focus_mode
308
- when :failures, :first_failure
309
- @collected_files.select { |f| f[:failures].any? || f[:errors].any? }
310
- else
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
- if files_to_show.any?
315
- output << ""
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
- files_to_show.each do |file_data|
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: #{passed_count} passed, #{@total_stats[:failures]} failed"
336
- summary += ", #{@total_stats[:errors]} errors" if @total_stats[:errors] > 0
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, 70)
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
- @stderr.puts "#{pretty_path}: #{test_count} tests"
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 "Total: #{result}#{time_str}"
223
- @stderr.puts "Files: #{successful_files} of #{total_files} successful"
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
- header_line = message.center(@line_width)
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, 2))
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, 1))
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 '=' * 50
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, 'Failure:')} #{failure.failure_reason}"
82
+ puts " #{Console.color(:red, failure.failure_reason)}"
77
83
 
78
- # Show source context in verbose mode
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), 2)
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}"), 2)
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), 2)
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), 2)
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}", 2)
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:', 3)
164
- puts indent_text(Console.color(:dim, '--- BEGIN OUTPUT ---'), 3)
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, 4)
173
+ puts indent_text(line.chomp, 3)
168
174
  end
169
175
 
170
- puts indent_text(Console.color(:dim, '--- END OUTPUT ---'), 3)
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), 2)
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), 2)
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 '=' * @line_width
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
- puts "#{details.join(', ')}, #{passed} passed#{time_str}"
224
+ printf("%-10s %s\n", "Testcases:", "#{details.join(', ')}, #{passed} passed#{time_str}")
220
225
  else
221
- puts "#{total_tests} tests passed#{time_str}"
226
+ printf("%-10s %s\n", "Testcases:", "#{total_tests} tests passed#{time_str}")
222
227
  end
223
228
 
224
- puts "Files: #{successful_files} of #{total_files} successful"
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, 1)
250
+ puts indent_text(error_msg, 0)
247
251
 
248
252
  return unless backtrace && @show_stack_traces
249
253
 
250
- puts indent_text('Details:', 2)
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, 3)
257
+ puts indent_text(line, 2)
254
258
  end
255
- puts indent_text("... (#{backtrace.length - 10} more lines)", 3) if backtrace.length > 10
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:', 4)
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)}", 5)
283
- puts indent_text("Expectation: #{Console.color(:green, expectation.content)}", 5)
284
- puts indent_text("Result: #{Console.color(:green, expected.inspect)}", 5) if expected
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, 4)
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)}", 4)
318
- puts indent_text("Actual: #{Console.color(:red, actual.inspect)}", 4)
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)}", 4)
322
- puts indent_text("Actual: #{Console.color(:red, actual.inspect)}", 4)
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)}", 4)
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:', 4)
341
- puts indent_text("- #{Console.color(:red, actual)}", 5)
342
- puts indent_text("+ #{Console.color(:green, expected)}", 5)
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('-' * @line_width, 1),
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
@@ -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
- validate_files_exist(files, output_manager)
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: 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(files, output_manager)
46
- missing_files = files.reject { |file| File.exist?(file) }
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
@@ -67,7 +67,7 @@ class Tryouts
67
67
  end
68
68
  end
69
69
 
70
- Testrun = Data.define(:setup, :test_cases, :teardown, :source_file, :metadata) do
70
+ Testrun = Data.define(:setup, :test_cases, :teardown, :source_file, :metadata, :warnings) do
71
71
  def total_tests
72
72
  test_cases.size
73
73
  end
@@ -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,
@@ -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
- show_grand_total if @global_tally[:aggregator].get_file_counts[:total] > 1
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(file_path)
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: file_path,
154
- options: @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, file_path, ex.message, ex
173
+ :file_processing, file, ex.message, ex
164
174
  )
165
175
  1
166
176
  end
@@ -1,5 +1,5 @@
1
1
  # lib/tryouts/version.rb
2
2
 
3
3
  class Tryouts
4
- VERSION = '3.4.0'
4
+ VERSION = '3.5.0'
5
5
  end
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.0
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