tryouts 3.1.1 → 3.2.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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/exe/try +3 -3
  3. data/lib/tryouts/cli/formatters/base.rb +108 -48
  4. data/lib/tryouts/cli/formatters/compact.rb +97 -105
  5. data/lib/tryouts/cli/formatters/factory.rb +8 -2
  6. data/lib/tryouts/cli/formatters/live_status_manager.rb +138 -0
  7. data/lib/tryouts/cli/formatters/output_manager.rb +78 -66
  8. data/lib/tryouts/cli/formatters/quiet.rb +54 -102
  9. data/lib/tryouts/cli/formatters/test_run_state.rb +122 -0
  10. data/lib/tryouts/cli/formatters/tty_status_display.rb +273 -0
  11. data/lib/tryouts/cli/formatters/verbose.rb +103 -105
  12. data/lib/tryouts/cli/formatters.rb +3 -0
  13. data/lib/tryouts/cli/opts.rb +17 -8
  14. data/lib/tryouts/cli/tty_detector.rb +92 -0
  15. data/lib/tryouts/console.rb +1 -1
  16. data/lib/tryouts/expectation_evaluators/boolean.rb +1 -1
  17. data/lib/tryouts/expectation_evaluators/exception.rb +2 -2
  18. data/lib/tryouts/expectation_evaluators/expectation_result.rb +3 -3
  19. data/lib/tryouts/expectation_evaluators/false.rb +1 -1
  20. data/lib/tryouts/expectation_evaluators/intentional_failure.rb +2 -2
  21. data/lib/tryouts/expectation_evaluators/output.rb +6 -6
  22. data/lib/tryouts/expectation_evaluators/performance_time.rb +3 -3
  23. data/lib/tryouts/expectation_evaluators/regex_match.rb +2 -2
  24. data/lib/tryouts/expectation_evaluators/regular.rb +1 -1
  25. data/lib/tryouts/expectation_evaluators/result_type.rb +1 -1
  26. data/lib/tryouts/expectation_evaluators/true.rb +2 -2
  27. data/lib/tryouts/failure_collector.rb +109 -0
  28. data/lib/tryouts/prism_parser.rb +17 -17
  29. data/lib/tryouts/test_batch.rb +9 -5
  30. data/lib/tryouts/test_case.rb +4 -4
  31. data/lib/tryouts/test_runner.rb +12 -1
  32. data/lib/tryouts/version.rb +1 -1
  33. data/lib/tryouts.rb +0 -9
  34. metadata +21 -22
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ca346c47683b393c8c60c586ab053a73c09b7d5f3e19ce184b35453135ae6594
4
- data.tar.gz: 2ba211232994981e53393167cce8e09fc29bbc988d14b9ac3178321cbf158aaf
3
+ metadata.gz: f66a1e1e61b8c2d5e7201db55fe494954a8ec626bcb333bfc46d0842d469d897
4
+ data.tar.gz: a6fd7107a2934b4011a7093a1f28807fc5c07e67dfbcfdf266881978b1093294
5
5
  SHA512:
6
- metadata.gz: 12f4d97b1f2049f1f6df4d0badca42d2a585360f12678e678cfbbcb85e0768cc2365eb7eb09a744713df15e2d51d3df9690fde78d160ff73947d2d9bdf986aa9
7
- data.tar.gz: d919d1923c471aaa240f66dd0f03760bf7f5f9e53f71c35f8ace03d84935648dc21dafd5c8dceb68468a1b7e31ed6a47ad554471758d6e345146e15a661eca4f
6
+ metadata.gz: 8846a206ebdebcbb816f5e9af65c7054f1719b9f511912dbf4b311811557a3124f18d026424e83a7a31150b8de6525c395e19748e45d0297f356465561ed30bb
7
+ data.tar.gz: f2affd5b231a61f9e5e5f4469c9297718c15da7637d849aad221249d1f83345b75cc17ab8098a1ea9ec9e173990f940c450ef31e3b008be84aa99b3ac9d86af4
data/exe/try CHANGED
@@ -46,8 +46,8 @@ begin
46
46
  expanded_files = []
47
47
  files.each do |file_or_dir|
48
48
  if File.directory?(file_or_dir)
49
- # If it's a directory, find all *_try.rb files within it
50
- dir_files = Dir.glob('**/*_try.rb', base: file_or_dir)
49
+ # 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
51
  expanded_files.concat(dir_files.map { |f| File.join(file_or_dir, f) })
52
52
  else
53
53
  # If it's a file, add it as-is
@@ -59,7 +59,7 @@ begin
59
59
  # Default file discovery if no files specified
60
60
  if files.empty?
61
61
  raw_files = Dir.glob(
62
- ['{app,apps,lib,try,tryouts}/**/*_try.rb', './*_try.rb'],
62
+ ['{app,apps,lib,try,tryouts}/**/*_try.rb', './*_try.rb', '{app,apps,lib,try,tryouts}/**/*.try.rb', './*.try.rb'],
63
63
  base: Dir.pwd,
64
64
  )
65
65
 
@@ -1,120 +1,180 @@
1
1
  # lib/tryouts/cli/formatters/base.rb
2
2
 
3
- require_relative 'factory'
4
3
  require_relative 'output_manager'
5
4
 
6
5
  class Tryouts
7
6
  class CLI
8
7
  # Enhanced interface for all test output formatting
9
8
  module FormatterInterface
9
+ attr_reader :stdout, :stderr, :current_indent
10
10
 
11
- attr_reader :current_indent
11
+ def initialize(options = {})
12
+ @stdout = options.fetch(:stdout, $stdout)
13
+ @stderr = options.fetch(:stderr, $stderr)
14
+ @current_indent = 0
15
+ @options = options
16
+ end
12
17
 
13
18
  # Phase-level output (major sections)
14
- def phase_header(message, file_count = nil, level = 0, io = $stdout)
15
- raise NotImplementedError, "#{self.class} must implement #phase_header"
19
+ def phase_header(message, file_count: nil)
20
+ # Default: no output
16
21
  end
17
22
 
18
23
  # File-level operations
19
- def file_start(file_path, context_info = {}, io = $stdout)
20
- raise NotImplementedError, "#{self.class} must implement #file_start"
24
+ def file_start(file_path, context_info: {})
25
+ # Default: no output
21
26
  end
22
27
 
23
- def file_end(file_path, context_info = {}, io = $stdout)
24
- raise NotImplementedError, "#{self.class} must implement #file_end"
28
+ def file_end(file_path, context_info: {})
29
+ # Default: no output
25
30
  end
26
31
 
27
- def file_parsed(file_path, test_count, io = $stdout, setup_present: false, teardown_present: false)
28
- raise NotImplementedError, "#{self.class} must implement #file_parsed"
32
+ def file_parsed(file_path, test_count:, setup_present: false, teardown_present: false)
33
+ # Default: no output
29
34
  end
30
35
 
31
- def file_execution_start(file_path, test_count, context_mode, io = $stdout)
32
- raise NotImplementedError, "#{self.class} must implement #file_execution_start"
36
+ def file_execution_start(file_path, test_count:, context_mode:)
37
+ # Default: no output
33
38
  end
34
39
 
35
- def file_result(file_path, total_tests, failed_count, error_count, elapsed_time, io = $stdout)
36
- raise NotImplementedError, "#{self.class} must implement #file_result"
40
+ def file_result(file_path, total_tests:, failed_count:, error_count:, elapsed_time: nil)
41
+ # Default: no output
37
42
  end
38
43
 
39
44
  # Test-level operations
40
- def test_start(test_case, index, total, io = $stdout)
41
- raise NotImplementedError, "#{self.class} must implement #test_start"
45
+ def test_start(test_case:, index:, total:)
46
+ # Default: no output
42
47
  end
43
48
 
44
- def test_end(test_case, index, total, io = $stdout)
45
- raise NotImplementedError, "#{self.class} must implement #test_end"
49
+ def test_end(test_case:, index:, total:)
50
+ # Default: no output
46
51
  end
47
52
 
48
- def test_result(result_packet, io = $stdout)
49
- raise NotImplementedError, "#{self.class} must implement #test_result"
53
+ def test_result(result_packet)
54
+ # Default: no output
50
55
  end
51
56
 
52
- def test_output(test_case, output_text, io = $stdout)
53
- raise NotImplementedError, "#{self.class} must implement #test_output"
57
+ def test_output(test_case:, output_text:, result_packet:)
58
+ # Default: no output
54
59
  end
55
60
 
56
61
  # Setup/teardown operations
57
- def setup_start(line_range, io = $stdout)
58
- raise NotImplementedError, "#{self.class} must implement #setup_start"
62
+ def setup_start(line_range:)
63
+ # Default: no output
59
64
  end
60
65
 
61
- def setup_output(output_text, io = $stdout)
62
- raise NotImplementedError, "#{self.class} must implement #setup_output"
66
+ def setup_output(output_text)
67
+ # Default: no output
63
68
  end
64
69
 
65
- def teardown_start(line_range, io = $stdout)
66
- raise NotImplementedError, "#{self.class} must implement #teardown_start"
70
+ def teardown_start(line_range:)
71
+ # Default: no output
67
72
  end
68
73
 
69
- def teardown_output(output_text, io = $stdout)
70
- raise NotImplementedError, "#{self.class} must implement #teardown_output"
74
+ def teardown_output(output_text)
75
+ # Default: no output
71
76
  end
72
77
 
73
78
  # Summary operations
74
- def batch_summary(total_tests, failed_count, elapsed_time, io = $stdout)
75
- raise NotImplementedError, "#{self.class} must implement #batch_summary"
79
+ def batch_summary(failure_collector)
80
+ # Default: no output
76
81
  end
77
82
 
78
- def grand_total(total_tests, failed_count, error_count, successful_files, total_files, elapsed_time, io = $stdout)
79
- raise NotImplementedError, "#{self.class} must implement #grand_total"
83
+ def grand_total(total_tests:, failed_count:, error_count:, successful_files:, total_files:, elapsed_time:)
84
+ # Default: no output
80
85
  end
81
86
 
82
87
  # Debug and diagnostic output
83
- def debug_info(message, level = 0, io = $stdout)
84
- raise NotImplementedError, "#{self.class} must implement #debug_info"
88
+ def debug_info(message, level: 0)
89
+ # Default: no output
90
+ end
91
+
92
+ def trace_info(message, level: 0)
93
+ # Default: no output
94
+ end
95
+
96
+ def error_message(message, backtrace: nil)
97
+ # Default: no output
98
+ end
99
+
100
+ # Live status capability negotiation
101
+ def live_status_capabilities
102
+ {
103
+ supports_coordination: false, # Can work with coordinated output
104
+ output_frequency: :medium, # :low, :medium, :high
105
+ requires_tty: false, # Must have TTY to function
106
+ }
107
+ end
108
+
109
+ # Live status integration (optional methods)
110
+ def set_live_status_manager(manager)
111
+ @live_status_manager = manager
85
112
  end
86
113
 
87
- def trace_info(message, level = 0, io = $stdout)
88
- raise NotImplementedError, "#{self.class} must implement #trace_info"
114
+ def live_status_manager
115
+ @live_status_manager
89
116
  end
90
117
 
91
- def error_message(message, details = nil, io = $stdout)
92
- raise NotImplementedError, "#{self.class} must implement #error_message"
118
+ # Standard output methods that coordinate with live status automatically
119
+ def write(text)
120
+ if @live_status_manager&.enabled?
121
+ @live_status_manager.write_string(text)
122
+ else
123
+ @stdout.print(text)
124
+ end
93
125
  end
94
126
 
95
- # Utility methods
96
- def raw_output(text, io = $stdout)
97
- raise NotImplementedError, "#{self.class} must implement #raw_output"
127
+ def puts(text = '')
128
+ write("#{text}\n")
98
129
  end
99
130
 
100
- def separator(style = :light, io = $stdout)
101
- raise NotImplementedError, "#{self.class} must implement #separator"
131
+ # Optional: formatters can implement this to provide custom live status updates
132
+ def update_live_status(state_updates = {})
133
+ @live_status_manager&.update_status(state_updates)
102
134
  end
103
135
 
136
+ protected
137
+
138
+ # Utility methods for formatters to use
104
139
  def indent_text(text, level = nil)
105
140
  level ||= current_indent || 0
106
- indent = ' ' * level
141
+ indent = ' ' * level
107
142
  "#{indent}#{text}"
108
143
  end
109
144
 
110
145
  def with_indent(level)
111
- old_indent = @current_indent
146
+ old_indent = @current_indent
112
147
  @current_indent = level
113
148
  yield
114
149
  ensure
115
150
  @current_indent = old_indent
116
151
  end
117
- end
118
152
 
153
+ def separator(style = :light)
154
+ width = @options.fetch(:line_width, 70)
155
+ case style
156
+ when :heavy
157
+ '=' * width
158
+ when :light
159
+ '-' * width
160
+ when :dotted
161
+ '.' * width
162
+ else
163
+ '-' * width
164
+ end
165
+ end
166
+
167
+ def format_timing(elapsed_time)
168
+ return '' unless elapsed_time
169
+
170
+ if elapsed_time < 0.001
171
+ " (#{(elapsed_time * 1_000_000).round}μs)"
172
+ elsif elapsed_time < 1
173
+ " (#{(elapsed_time * 1000).round}ms)"
174
+ else
175
+ " (#{elapsed_time.round(2)}s)"
176
+ end
177
+ end
178
+ end
119
179
  end
120
180
  end
@@ -7,38 +7,31 @@ class Tryouts
7
7
  include FormatterInterface
8
8
 
9
9
  def initialize(options = {})
10
- @show_debug = options.fetch(:debug, false)
11
- @show_trace = options.fetch(:trace, false)
12
- @show_passed = options.fetch(:show_passed, true)
10
+ super
11
+ @show_debug = options.fetch(:debug, false)
12
+ @show_trace = options.fetch(:trace, false)
13
+ @show_passed = options.fetch(:show_passed, true)
13
14
  end
14
15
 
15
16
  # Phase-level output - minimal for compact mode
16
- def phase_header(message, file_count = nil, level = 0, io = $stderr)
17
+ def phase_header(message, file_count: nil)
17
18
  # Show processing header but skip execution phase headers to avoid empty lines
18
- case level
19
- when 0
19
+ if message.include?('PROCESSING')
20
20
  # Main processing header
21
21
  text = file_count ? "#{message}" : "#{message}..."
22
- io.puts text
23
- when 1
24
- # Skip execution phase headers - they create unwanted empty lines
25
- return
26
- else
22
+ @stderr.puts text
23
+ elsif !message.include?('EXECUTING')
27
24
  # Other phase headers with minimal formatting
28
- io.puts indent_text(message, level - 1)
25
+ @stderr.puts message
29
26
  end
30
27
  end
31
28
 
32
29
  # File-level operations - compact single lines
33
- def file_start(file_path, _context_info = {}, io = $stderr)
34
- # See file_execution_start
35
- end
36
-
37
- def file_end(file_path, context_info = {}, io = $stderr)
38
- # No output in compact mode
30
+ def file_start(file_path, context_info: {})
31
+ # Output handled in file_execution_start for compact mode
39
32
  end
40
33
 
41
- def file_parsed(_file_path, test_count, io = $stderr, setup_present: false, teardown_present: false)
34
+ def file_parsed(_file_path, test_count:, setup_present: false, teardown_present: false)
42
35
  # Don't show parsing info in compact mode unless debug
43
36
  return unless @show_debug
44
37
 
@@ -47,25 +40,46 @@ class Tryouts
47
40
  extras << 'teardown' if teardown_present
48
41
  suffix = extras.empty? ? '' : " +#{extras.join(',')}"
49
42
 
50
- io.puts indent_text("Parsed #{test_count} tests#{suffix}", 1)
43
+ @stderr.puts indent_text("Parsed #{test_count} tests#{suffix}", 1)
51
44
  end
52
45
 
53
- def file_execution_start(file_path, test_count, _context_mode, io = $stderr)
46
+ def file_execution_start(file_path, test_count:, context_mode:)
54
47
  pretty_path = Console.pretty_path(file_path)
55
- io.puts "#{pretty_path}: #{test_count} tests"
48
+ @stderr.puts "#{pretty_path}: #{test_count} tests"
56
49
  end
57
50
 
58
- # Summary operations
59
- def batch_summary(total_tests, failed_count, elapsed_time)
60
- # Skip - file_result already shows this information with better alignment
51
+ # Summary operations - show failure summary
52
+ def batch_summary(failure_collector)
53
+ return unless failure_collector.any_failures?
54
+
55
+ puts
56
+ puts separator
57
+ puts Console.color(:red, 'Failed Tests:')
58
+ puts
59
+
60
+ failure_collector.failures_by_file.each do |file_path, failures|
61
+ failures.each do |failure|
62
+ pretty_path = Console.pretty_path(file_path)
63
+
64
+ # Include line number with file path for easy copying/clicking
65
+ location = if failure.line_number > 0
66
+ "#{pretty_path}:#{failure.line_number}"
67
+ else
68
+ pretty_path
69
+ end
70
+
71
+ puts " #{location}"
72
+ puts " #{Console.color(:red, '✗')} #{failure.description}"
73
+ puts " #{failure.failure_reason}"
74
+ puts
75
+ end
76
+ end
61
77
  end
62
78
 
63
- def file_result(_file_path, total_tests, failed_count, error_count, elapsed_time, io = $stdout)
79
+ def file_result(_file_path, total_tests:, failed_count:, error_count:, elapsed_time: nil)
64
80
  issues_count = failed_count + error_count
65
81
  passed_count = total_tests - issues_count
66
- details = [
67
- # "#{passed_count} passed",
68
- ]
82
+ details = []
69
83
 
70
84
  if issues_count > 0
71
85
  status = Console.color(:red, '✗')
@@ -76,38 +90,28 @@ class Tryouts
76
90
  end
77
91
 
78
92
  if error_count > 0
79
- status = Console.color(:yellow, '⚠') if error_count == 0
80
93
  details << "#{error_count} errors"
81
94
  end
82
95
 
83
96
  if failed_count > 0
84
- status = Console.color(:yellow, '⚠') if failed_count == 0
85
97
  details << "#{failed_count} failed"
86
98
  end
87
99
 
88
- time_str = if elapsed_time
89
- format_timing(elapsed_time)
90
- else
91
- ''
92
- end
93
- io.puts " #{status} #{details.join(', ')}#{time_str}"
100
+ time_str = format_timing(elapsed_time)
101
+ puts " #{status} #{details.join(', ')}#{time_str}"
94
102
  end
95
103
 
96
104
  # Test-level operations - only show in debug mode for compact
97
- def test_start(test_case, index, _total, io = $stdout)
105
+ def test_start(test_case:, index:, total:)
98
106
  return unless @show_debug
99
107
 
100
108
  desc = test_case.description.to_s
101
109
  desc = "test #{index}" if desc.empty?
102
110
 
103
- io.puts " Running: #{desc}"
104
- end
105
-
106
- def test_end(test_case, index, _total, io = $stdout)
107
- # No output for test end
111
+ puts " Running: #{desc}"
108
112
  end
109
113
 
110
- def test_result(result_packet, io = $stdout)
114
+ def test_result(result_packet)
111
115
  # Only show failed tests in compact mode unless show_passed is true
112
116
  return if result_packet.passed? && !@show_passed
113
117
 
@@ -118,152 +122,132 @@ class Tryouts
118
122
  case result_packet.status
119
123
  when :passed
120
124
  status = Console.color(:green, '✓')
121
- io.puts indent_text("#{status} #{desc}", 1)
125
+ puts indent_text("#{status} #{desc}", 1)
122
126
  when :failed
123
127
  status = Console.color(:red, '✗')
124
- io.puts indent_text("#{status} #{desc}", 1)
128
+ puts indent_text("#{status} #{desc}", 1)
125
129
 
126
130
  # Show minimal context for failures
127
131
  if result_packet.actual_results.any?
128
132
  failure_info = "got: #{result_packet.first_actual.inspect}"
129
- io.puts indent_text(" #{failure_info}", 1)
133
+ puts indent_text(" #{failure_info}", 1)
130
134
  end
131
135
 
132
136
  # Show 1-2 lines of test context if available
133
137
  if test_case.source_lines && test_case.source_lines.size <= 3
134
138
  test_case.source_lines.each do |line|
135
139
  next if line.strip.empty? || line.strip.start_with?('#')
136
- io.puts indent_text(" #{line.strip}", 1)
140
+
141
+ puts indent_text(" #{line.strip}", 1)
137
142
  break # Only show first relevant line
138
143
  end
139
144
  end
140
145
  when :skipped
141
146
  status = Console.color(:yellow, '-')
142
- io.puts indent_text("#{status} #{desc}", 1)
147
+ puts indent_text("#{status} #{desc}", 1)
143
148
  else
144
149
  status = '?'
145
- io.puts indent_text("#{status} #{desc}", 1)
150
+ puts indent_text("#{status} #{desc}", 1)
146
151
  end
147
152
  end
148
153
 
149
- def test_output(_test_case, output_text, io = $stdout)
154
+ def test_output(test_case:, output_text:, result_packet:)
150
155
  # In compact mode, only show output for failed tests and only if debug mode is enabled
151
156
  return if output_text.nil? || output_text.strip.empty?
152
157
  return unless @show_debug
158
+ return if result_packet.passed?
153
159
 
154
- io.puts " Output: #{output_text.lines.count} lines"
160
+ puts " Output: #{output_text.lines.count} lines"
155
161
  if output_text.lines.count <= 3
156
162
  output_text.lines.each do |line|
157
- io.puts " #{line.chomp}"
163
+ puts " #{line.chomp}"
158
164
  end
159
165
  else
160
- io.puts " #{output_text.lines.first.chomp}"
161
- io.puts " ... (#{output_text.lines.count - 2} more lines)"
162
- io.puts " #{output_text.lines.last.chomp}"
166
+ puts " #{output_text.lines.first.chomp}"
167
+ puts " ... (#{output_text.lines.count - 2} more lines)"
168
+ puts " #{output_text.lines.last.chomp}"
163
169
  end
164
170
  end
165
171
 
166
172
  # Setup/teardown operations - minimal output
167
- def setup_start(_line_range)
173
+ def setup_start(line_range:)
168
174
  # No file setup start output for compact
169
175
  end
170
176
 
171
- def setup_output(output_text, io = $stderr)
177
+ def setup_output(output_text)
172
178
  return if output_text.strip.empty?
173
179
  return unless @show_debug
174
180
 
175
181
  # In compact mode, just show that there was output
176
182
  lines = output_text.lines.count
177
- io.puts " Setup output (#{lines} lines)"
183
+ @stderr.puts " Setup output (#{lines} lines)"
178
184
  end
179
185
 
180
- def teardown_start(_line_range, io = $stderr)
186
+ def teardown_start(line_range:)
181
187
  return unless @show_debug
182
188
 
183
- io.puts ' Teardown...'
189
+ @stderr.puts ' Teardown...'
184
190
  end
185
191
 
186
- def teardown_output(output_text, io = $stderr)
192
+ def teardown_output(output_text)
187
193
  return if output_text.strip.empty?
188
194
  return unless @show_debug
189
195
 
190
196
  # In compact mode, just show that there was output
191
197
  lines = output_text.lines.count
192
- io.puts " Teardown output (#{lines} lines)"
198
+ @stderr.puts " Teardown output (#{lines} lines)"
193
199
  end
194
200
 
195
- def grand_total(total_tests, failed_count, error_count, successful_files, total_files, elapsed_time, io = $stderr)
196
- io.puts
197
- io.puts '=' * 50
201
+ def grand_total(total_tests:, failed_count:, error_count:, successful_files:, total_files:, elapsed_time:)
202
+ @stderr.puts
203
+ @stderr.puts '=' * 50
198
204
 
199
205
  issues_count = failed_count + error_count
200
206
  if issues_count > 0
201
- passed = [total_tests - issues_count, 0].max # Ensure passed never goes negative
207
+ passed = [total_tests - issues_count, 0].max # Ensure passed never goes negative
202
208
  details = []
203
209
  details << "#{failed_count} failed" if failed_count > 0
204
210
  details << "#{error_count} errors" if error_count > 0
205
- result = Console.color(:red, "#{details.join(', ')}, #{passed} passed")
211
+ result = Console.color(:red, "#{details.join(', ')}, #{passed} passed")
206
212
  else
207
213
  result = Console.color(:green, "#{total_tests} tests passed")
208
214
  end
209
215
 
210
216
  time_str = format_timing(elapsed_time)
211
217
 
212
- io.puts "Total: #{result} (#{time_str})"
213
- io.puts "Files: #{successful_files} of #{total_files} successful"
218
+ @stderr.puts "Total: #{result}#{time_str}"
219
+ @stderr.puts "Files: #{successful_files} of #{total_files} successful"
214
220
  end
215
221
 
216
222
  # Debug and diagnostic output - minimal in compact mode
217
- def debug_info(message, level = 0, io = $stderr)
223
+ def debug_info(message, level: 0)
218
224
  return unless @show_debug
219
225
 
220
- io.puts indent_text("DEBUG: #{message}", level)
226
+ @stderr.puts indent_text("DEBUG: #{message}", level)
221
227
  end
222
228
 
223
- def trace_info(message, level = 0, io = $stderr)
229
+ def trace_info(message, level: 0)
224
230
  return unless @show_trace
225
231
 
226
- io.puts indent_text("TRACE: #{message}", level)
232
+ @stderr.puts indent_text("TRACE: #{message}", level)
227
233
  end
228
234
 
229
- def error_message(message, backtrace = nil, io = $stderr)
230
- io.puts Console.color(:red, "ERROR: #{message}")
235
+ def error_message(message, backtrace: nil)
236
+ @stderr.puts Console.color(:red, "ERROR: #{message}")
231
237
 
232
238
  return unless backtrace && @show_debug
233
239
 
234
240
  backtrace.first(3).each do |line|
235
- io.puts indent_text(line.chomp, 1)
241
+ @stderr.puts indent_text(line.chomp, 1)
236
242
  end
237
243
  end
238
244
 
239
- # Utility methods
240
- def raw_output(text, io = $stdout)
241
- io.puts text
242
- end
243
-
244
- def separator(style = :light, io = $stdout)
245
- case style
246
- when :heavy
247
- io.puts '=' * 50
248
- when :light
249
- io.puts '-' * 50
250
- when :dotted
251
- io.puts '.' * 50
252
- else
253
- io.puts '-' * 50
254
- end
255
- end
256
-
257
- private
258
-
259
- def format_timing(elapsed_time)
260
- if elapsed_time < 0.001
261
- " (#{(elapsed_time * 1_000_000).round}μs)"
262
- elsif elapsed_time < 1
263
- " (#{(elapsed_time * 1000).round}ms)"
264
- else
265
- " (#{elapsed_time.round(2)}s)"
266
- end
245
+ def live_status_capabilities
246
+ {
247
+ supports_coordination: true, # Compact can work with coordinated output
248
+ output_frequency: :medium, # Outputs at medium frequency
249
+ requires_tty: false, # Works without TTY
250
+ }
267
251
  end
268
252
  end
269
253
 
@@ -279,6 +263,14 @@ class Tryouts
279
263
 
280
264
  super
281
265
  end
266
+
267
+ def live_status_capabilities
268
+ {
269
+ supports_coordination: true, # Compact can work with coordinated output
270
+ output_frequency: :low, # Outputs infrequently, mainly summaries
271
+ requires_tty: false, # Works without TTY
272
+ }
273
+ end
282
274
  end
283
275
  end
284
276
  end
@@ -1,19 +1,22 @@
1
1
  # lib/tryouts/cli/formatters/factory.rb
2
2
 
3
+ require_relative '../tty_detector'
4
+
3
5
  class Tryouts
4
6
  class CLI
5
7
  # Factory for creating formatters and output managers
6
8
  class FormatterFactory
7
9
  def self.create_output_manager(options = {})
8
10
  formatter = create_formatter(options)
9
- OutputManager.new(formatter)
11
+ OutputManager.new(formatter, options)
10
12
  end
11
13
 
12
14
  def self.create_formatter(options = {})
13
15
  # Map boolean flags to format symbols if format not explicitly set
14
16
  format = options[:format]&.to_sym || determine_format_from_flags(options)
15
17
 
16
- case format
18
+ # Create base formatter first
19
+ base_formatter = case format
17
20
  when :verbose
18
21
  if options[:fails_only]
19
22
  VerboseFailsFormatter.new(options)
@@ -35,6 +38,9 @@ class Tryouts
35
38
  else
36
39
  CompactFormatter.new(options) # Default to compact
37
40
  end
41
+
42
+ # Return base formatter - live status is now handled by OutputManager/LiveStatusManager
43
+ base_formatter
38
44
  end
39
45
 
40
46
  class << self