tryouts 3.4.0 → 3.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b0f0a0ca7de83a58c4e822d127a963bb1fb08523d9146b24c8b33f8a2c12ff2
4
- data.tar.gz: 2f4e61611e23176da42cc9bbf82a92a2cc1e9d6049943fd4d4827dcd2b34c84f
3
+ metadata.gz: ab0eb9c341b10dd5e505a3f62cc33a19a393cb294aecd756809b19c2484784f1
4
+ data.tar.gz: cddfd1a827b0ef37a9e69224124f2f696bbd8c2f098edbfce79b563b3017fbcd
5
5
  SHA512:
6
- metadata.gz: 1505d26c47b4fb3fb75675425e6aaac3e1ba81b6f39ac8ae47622ae43acd0c19a13a16e82486663de0201f21bf7b997281744fc6d69cae6116e67065803b4a39
7
- data.tar.gz: ca57f3c38348023c57f5e382fe8b45e29877393fae155a718c921550b4a2200b0fe37c1c31be4f41dcbc934527d8c6b53e74fbe88fcc574c53cbc3d765bb3534
6
+ metadata.gz: 066b7697b04b3501f01a0994e9d10a66c5a94d593bdfa5cb1b618bb1264f228425bd7d8b4d98cf9a555a5e23bbe002f137c943d5195ad4ce972de30f03dfac9c
7
+ data.tar.gz: 82390327e8da8e35d0c9eaa4557800b09bba76e065704f9d704acab45ab24dd62e97edc7aa54d8104ee50cab31a9dab321c75449386f70243674c8f2d838716a
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
@@ -38,19 +38,28 @@ require_relative '../lib/tryouts'
38
38
  lib_glob = File.join(Dir.pwd, '{lib,../lib,.}')
39
39
  Tryouts.update_load_path(lib_glob) if Tryouts.respond_to?(:update_load_path)
40
40
 
41
+ # Capture original command for agent mode before ARGV gets modified
42
+ original_command = [$0] + ARGV
43
+
41
44
  # Parse args and run CLI
42
45
  begin
43
46
  files, options = Tryouts::CLI.parse_args(ARGV)
44
47
 
45
- # Expand files if directories are given
48
+ # Add original command to options for agent formatter
49
+ options[:original_command] = original_command
50
+
51
+ # Expand files if directories are given, preserving line specs
46
52
  expanded_files = []
47
53
  files.each do |file_or_dir|
48
- if File.directory?(file_or_dir)
54
+ # Parse line spec from the argument
55
+ path_part, line_spec = Tryouts::CLI::LineSpecParser.parse(file_or_dir)
56
+
57
+ if File.directory?(path_part)
49
58
  # 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) })
59
+ dir_files = Dir.glob(['**/*_try.rb', '**/*.try.rb'], base: path_part)
60
+ expanded_files.concat(dir_files.map { |f| File.join(path_part, f) })
52
61
  else
53
- # If it's a file, add it as-is
62
+ # If it's a file, add it as-is (with line spec if present)
54
63
  expanded_files << file_or_dir
55
64
  end
56
65
  end
@@ -4,13 +4,42 @@ require_relative 'token_budget'
4
4
 
5
5
  class Tryouts
6
6
  class CLI
7
- # Agent-optimized formatter designed for LLM context management
8
- # Features:
9
- # - Token budget awareness
10
- # - Structured YAML-like output
11
- # - No redundant file paths
12
- # - Smart truncation
13
- # - Hierarchical organization
7
+ # TOPA (Test Output Protocol for AI) Formatter
8
+ #
9
+ # Language-agnostic test output format designed for LLM context management.
10
+ # This formatter implements the TOPA v1.0 specification for structured,
11
+ # token-efficient test result communication.
12
+ #
13
+ # TOPA Features:
14
+ # - Language-agnostic field naming (snake_case, hierarchical)
15
+ # - Standardized execution context (runtime, environment, VCS)
16
+ # - Token budget awareness with smart truncation
17
+ # - Cross-platform compatibility (CI/CD, package managers)
18
+ # - Structured failure reporting with diffs
19
+ # - Protocol versioning for forward compatibility
20
+ #
21
+ # Field Specifications:
22
+ # - command: Exact command executed
23
+ # - process_id: System process identifier
24
+ # - runtime: Language, version, platform info
25
+ # - package_manager: Dependency management system
26
+ # - version_control: VCS branch/commit info
27
+ # - environment: Normalized env vars (ci_system, app_env, etc.)
28
+ # - test_framework: Framework name, isolation mode, parser
29
+ # - execution_flags: Runtime flags in normalized form
30
+ # - protocol: TOPA version and configuration
31
+ # - project: Auto-detected project type
32
+ # - test_discovery: File pattern matching rules
33
+ #
34
+ # Compatible with: Ruby/RSpec/Minitest, Python/pytest/unittest,
35
+ # JavaScript/Jest/Mocha, Java/JUnit, Go, C#/NUnit, and more.
36
+ #
37
+ # Language Adaptation Examples:
38
+ # Python: runtime.language=python, package_manager.name=pip/poetry/conda
39
+ # Node.js: runtime.language=javascript, package_manager.name=npm/yarn/pnpm
40
+ # Java: runtime.language=java, package_manager.name=maven/gradle
41
+ # Go: runtime.language=go, package_manager.name=go_modules
42
+ # C#: runtime.language=csharp, package_manager.name=nuget/dotnet
14
43
  class AgentFormatter
15
44
  include FormatterInterface
16
45
 
@@ -22,6 +51,9 @@ class Tryouts
22
51
  @current_file_data = nil
23
52
  @total_stats = { files: 0, tests: 0, failures: 0, errors: 0, elapsed: 0 }
24
53
  @output_rendered = false
54
+ @options = options # Store all options for execution context display
55
+ @all_warnings = [] # Store warnings globally for execution details
56
+ @syntax_errors = [] # Store syntax errors for execution details
25
57
 
26
58
  # No colors in agent mode for cleaner parsing
27
59
  @use_colors = false
@@ -42,7 +74,8 @@ class Tryouts
42
74
  tests: 0,
43
75
  failures: [],
44
76
  errors: [],
45
- passed: 0
77
+ passed: 0,
78
+ context_info: context_info # Store context info for later display
46
79
  }
47
80
  end
48
81
 
@@ -56,19 +89,47 @@ class Tryouts
56
89
  end
57
90
 
58
91
  def file_parsed(_file_path, test_count:, setup_present: false, teardown_present: false)
59
- @current_file_data[:tests] = test_count if @current_file_data
92
+ if @current_file_data
93
+ @current_file_data[:tests] = test_count
94
+ end
60
95
  @total_stats[:tests] += test_count
61
96
  end
62
97
 
63
- def file_result(_file_path, total_tests:, failed_count:, error_count:, elapsed_time: nil)
98
+ def parser_warnings(file_path, warnings:)
99
+ return if warnings.empty? || !@options.fetch(:warnings, true)
100
+
101
+ # Store warnings globally for execution details and per-file
102
+ warnings.each do |warning|
103
+ warning_data = {
104
+ type: warning.type.to_s,
105
+ message: warning.message,
106
+ line: warning.line_number,
107
+ suggestion: warning.suggestion,
108
+ file: relative_path(file_path)
109
+ }
110
+ @all_warnings << warning_data
111
+ end
112
+
113
+ # Also store in current file data for potential future use
114
+ if @current_file_data
115
+ @current_file_data[:warnings] = @all_warnings.select { |w| w[:file] == relative_path(file_path) }
116
+ end
117
+ end
118
+
119
+ def file_result(file_path, total_tests:, failed_count:, error_count:, elapsed_time: nil)
64
120
  # Always update global totals
65
121
  @total_stats[:failures] += failed_count
66
122
  @total_stats[:errors] += error_count
67
123
  @total_stats[:elapsed] += elapsed_time if elapsed_time
68
124
 
69
- # Update per-file data when available
70
- if @current_file_data
71
- @current_file_data[:passed] = total_tests - failed_count - error_count
125
+ # Update per-file data - file_result is called AFTER file_end, so data is in @collected_files
126
+ relative_file_path = relative_path(file_path)
127
+ file_data = @collected_files.find { |f| f[:path] == relative_file_path }
128
+
129
+ if file_data
130
+ file_data[:passed] = total_tests - failed_count - error_count
131
+ # Also ensure tests count is correct if it wasn't set properly earlier
132
+ file_data[:tests] ||= total_tests
72
133
  end
73
134
  end
74
135
 
@@ -135,6 +196,14 @@ class Tryouts
135
196
  @output_rendered = true
136
197
  end
137
198
 
199
+ def error_message(message, backtrace: nil)
200
+ # Store syntax errors for display in execution details
201
+ @syntax_errors << {
202
+ message: message,
203
+ backtrace: backtrace
204
+ }
205
+ end
206
+
138
207
  # Override live status - not needed for agent mode
139
208
  def live_status_capabilities
140
209
  {
@@ -209,50 +278,71 @@ class Tryouts
209
278
  def render_summary_only
210
279
  output = []
211
280
 
281
+ # Add execution context header for agent clarity
282
+ output << render_execution_context
283
+ output << ""
284
+
212
285
  # Count failures manually from collected file data (same as other render methods)
213
286
  failed_count = @collected_files.sum { |f| f[:failures].size }
214
287
  error_count = @collected_files.sum { |f| f[:errors].size }
215
288
  issues_count = failed_count + error_count
216
289
  passed_count = [@total_stats[:tests] - issues_count, 0].max
217
290
 
291
+ status_parts = []
218
292
  if issues_count > 0
219
- status = "FAIL: #{issues_count}/#{@total_stats[:tests]} tests"
220
293
  details = []
221
294
  details << "#{failed_count} failed" if failed_count > 0
222
295
  details << "#{error_count} errors" if error_count > 0
223
- status += " (#{details.join(', ')}, #{passed_count} passed)"
296
+ status_parts << "FAIL: #{issues_count}/#{@total_stats[:tests]} tests (#{details.join(', ')}, #{passed_count} passed)"
224
297
  else
225
- status = "PASS: #{@total_stats[:tests]} tests passed"
298
+ # Agent doesn't need output in the positive case (i.e. for passing
299
+ # tests). It just fills out the context window.
226
300
  end
227
301
 
228
- status += " (#{format_time(@total_stats[:elapsed])})" if @total_stats[:elapsed]
302
+ status_parts << "(#{format_time(@total_stats[:elapsed])})" if @total_stats[:elapsed]
229
303
 
230
- output << status
304
+ output << status_parts.join(" ")
305
+
306
+ # Always show file information for agent context
307
+ output << ""
231
308
 
232
- # Show which files had failures
233
309
  files_with_issues = @collected_files.select { |f| f[:failures].any? || f[:errors].any? }
234
310
  if files_with_issues.any?
235
- output << ""
236
- output << "Files with issues:"
311
+ output << "Files:"
237
312
  files_with_issues.each do |file_data|
238
313
  issue_count = file_data[:failures].size + file_data[:errors].size
239
314
  output << " #{file_data[:path]}: #{issue_count} issue#{'s' if issue_count != 1}"
240
315
  end
316
+ elsif @collected_files.any?
317
+ # Show files that were processed successfully
318
+ output << "Files:"
319
+ @collected_files.each do |file_data|
320
+ # Use the passed count from file_result if available, otherwise calculate
321
+ passed_tests = file_data[:passed] ||
322
+ ((file_data[:tests] || 0) - file_data[:failures].size - file_data[:errors].size)
323
+ output << " #{file_data[:path]}: #{passed_tests} test#{'s' if passed_tests != 1} passed"
324
+ end
241
325
  end
242
326
 
243
- puts output.join("\n")
327
+ puts output.join("\n") if output.any?
244
328
  end
245
329
 
246
330
  def render_critical_only
247
331
  # Only show errors (exceptions), skip assertion failures
248
332
  critical_files = @collected_files.select { |f| f[:errors].any? }
249
333
 
334
+ output = []
335
+
336
+ # Add execution context header for agent clarity
337
+ output << render_execution_context
338
+ output << ""
339
+
250
340
  if critical_files.empty?
251
- puts "No critical errors found"
341
+ output << "No critical errors found"
342
+ puts output.join("\n")
252
343
  return
253
344
  end
254
345
 
255
- output = []
256
346
  output << "CRITICAL: #{critical_files.size} file#{'s' if critical_files.size != 1} with errors"
257
347
  output << ""
258
348
 
@@ -283,39 +373,21 @@ class Tryouts
283
373
  def render_full_structured
284
374
  output = []
285
375
 
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)
376
+ # Add execution context header for agent clarity
377
+ output << render_execution_context
378
+ output << ""
305
379
 
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
380
+ # Count actual failures from collected data
381
+ failed_count = @collected_files.sum { |f| f[:failures].size }
382
+ error_count = @collected_files.sum { |f| f[:errors].size }
383
+ issues_count = failed_count + error_count
384
+ passed_count = [@total_stats[:tests] - issues_count, 0].max
313
385
 
314
- if files_to_show.any?
315
- output << ""
316
- @budget.consume("\n")
386
+ # Show files with issues only
387
+ files_with_issues = @collected_files.select { |f| f[:failures].any? || f[:errors].any? }
317
388
 
318
- files_to_show.each do |file_data|
389
+ if files_with_issues.any?
390
+ files_with_issues.each do |file_data|
319
391
  break unless @budget.has_budget?
320
392
 
321
393
  file_section = render_file_section(file_data)
@@ -329,14 +401,15 @@ class Tryouts
329
401
  @budget.consume(file_section)
330
402
  end
331
403
  end
404
+ output << ""
332
405
  end
333
406
 
334
407
  # Final summary line
335
- summary = "Summary: #{passed_count} passed, #{@total_stats[:failures]} failed"
336
- summary += ", #{@total_stats[:errors]} errors" if @total_stats[:errors] > 0
408
+ summary = "Summary: \n"
409
+ summary += "#{passed_count} testcases passed, #{failed_count} failed"
410
+ summary += ", #{error_count} errors" if error_count > 0
337
411
  summary += " in #{@total_stats[:files]} files"
338
412
 
339
- output << ""
340
413
  output << summary
341
414
 
342
415
  puts output.join("\n")
@@ -348,6 +421,20 @@ class Tryouts
348
421
  # File header
349
422
  lines << "#{file_data[:path]}:"
350
423
 
424
+ # Check if file has any issues
425
+ has_issues = file_data[:failures].any? || file_data[:errors].any?
426
+
427
+ # If no issues, show success summary
428
+ if !has_issues
429
+ # Use the passed count from file_result if available, otherwise calculate
430
+ passed_tests = file_data[:passed] ||
431
+ ((file_data[:tests] || 0) - file_data[:failures].size - file_data[:errors].size)
432
+
433
+
434
+ lines << " ✓ #{passed_tests} test#{'s' if passed_tests != 1} passed"
435
+ return lines.join("\n")
436
+ end
437
+
351
438
  # For first-failure mode, only show first error or failure
352
439
  if @focus_mode == :first_failure || @focus_mode == :'first-failure'
353
440
  shown_count = 0
@@ -445,6 +532,199 @@ class Tryouts
445
532
  "#{seconds.round(2)}s"
446
533
  end
447
534
  end
535
+
536
+ def render_execution_context
537
+ context_lines = []
538
+ context_lines << "EXECUTION_CONTEXT:"
539
+
540
+ # Command that was executed
541
+ if @options[:original_command]
542
+ command_str = @options[:original_command].join(' ')
543
+ context_lines << " command: #{command_str}"
544
+ end
545
+
546
+ # Compact system info on one line when possible
547
+ context_lines << " pid: #{Process.pid} | pwd: #{Dir.pwd}"
548
+
549
+ # Runtime - compact format
550
+ platform = RUBY_PLATFORM.gsub(/darwin\d+/, 'darwin') # Simplify darwin25 -> darwin
551
+ context_lines << " runtime: ruby #{RUBY_VERSION} (#{platform})"
552
+
553
+ # Package manager - only if present, compact format
554
+ if defined?(Bundler)
555
+ context_lines << " package_manager: bundler #{Bundler::VERSION}"
556
+ end
557
+
558
+ # Version control - compact single line with timeout protection
559
+ git_info = safe_git_info
560
+ if git_info[:branch] && git_info[:commit] && !git_info[:branch].empty? && !git_info[:commit].empty?
561
+ context_lines << " vcs: git #{git_info[:branch]}@#{git_info[:commit]}"
562
+ end
563
+
564
+ # Environment - only non-defaults
565
+ env_vars = build_environment_context
566
+ if env_vars.any?
567
+ # Compact key=value format
568
+ env_str = env_vars.map { |k, v| "#{k}=#{v}" }.join(', ')
569
+ context_lines << " environment: #{env_str}"
570
+ end
571
+
572
+ # Test framework - compact critical info only
573
+ framework = @options[:framework] || :direct
574
+ shared_context = if @options.key?(:shared_context)
575
+ @options[:shared_context]
576
+ else
577
+ # Apply framework defaults
578
+ case framework
579
+ when :rspec, :minitest
580
+ false
581
+ else
582
+ true # direct/tryouts defaults to shared
583
+ end
584
+ end
585
+
586
+ isolation = shared_context ? 'shared' : 'isolated'
587
+ context_lines << " test_framework: #{framework} (#{isolation})"
588
+
589
+ # Execution flags - only if non-standard
590
+ flags = build_execution_flags
591
+ if flags.any?
592
+ context_lines << " flags: #{flags.join(', ')}"
593
+ end
594
+
595
+ # TOPA protocol - compact
596
+ context_lines << " protocol: TOPA v1.0 | focus: #{@focus_mode} | limit: #{@budget.limit}"
597
+
598
+ # File count being tested
599
+ if @collected_files && @collected_files.any?
600
+ context_lines << " files_under_test: #{@collected_files.size}"
601
+ elsif @total_stats[:files] && @total_stats[:files] > 0
602
+ context_lines << " files_under_test: #{@total_stats[:files]}"
603
+ end
604
+
605
+ # Add syntax errors if any (these prevent test execution)
606
+ if @syntax_errors.any?
607
+ context_lines << ""
608
+ context_lines << "Syntax Errors:"
609
+ @syntax_errors.each do |error|
610
+ # Clean up the error message to remove redundant prefixes
611
+ clean_message = error[:message].gsub(/^ERROR:\s*/i, '').strip
612
+ context_lines << " #{clean_message}"
613
+ if error[:backtrace] && @options[:debug]
614
+ error[:backtrace].first(3).each do |trace|
615
+ context_lines << " #{trace}"
616
+ end
617
+ end
618
+ end
619
+ end
620
+
621
+ # Add warnings if any
622
+ if @all_warnings.any? && @options.fetch(:warnings, true)
623
+ context_lines << ""
624
+ context_lines << "Parser Warnings:"
625
+ @all_warnings.each do |warning|
626
+ context_lines << " #{warning[:file]}:#{warning[:line]}: #{warning[:message]}"
627
+ context_lines << " #{warning[:suggestion]}" if warning[:suggestion]
628
+ end
629
+ end
630
+
631
+ context_lines.join("\n")
632
+ end
633
+
634
+ # Build environment context with language-agnostic keys
635
+ def build_environment_context
636
+ env_vars = {}
637
+
638
+ # CI/CD detection - prioritize most specific
639
+ if ENV['GITHUB_ACTIONS']
640
+ env_vars['CI'] = 'github'
641
+ elsif ENV['GITLAB_CI']
642
+ env_vars['CI'] = 'gitlab'
643
+ elsif ENV['JENKINS_URL']
644
+ env_vars['CI'] = 'jenkins'
645
+ elsif ENV['CI']
646
+ env_vars['CI'] = 'true'
647
+ end
648
+
649
+ # Runtime environment - only if not default
650
+ if ENV['RAILS_ENV'] && ENV['RAILS_ENV'] != 'development'
651
+ env_vars['ENV'] = ENV['RAILS_ENV']
652
+ elsif ENV['RACK_ENV'] && ENV['RACK_ENV'] != 'development'
653
+ env_vars['ENV'] = ENV['RACK_ENV']
654
+ elsif ENV['NODE_ENV'] && ENV['NODE_ENV'] != 'development'
655
+ env_vars['ENV'] = ENV['NODE_ENV']
656
+ end
657
+
658
+ # Coverage - simplified
659
+ env_vars['COV'] = '1' if ENV['COVERAGE'] || ENV['SIMPLECOV']
660
+
661
+ # Test seed for reproducibility
662
+ env_vars['SEED'] = ENV['SEED'] if ENV['SEED']
663
+
664
+ env_vars
665
+ end
666
+
667
+ # Build execution flags in language-agnostic format
668
+ def build_execution_flags
669
+ flags = []
670
+ flags << "verbose" if @options[:verbose]
671
+ flags << "fails-only" if @options[:fails_only]
672
+ flags << "debug" if @options[:debug]
673
+ flags << "traces" if @options[:stack_traces] && !@options[:debug] # debug implies traces
674
+ flags << "parallel" if @options[:parallel]
675
+ flags << "line-spec" if @options[:line_spec]
676
+ flags << "strict" if @options[:strict]
677
+ flags << "quiet" if @options[:quiet]
678
+ flags
679
+ end
680
+
681
+ # Get test discovery patterns in language-agnostic format
682
+ def get_test_discovery_patterns
683
+ patterns = []
684
+
685
+ # Ruby/Tryouts patterns
686
+ patterns.concat([
687
+ "**/*_try.rb",
688
+ "**/*.try.rb",
689
+ "try/**/*.rb",
690
+ "tryouts/**/*.rb"
691
+ ])
692
+
693
+ # TOPA-compatible patterns for other languages:
694
+ # Python: ["**/*_test.py", "**/test_*.py", "tests/**/*.py"]
695
+ # JavaScript: ["**/*.test.js", "**/*.spec.js", "__tests__/**/*.js"]
696
+ # Java: ["**/*Test.java", "**/Test*.java", "src/test/**/*.java"]
697
+ # Go: ["**/*_test.go"]
698
+ # C#: ["**/*Test.cs", "**/*Tests.cs"]
699
+ # PHP: ["**/*Test.php", "tests/**/*.php"]
700
+ # Rust: ["**/*_test.rs", "tests/**/*.rs"]
701
+
702
+ patterns
703
+ end
704
+
705
+ private
706
+
707
+ # Safely get git information with timeout protection
708
+ def safe_git_info
709
+ # Check if we're in a git repository
710
+ return {} unless File.directory?('.git') || system('git rev-parse --git-dir >/dev/null 2>&1')
711
+
712
+ require 'timeout'
713
+
714
+ Timeout.timeout(2) do
715
+ branch = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
716
+ commit = `git rev-parse --short HEAD 2>/dev/null`.strip
717
+
718
+ # Validate output to prevent injection
719
+ branch = nil unless branch =~ /\A[\w\-\/\.]+\z/
720
+ commit = nil unless commit =~ /\A[a-f0-9]+\z/i
721
+
722
+ { branch: branch, commit: commit }
723
+ end
724
+ rescue Timeout::Error, StandardError
725
+ # Return empty hash on any error (timeout, permission, etc.)
726
+ {}
727
+ end
448
728
  end
449
729
  end
450
730
  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,