tryouts 3.1.2 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/tryouts/cli/formatters/base.rb +109 -47
- data/lib/tryouts/cli/formatters/compact.rb +98 -107
- data/lib/tryouts/cli/formatters/factory.rb +8 -2
- data/lib/tryouts/cli/formatters/live_status_manager.rb +138 -0
- data/lib/tryouts/cli/formatters/output_manager.rb +78 -66
- data/lib/tryouts/cli/formatters/quiet.rb +53 -101
- data/lib/tryouts/cli/formatters/test_run_state.rb +122 -0
- data/lib/tryouts/cli/formatters/tty_status_display.rb +273 -0
- data/lib/tryouts/cli/formatters/verbose.rb +99 -103
- data/lib/tryouts/cli/formatters.rb +3 -0
- data/lib/tryouts/cli/opts.rb +17 -8
- data/lib/tryouts/cli/tty_detector.rb +92 -0
- data/lib/tryouts/failure_collector.rb +109 -0
- data/lib/tryouts/test_batch.rb +9 -5
- data/lib/tryouts/test_runner.rb +11 -0
- data/lib/tryouts/version.rb +1 -1
- metadata +78 -3
@@ -0,0 +1,273 @@
|
|
1
|
+
# lib/tryouts/cli/formatters/tty_status_display.rb
|
2
|
+
|
3
|
+
require 'tty-cursor'
|
4
|
+
require 'tty-screen'
|
5
|
+
require 'pastel'
|
6
|
+
require 'io/console'
|
7
|
+
|
8
|
+
class Tryouts
|
9
|
+
class CLI
|
10
|
+
# Encapsulates TTY manipulation for live status display with fallback
|
11
|
+
class TTYStatusDisplay
|
12
|
+
STATUS_LINES = 5 # Lines reserved for fixed status display (4 content + 1 separator)
|
13
|
+
|
14
|
+
def initialize(io = $stdout, options = {})
|
15
|
+
@io = io
|
16
|
+
@available = check_tty_availability
|
17
|
+
@show_debug = options.fetch(:debug, false)
|
18
|
+
@cleanup_registered = false
|
19
|
+
|
20
|
+
return unless @available
|
21
|
+
|
22
|
+
@cursor = TTY::Cursor
|
23
|
+
@pastel = Pastel.new
|
24
|
+
@status_active = false
|
25
|
+
@original_cursor_position = nil
|
26
|
+
|
27
|
+
register_cleanup_handlers
|
28
|
+
end
|
29
|
+
|
30
|
+
def available?
|
31
|
+
@available
|
32
|
+
end
|
33
|
+
|
34
|
+
def reserve_status_area
|
35
|
+
return unless @available && !@status_active
|
36
|
+
|
37
|
+
with_terminal_safety do
|
38
|
+
# Store original cursor position if possible
|
39
|
+
@original_cursor_position = get_cursor_position
|
40
|
+
|
41
|
+
# Simply print empty lines to push content up and make room at bottom
|
42
|
+
STATUS_LINES.times { @io.print "\n" }
|
43
|
+
|
44
|
+
@status_active = true
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def write_scrolling(text)
|
49
|
+
return @io.print(text) unless @available
|
50
|
+
|
51
|
+
# Always write content normally - the status will be updated separately
|
52
|
+
@io.print text
|
53
|
+
end
|
54
|
+
|
55
|
+
def update_status(state)
|
56
|
+
return unless @available && @status_active
|
57
|
+
|
58
|
+
with_terminal_safety do
|
59
|
+
# Move to status area (bottom of screen) and update in place
|
60
|
+
current_row, current_col = get_cursor_position
|
61
|
+
|
62
|
+
# Move to status area at bottom
|
63
|
+
@io.print @cursor.move_to(0, TTY::Screen.height - STATUS_LINES + 1)
|
64
|
+
|
65
|
+
# Clear and write status content
|
66
|
+
STATUS_LINES.times do
|
67
|
+
@io.print @cursor.clear_line
|
68
|
+
@io.print @cursor.down(1) unless STATUS_LINES == 1
|
69
|
+
end
|
70
|
+
|
71
|
+
# Move back to start of status area and write content
|
72
|
+
@io.print @cursor.move_to(0, TTY::Screen.height - STATUS_LINES + 1)
|
73
|
+
write_status_content(state)
|
74
|
+
|
75
|
+
# Move cursor back to where content should continue (just before status area)
|
76
|
+
@io.print @cursor.move_to(0, TTY::Screen.height - STATUS_LINES)
|
77
|
+
@io.flush
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def clear_status_area
|
82
|
+
return unless @available && @status_active
|
83
|
+
|
84
|
+
with_terminal_safety do
|
85
|
+
# Move to status area and clear it completely - start from first status line
|
86
|
+
@io.print @cursor.move_to(0, TTY::Screen.height - STATUS_LINES + 1)
|
87
|
+
|
88
|
+
# Clear each line thoroughly
|
89
|
+
STATUS_LINES.times do |i|
|
90
|
+
@io.print @cursor.clear_line
|
91
|
+
@io.print @cursor.down(1) if i < STATUS_LINES - 1 # Don't go down after last line
|
92
|
+
end
|
93
|
+
|
94
|
+
# Move cursor to a clean area for final output - position it well above the cleared area
|
95
|
+
# This ensures no interference with the cleared status content
|
96
|
+
target_row = TTY::Screen.height - STATUS_LINES - 2 # Leave some buffer space
|
97
|
+
@io.print @cursor.move_to(0, target_row)
|
98
|
+
@io.print "\n" # Add a clean line for final output to start
|
99
|
+
@io.flush
|
100
|
+
end
|
101
|
+
|
102
|
+
@status_active = false
|
103
|
+
end
|
104
|
+
|
105
|
+
# Explicit cleanup method for manual invocation
|
106
|
+
def cleanup!
|
107
|
+
return unless @available
|
108
|
+
|
109
|
+
with_terminal_safety do
|
110
|
+
if @status_active
|
111
|
+
# Clear the status area completely
|
112
|
+
@io.print @cursor.move_to(0, TTY::Screen.height - STATUS_LINES + 1)
|
113
|
+
STATUS_LINES.times do |i|
|
114
|
+
@io.print @cursor.clear_line
|
115
|
+
@io.print @cursor.down(1) if i < STATUS_LINES - 1
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Reset cursor to a safe position (start of line below cleared area)
|
120
|
+
@io.print @cursor.move_to(0, TTY::Screen.height - STATUS_LINES)
|
121
|
+
@io.print "\n"
|
122
|
+
@io.flush
|
123
|
+
end
|
124
|
+
|
125
|
+
@status_active = false
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def get_cursor_position
|
131
|
+
# Simple approximation - in a real terminal this would query cursor position
|
132
|
+
# For now, return reasonable defaults
|
133
|
+
[10, 0]
|
134
|
+
end
|
135
|
+
|
136
|
+
def check_tty_availability
|
137
|
+
# Check if we have a real TTY
|
138
|
+
return false unless @io.respond_to?(:tty?) && @io.tty?
|
139
|
+
|
140
|
+
# Check if we can get screen dimensions
|
141
|
+
return false unless TTY::Screen.width > 0 && TTY::Screen.height > STATUS_LINES + 5
|
142
|
+
|
143
|
+
# Check if TERM environment variable suggests terminal capabilities
|
144
|
+
term = ENV.fetch('TERM', nil)
|
145
|
+
return false if term.nil? || term == 'dumb'
|
146
|
+
|
147
|
+
# Check if we're likely in a CI environment (common CI env vars)
|
148
|
+
ci_vars = %w[CI CONTINUOUS_INTEGRATION BUILD_NUMBER JENKINS_URL GITHUB_ACTIONS]
|
149
|
+
return false if ci_vars.any? { |var| ENV[var] }
|
150
|
+
|
151
|
+
true
|
152
|
+
rescue StandardError => ex
|
153
|
+
# If any TTY detection fails, assume not available
|
154
|
+
if @show_debug
|
155
|
+
@io.puts "TTY detection failed: #{ex.message}"
|
156
|
+
end
|
157
|
+
false
|
158
|
+
end
|
159
|
+
|
160
|
+
def write_status_content(state)
|
161
|
+
return unless @available
|
162
|
+
|
163
|
+
# Line 1: Empty separator line
|
164
|
+
@io.print "\n"
|
165
|
+
|
166
|
+
# Line 2: Current progress
|
167
|
+
if state.current_file
|
168
|
+
current_info = "Running: #{state.current_file}"
|
169
|
+
current_info += " → #{state.current_test}" if state.current_test
|
170
|
+
@io.print current_info
|
171
|
+
else
|
172
|
+
@io.print 'Ready'
|
173
|
+
end
|
174
|
+
@io.print "\n"
|
175
|
+
|
176
|
+
# Line 3: Test counts
|
177
|
+
parts = []
|
178
|
+
parts << @pastel.green("#{state.passed} passed") if state.passed > 0
|
179
|
+
parts << @pastel.red("#{state.failed} failed") if state.failed > 0
|
180
|
+
parts << @pastel.yellow("#{state.errors} errors") if state.errors > 0
|
181
|
+
|
182
|
+
if parts.any?
|
183
|
+
@io.print "Tests: #{parts.join(', ')}"
|
184
|
+
else
|
185
|
+
@io.print 'Tests: 0 run'
|
186
|
+
end
|
187
|
+
@io.print "\n"
|
188
|
+
|
189
|
+
# Line 4: File progress
|
190
|
+
files_info = "Files: #{state.files_completed}"
|
191
|
+
files_info += "/#{state.total_files}" if state.total_files > 0
|
192
|
+
files_info += ' completed'
|
193
|
+
@io.print files_info
|
194
|
+
@io.print "\n"
|
195
|
+
|
196
|
+
# Line 5: Timing
|
197
|
+
@io.print "Time: #{format_timing(state.elapsed_time)}"
|
198
|
+
end
|
199
|
+
|
200
|
+
def format_timing(elapsed_time)
|
201
|
+
if elapsed_time < 0.001
|
202
|
+
"#{(elapsed_time * 1_000_000).round}μs"
|
203
|
+
elsif elapsed_time < 1
|
204
|
+
"#{(elapsed_time * 1000).round}ms"
|
205
|
+
else
|
206
|
+
"#{elapsed_time.round(2)}s"
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Register cleanup handlers for various termination scenarios
|
211
|
+
def register_cleanup_handlers
|
212
|
+
return if @cleanup_registered
|
213
|
+
|
214
|
+
# Handle common termination signals
|
215
|
+
%w[INT TERM QUIT HUP].each do |signal|
|
216
|
+
begin
|
217
|
+
Signal.trap(signal) do
|
218
|
+
cleanup!
|
219
|
+
# Re-raise the signal with default handler
|
220
|
+
Signal.trap(signal, 'DEFAULT')
|
221
|
+
Process.kill(signal, Process.pid)
|
222
|
+
end
|
223
|
+
rescue ArgumentError
|
224
|
+
# Signal not supported on this platform, skip it
|
225
|
+
debug_log("Signal #{signal} not supported, skipping cleanup handler")
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# Register at_exit handler as final fallback
|
230
|
+
at_exit { cleanup! }
|
231
|
+
|
232
|
+
@cleanup_registered = true
|
233
|
+
debug_log('TTY cleanup handlers registered')
|
234
|
+
end
|
235
|
+
|
236
|
+
# Wrap terminal operations with exception handling
|
237
|
+
def with_terminal_safety
|
238
|
+
yield
|
239
|
+
rescue StandardError => e
|
240
|
+
debug_log("Terminal operation failed: #{e.message}")
|
241
|
+
# Attempt basic cleanup on any terminal operation failure
|
242
|
+
begin
|
243
|
+
@io.print "\n" if @io.respond_to?(:print)
|
244
|
+
@io.flush if @io.respond_to?(:flush)
|
245
|
+
rescue StandardError
|
246
|
+
# If even basic cleanup fails, there's nothing more we can do
|
247
|
+
end
|
248
|
+
@status_active = false
|
249
|
+
end
|
250
|
+
|
251
|
+
def debug_log(message)
|
252
|
+
return unless @show_debug
|
253
|
+
|
254
|
+
# Use stderr to avoid interfering with TTY operations on stdout
|
255
|
+
$stderr.puts "TTY DEBUG: #{message}"
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# No-op implementation for when TTY is not available
|
260
|
+
class NoOpStatusDisplay
|
261
|
+
def initialize(io = $stdout, _options = {})
|
262
|
+
@io = io
|
263
|
+
end
|
264
|
+
|
265
|
+
def available? = false
|
266
|
+
def reserve_status_area; end
|
267
|
+
def write_scrolling(text) = @io.print(text)
|
268
|
+
def update_status(state); end
|
269
|
+
def clear_status_area; end
|
270
|
+
def cleanup!; end # No-op cleanup for consistency
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
@@ -7,117 +7,119 @@ class Tryouts
|
|
7
7
|
include FormatterInterface
|
8
8
|
|
9
9
|
def initialize(options = {})
|
10
|
+
super
|
10
11
|
@line_width = options.fetch(:line_width, 70)
|
11
12
|
@show_passed = options.fetch(:show_passed, true)
|
12
13
|
@show_debug = options.fetch(:debug, false)
|
13
14
|
@show_trace = options.fetch(:trace, false)
|
14
|
-
@current_indent = 0
|
15
15
|
end
|
16
16
|
|
17
17
|
# Phase-level output
|
18
|
-
def phase_header(message,
|
19
|
-
return if
|
20
|
-
|
21
|
-
separators = [
|
22
|
-
{ char: '=', width: @line_width }, # Major phases
|
23
|
-
{ char: '-', width: @line_width - 10 }, # Sub-phases
|
24
|
-
{ char: '.', width: @line_width - 20 }, # Details
|
25
|
-
{ char: '~', width: @line_width - 30 }, # Minor items
|
26
|
-
]
|
27
|
-
|
28
|
-
config = separators[level] || separators.last
|
18
|
+
def phase_header(message, file_count: nil)
|
19
|
+
return if message.include?('EXECUTING') # Skip execution phase headers
|
29
20
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
output = case level
|
34
|
-
when 0, 1
|
35
|
-
[separator_line, header_line, separator_line]
|
36
|
-
else
|
37
|
-
[header_line, separator_line]
|
38
|
-
end
|
21
|
+
header_line = message.center(@line_width)
|
22
|
+
separator_line = '=' * @line_width
|
39
23
|
|
40
|
-
|
41
|
-
|
42
|
-
|
24
|
+
puts(separator_line)
|
25
|
+
puts(header_line)
|
26
|
+
puts(separator_line)
|
43
27
|
end
|
44
28
|
|
45
29
|
# File-level operations
|
46
|
-
def file_start(file_path,
|
47
|
-
puts
|
30
|
+
def file_start(file_path, context_info: {})
|
31
|
+
puts(file_header_visual(file_path))
|
48
32
|
end
|
49
33
|
|
50
|
-
def
|
51
|
-
# No output in verbose mode
|
52
|
-
end
|
53
|
-
|
54
|
-
def file_parsed(_file_path, _test_count, setup_present: false, teardown_present: false)
|
34
|
+
def file_parsed(_file_path, test_count:, setup_present: false, teardown_present: false)
|
55
35
|
message = ''
|
56
36
|
|
57
|
-
extras
|
37
|
+
extras = []
|
58
38
|
extras << 'setup' if setup_present
|
59
39
|
extras << 'teardown' if teardown_present
|
60
40
|
message += " (#{extras.join(', ')})" unless extras.empty?
|
61
41
|
|
62
|
-
puts
|
42
|
+
puts(indent_text(message, 2))
|
63
43
|
end
|
64
44
|
|
65
|
-
def file_execution_start(_file_path, test_count
|
45
|
+
def file_execution_start(_file_path, test_count:, context_mode:)
|
66
46
|
message = "Running #{test_count} tests with #{context_mode} context"
|
67
|
-
puts
|
47
|
+
puts(indent_text(message, 1))
|
68
48
|
end
|
69
49
|
|
70
|
-
# Summary operations
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
50
|
+
# Summary operations - show detailed failure summary
|
51
|
+
def batch_summary(failure_collector)
|
52
|
+
return unless failure_collector.any_failures?
|
53
|
+
|
54
|
+
puts
|
55
|
+
write '=' * 50
|
56
|
+
puts Console.color(:red, 'Failed Tests:')
|
57
|
+
|
58
|
+
failure_collector.failures_by_file.each do |file_path, failures|
|
59
|
+
failures.each_with_index do |failure, index|
|
60
|
+
pretty_path = Console.pretty_path(file_path)
|
61
|
+
|
62
|
+
# Include line number with file path for easy copying/clicking
|
63
|
+
if failure.line_number > 0
|
64
|
+
location = "#{pretty_path}:#{failure.line_number}"
|
65
|
+
else
|
66
|
+
location = pretty_path
|
67
|
+
end
|
68
|
+
|
69
|
+
puts
|
70
|
+
puts Console.color(:yellow, location)
|
71
|
+
puts " #{index + 1}) #{failure.description}"
|
72
|
+
puts " #{Console.color(:red, 'Failure:')} #{failure.failure_reason}"
|
73
|
+
|
74
|
+
# Show source context in verbose mode
|
75
|
+
if failure.source_context.any?
|
76
|
+
puts " #{Console.color(:cyan, 'Source:')}"
|
77
|
+
failure.source_context.each do |line|
|
78
|
+
puts " #{line.strip}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
puts
|
82
|
+
end
|
83
|
+
end
|
75
84
|
end
|
76
85
|
|
77
|
-
def file_result(_file_path, total_tests
|
86
|
+
def file_result(_file_path, total_tests:, failed_count:, error_count:, elapsed_time: nil)
|
78
87
|
issues_count = failed_count + error_count
|
79
88
|
passed_count = total_tests - issues_count
|
80
|
-
details
|
81
|
-
|
82
|
-
]
|
89
|
+
details = ["#{passed_count} passed"]
|
90
|
+
|
83
91
|
puts
|
84
92
|
if issues_count > 0
|
85
93
|
details << "#{failed_count} failed" if failed_count > 0
|
86
94
|
details << "#{error_count} errors" if error_count > 0
|
87
95
|
details_str = details.join(', ')
|
88
|
-
color
|
96
|
+
color = :red
|
89
97
|
|
90
98
|
time_str = elapsed_time ? " (#{elapsed_time.round(2)}s)" : ''
|
91
|
-
message
|
99
|
+
message = "✗ Out of #{total_tests} tests: #{details_str}#{time_str}"
|
92
100
|
puts indent_text(Console.color(color, message), 2)
|
93
101
|
else
|
94
102
|
message = "#{total_tests} tests passed"
|
95
|
-
color
|
103
|
+
color = :green
|
96
104
|
puts indent_text(Console.color(color, "✓ #{message}"), 2)
|
97
105
|
end
|
98
106
|
|
99
107
|
return unless elapsed_time
|
100
108
|
|
101
109
|
time_msg = "Completed in #{format_timing(elapsed_time).strip.tr('()', '')}"
|
102
|
-
|
103
110
|
puts indent_text(Console.color(:dim, time_msg), 2)
|
104
111
|
end
|
105
112
|
|
106
113
|
# Test-level operations
|
107
|
-
def test_start(test_case
|
108
|
-
desc
|
109
|
-
desc
|
114
|
+
def test_start(test_case:, index:, total:)
|
115
|
+
desc = test_case.description.to_s
|
116
|
+
desc = 'Unnamed test' if desc.empty?
|
110
117
|
message = "Test #{index}/#{total}: #{desc}"
|
111
118
|
puts indent_text(Console.color(:dim, message), 2)
|
112
119
|
end
|
113
120
|
|
114
|
-
def test_end(_test_case, _index, _total)
|
115
|
-
# No output in verbose mode
|
116
|
-
end
|
117
|
-
|
118
121
|
def test_result(result_packet)
|
119
122
|
should_show = @show_passed || !result_packet.passed?
|
120
|
-
|
121
123
|
return unless should_show
|
122
124
|
|
123
125
|
status_line = case result_packet.status
|
@@ -134,7 +136,7 @@ class Tryouts
|
|
134
136
|
end
|
135
137
|
|
136
138
|
test_case = result_packet.test_case
|
137
|
-
location
|
139
|
+
location = "#{Console.pretty_path(test_case.path)}:#{test_case.first_expectation_line + 1}"
|
138
140
|
puts
|
139
141
|
puts indent_text("#{status_line} @ #{location}", 2)
|
140
142
|
|
@@ -150,7 +152,7 @@ class Tryouts
|
|
150
152
|
end
|
151
153
|
end
|
152
154
|
|
153
|
-
def test_output(
|
155
|
+
def test_output(test_case:, output_text:, result_packet:)
|
154
156
|
return if output_text.nil? || output_text.strip.empty?
|
155
157
|
|
156
158
|
puts indent_text('Test Output:', 3)
|
@@ -165,7 +167,7 @@ class Tryouts
|
|
165
167
|
end
|
166
168
|
|
167
169
|
# Setup/teardown operations
|
168
|
-
def setup_start(line_range)
|
170
|
+
def setup_start(line_range:)
|
169
171
|
message = "Executing global setup (lines #{line_range.first}..#{line_range.last})"
|
170
172
|
puts indent_text(Console.color(:cyan, message), 2)
|
171
173
|
end
|
@@ -178,7 +180,7 @@ class Tryouts
|
|
178
180
|
end
|
179
181
|
end
|
180
182
|
|
181
|
-
def teardown_start(line_range)
|
183
|
+
def teardown_start(line_range:)
|
182
184
|
message = "Executing teardown (lines #{line_range.first}..#{line_range.last})"
|
183
185
|
puts indent_text(Console.color(:cyan, message), 2)
|
184
186
|
puts
|
@@ -192,21 +194,20 @@ class Tryouts
|
|
192
194
|
end
|
193
195
|
end
|
194
196
|
|
195
|
-
def grand_total(total_tests
|
197
|
+
def grand_total(total_tests:, failed_count:, error_count:, successful_files:, total_files:, elapsed_time:)
|
196
198
|
puts
|
197
199
|
puts '=' * @line_width
|
198
200
|
puts 'Grand Total:'
|
199
201
|
|
200
202
|
issues_count = failed_count + error_count
|
201
|
-
time_str
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
end
|
203
|
+
time_str = if elapsed_time < 2.0
|
204
|
+
" (#{(elapsed_time * 1000).round}ms)"
|
205
|
+
else
|
206
|
+
" (#{elapsed_time.round(2)}s)"
|
207
|
+
end
|
207
208
|
|
208
209
|
if issues_count > 0
|
209
|
-
passed
|
210
|
+
passed = [total_tests - issues_count, 0].max # Ensure passed never goes negative
|
210
211
|
details = []
|
211
212
|
details << "#{failed_count} failed" if failed_count > 0
|
212
213
|
details << "#{error_count} errors" if error_count > 0
|
@@ -215,12 +216,12 @@ class Tryouts
|
|
215
216
|
puts "#{total_tests} tests passed#{time_str}"
|
216
217
|
end
|
217
218
|
|
218
|
-
puts "Files
|
219
|
+
puts "Files: #{successful_files} of #{total_files} successful"
|
219
220
|
puts '=' * @line_width
|
220
221
|
end
|
221
222
|
|
222
223
|
# Debug and diagnostic output
|
223
|
-
def debug_info(message, level
|
224
|
+
def debug_info(message, level: 0)
|
224
225
|
return unless @show_debug
|
225
226
|
|
226
227
|
prefix = Console.color(:cyan, 'INFO ')
|
@@ -228,14 +229,14 @@ class Tryouts
|
|
228
229
|
puts indent_text("#{prefix} #{message}", level + 1)
|
229
230
|
end
|
230
231
|
|
231
|
-
def trace_info(message, level
|
232
|
+
def trace_info(message, level: 0)
|
232
233
|
return unless @show_trace
|
233
234
|
|
234
235
|
prefix = Console.color(:dim, 'TRACE')
|
235
236
|
puts indent_text("#{prefix} #{message}", level + 1)
|
236
237
|
end
|
237
238
|
|
238
|
-
def error_message(message, backtrace
|
239
|
+
def error_message(message, backtrace: nil)
|
239
240
|
error_msg = Console.color(:red, "ERROR: #{message}")
|
240
241
|
puts indent_text(error_msg, 1)
|
241
242
|
|
@@ -249,22 +250,12 @@ class Tryouts
|
|
249
250
|
puts indent_text("... (#{backtrace.length - 10} more lines)", 3) if backtrace.length > 10
|
250
251
|
end
|
251
252
|
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
case style
|
259
|
-
when :heavy
|
260
|
-
puts '=' * @line_width
|
261
|
-
when :light
|
262
|
-
puts '-' * @line_width
|
263
|
-
when :dotted
|
264
|
-
puts '.' * @line_width
|
265
|
-
else # rubocop:disable Lint/DuplicateBranch
|
266
|
-
puts '-' * @line_width
|
267
|
-
end
|
253
|
+
def live_status_capabilities
|
254
|
+
{
|
255
|
+
supports_coordination: true, # Verbose can work with coordinated output
|
256
|
+
output_frequency: :high, # Outputs frequently for each test
|
257
|
+
requires_tty: false, # Works without TTY
|
258
|
+
}
|
268
259
|
end
|
269
260
|
|
270
261
|
private
|
@@ -279,7 +270,7 @@ class Tryouts
|
|
279
270
|
puts indent_text('Exception Details:', 4)
|
280
271
|
|
281
272
|
actual_results.each_with_index do |actual, idx|
|
282
|
-
expected
|
273
|
+
expected = expected_results[idx] if expected_results && idx < expected_results.length
|
283
274
|
expectation = test_case.expectations[idx] if test_case.expectations
|
284
275
|
|
285
276
|
if expectation&.type == :exception
|
@@ -296,7 +287,7 @@ class Tryouts
|
|
296
287
|
start_line = test_case.line_range.first
|
297
288
|
|
298
289
|
test_case.source_lines.each_with_index do |line_content, index|
|
299
|
-
line_num
|
290
|
+
line_num = start_line + index
|
300
291
|
line_display = format('%3d: %s', line_num + 1, line_content)
|
301
292
|
|
302
293
|
# Highlight expectation lines by checking if this line contains any expectation syntax
|
@@ -313,7 +304,7 @@ class Tryouts
|
|
313
304
|
return if actual_results.empty?
|
314
305
|
|
315
306
|
actual_results.each_with_index do |actual, idx|
|
316
|
-
expected
|
307
|
+
expected = expected_results[idx] if expected_results && idx < expected_results.length
|
317
308
|
expected_line = test_case.expectations[idx] if test_case.expectations
|
318
309
|
|
319
310
|
if !expected.nil?
|
@@ -347,10 +338,10 @@ class Tryouts
|
|
347
338
|
end
|
348
339
|
|
349
340
|
def file_header_visual(file_path)
|
350
|
-
pretty_path
|
341
|
+
pretty_path = Console.pretty_path(file_path)
|
351
342
|
header_content = ">>>>> #{pretty_path} "
|
352
343
|
padding_length = [@line_width - header_content.length, 0].max
|
353
|
-
padding
|
344
|
+
padding = '<' * padding_length
|
354
345
|
|
355
346
|
[
|
356
347
|
indent_text('-' * @line_width, 1),
|
@@ -358,16 +349,6 @@ class Tryouts
|
|
358
349
|
indent_text('-' * @line_width, 1),
|
359
350
|
].join("\n")
|
360
351
|
end
|
361
|
-
|
362
|
-
def format_timing(elapsed_time)
|
363
|
-
if elapsed_time < 0.001
|
364
|
-
" (#{(elapsed_time * 1_000_000).round}μs)"
|
365
|
-
elsif elapsed_time < 1
|
366
|
-
" (#{(elapsed_time * 1000).round}ms)"
|
367
|
-
else
|
368
|
-
" (#{elapsed_time.round(2)}s)"
|
369
|
-
end
|
370
|
-
end
|
371
352
|
end
|
372
353
|
|
373
354
|
# Verbose formatter that only shows failures and errors
|
@@ -376,12 +357,27 @@ class Tryouts
|
|
376
357
|
super(options.merge(show_passed: false))
|
377
358
|
end
|
378
359
|
|
360
|
+
def test_output(test_case:, output_text:, result_packet:)
|
361
|
+
# Only show output for failed tests
|
362
|
+
return if result_packet.passed?
|
363
|
+
|
364
|
+
super
|
365
|
+
end
|
366
|
+
|
379
367
|
def test_result(result_packet)
|
380
368
|
# Only show failed/error tests, but with full source code
|
381
369
|
return if result_packet.passed?
|
382
370
|
|
383
371
|
super
|
384
372
|
end
|
373
|
+
|
374
|
+
def live_status_capabilities
|
375
|
+
{
|
376
|
+
supports_coordination: true, # Verbose can work with coordinated output
|
377
|
+
output_frequency: :high, # Outputs frequently for each test
|
378
|
+
requires_tty: false, # Works without TTY
|
379
|
+
}
|
380
|
+
end
|
385
381
|
end
|
386
382
|
end
|
387
383
|
end
|
@@ -4,3 +4,6 @@ require_relative 'formatters/base'
|
|
4
4
|
require_relative 'formatters/compact'
|
5
5
|
require_relative 'formatters/quiet'
|
6
6
|
require_relative 'formatters/verbose'
|
7
|
+
require_relative 'formatters/test_run_state'
|
8
|
+
require_relative 'formatters/tty_status_display'
|
9
|
+
require_relative 'formatters/factory'
|