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.
- checksums.yaml +4 -4
- data/exe/try +3 -3
- data/lib/tryouts/cli/formatters/base.rb +108 -48
- data/lib/tryouts/cli/formatters/compact.rb +97 -105
- 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 +54 -102
- 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 +103 -105
- 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/console.rb +1 -1
- data/lib/tryouts/expectation_evaluators/boolean.rb +1 -1
- data/lib/tryouts/expectation_evaluators/exception.rb +2 -2
- data/lib/tryouts/expectation_evaluators/expectation_result.rb +3 -3
- data/lib/tryouts/expectation_evaluators/false.rb +1 -1
- data/lib/tryouts/expectation_evaluators/intentional_failure.rb +2 -2
- data/lib/tryouts/expectation_evaluators/output.rb +6 -6
- data/lib/tryouts/expectation_evaluators/performance_time.rb +3 -3
- data/lib/tryouts/expectation_evaluators/regex_match.rb +2 -2
- data/lib/tryouts/expectation_evaluators/regular.rb +1 -1
- data/lib/tryouts/expectation_evaluators/result_type.rb +1 -1
- data/lib/tryouts/expectation_evaluators/true.rb +2 -2
- data/lib/tryouts/failure_collector.rb +109 -0
- data/lib/tryouts/prism_parser.rb +17 -17
- data/lib/tryouts/test_batch.rb +9 -5
- data/lib/tryouts/test_case.rb +4 -4
- data/lib/tryouts/test_runner.rb +12 -1
- data/lib/tryouts/version.rb +1 -1
- data/lib/tryouts.rb +0 -9
- metadata +21 -22
@@ -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
|
48
|
-
end
|
49
|
-
|
50
|
-
def file_end(_file_path, _context_info = {})
|
51
|
-
# No output in verbose mode
|
30
|
+
def file_start(file_path, context_info: {})
|
31
|
+
puts(file_header_visual(file_path))
|
52
32
|
end
|
53
33
|
|
54
|
-
def file_parsed(_file_path,
|
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
|
-
"#{passed_count} passed",
|
82
|
-
]
|
89
|
+
details = ["#{passed_count} passed"]
|
83
90
|
|
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
|
@@ -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,9 +180,10 @@ 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)
|
186
|
+
puts
|
184
187
|
end
|
185
188
|
|
186
189
|
def teardown_output(output_text)
|
@@ -191,21 +194,20 @@ class Tryouts
|
|
191
194
|
end
|
192
195
|
end
|
193
196
|
|
194
|
-
def grand_total(total_tests
|
197
|
+
def grand_total(total_tests:, failed_count:, error_count:, successful_files:, total_files:, elapsed_time:)
|
195
198
|
puts
|
196
199
|
puts '=' * @line_width
|
197
200
|
puts 'Grand Total:'
|
198
201
|
|
199
202
|
issues_count = failed_count + error_count
|
200
|
-
time_str
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
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
|
206
208
|
|
207
209
|
if issues_count > 0
|
208
|
-
passed
|
210
|
+
passed = [total_tests - issues_count, 0].max # Ensure passed never goes negative
|
209
211
|
details = []
|
210
212
|
details << "#{failed_count} failed" if failed_count > 0
|
211
213
|
details << "#{error_count} errors" if error_count > 0
|
@@ -214,12 +216,12 @@ class Tryouts
|
|
214
216
|
puts "#{total_tests} tests passed#{time_str}"
|
215
217
|
end
|
216
218
|
|
217
|
-
puts "Files
|
219
|
+
puts "Files: #{successful_files} of #{total_files} successful"
|
218
220
|
puts '=' * @line_width
|
219
221
|
end
|
220
222
|
|
221
223
|
# Debug and diagnostic output
|
222
|
-
def debug_info(message, level
|
224
|
+
def debug_info(message, level: 0)
|
223
225
|
return unless @show_debug
|
224
226
|
|
225
227
|
prefix = Console.color(:cyan, 'INFO ')
|
@@ -227,14 +229,14 @@ class Tryouts
|
|
227
229
|
puts indent_text("#{prefix} #{message}", level + 1)
|
228
230
|
end
|
229
231
|
|
230
|
-
def trace_info(message, level
|
232
|
+
def trace_info(message, level: 0)
|
231
233
|
return unless @show_trace
|
232
234
|
|
233
235
|
prefix = Console.color(:dim, 'TRACE')
|
234
236
|
puts indent_text("#{prefix} #{message}", level + 1)
|
235
237
|
end
|
236
238
|
|
237
|
-
def error_message(message, backtrace
|
239
|
+
def error_message(message, backtrace: nil)
|
238
240
|
error_msg = Console.color(:red, "ERROR: #{message}")
|
239
241
|
puts indent_text(error_msg, 1)
|
240
242
|
|
@@ -248,22 +250,12 @@ class Tryouts
|
|
248
250
|
puts indent_text("... (#{backtrace.length - 10} more lines)", 3) if backtrace.length > 10
|
249
251
|
end
|
250
252
|
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
case style
|
258
|
-
when :heavy
|
259
|
-
puts '=' * @line_width
|
260
|
-
when :light
|
261
|
-
puts '-' * @line_width
|
262
|
-
when :dotted
|
263
|
-
puts '.' * @line_width
|
264
|
-
else # rubocop:disable Lint/DuplicateBranch
|
265
|
-
puts '-' * @line_width
|
266
|
-
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
|
+
}
|
267
259
|
end
|
268
260
|
|
269
261
|
private
|
@@ -295,11 +287,11 @@ class Tryouts
|
|
295
287
|
start_line = test_case.line_range.first
|
296
288
|
|
297
289
|
test_case.source_lines.each_with_index do |line_content, index|
|
298
|
-
line_num
|
290
|
+
line_num = start_line + index
|
299
291
|
line_display = format('%3d: %s', line_num + 1, line_content)
|
300
292
|
|
301
293
|
# Highlight expectation lines by checking if this line contains any expectation syntax
|
302
|
-
if line_content.match?(
|
294
|
+
if line_content.match?(%r{^\s*#\s*=(!|<|=|/=|\||:|~|%|\d+)?>\s*})
|
303
295
|
line_display = Console.color(:yellow, line_display)
|
304
296
|
end
|
305
297
|
|
@@ -312,19 +304,20 @@ class Tryouts
|
|
312
304
|
return if actual_results.empty?
|
313
305
|
|
314
306
|
actual_results.each_with_index do |actual, idx|
|
315
|
-
expected
|
307
|
+
expected = expected_results[idx] if expected_results && idx < expected_results.length
|
316
308
|
expected_line = test_case.expectations[idx] if test_case.expectations
|
317
309
|
|
318
310
|
if !expected.nil?
|
319
311
|
# Use the evaluated expected value from the evaluator
|
320
312
|
puts indent_text("Expected: #{Console.color(:green, expected.inspect)}", 4)
|
321
313
|
puts indent_text("Actual: #{Console.color(:red, actual.inspect)}", 4)
|
322
|
-
elsif expected_line
|
323
|
-
#
|
314
|
+
elsif expected_line && !expected_results.empty?
|
315
|
+
# Only show raw expectation content if we have expected_results (non-error case)
|
324
316
|
puts indent_text("Expected: #{Console.color(:green, expected_line.content)}", 4)
|
325
317
|
puts indent_text("Actual: #{Console.color(:red, actual.inspect)}", 4)
|
326
318
|
else
|
327
|
-
|
319
|
+
# For error cases (empty expected_results), just show the error
|
320
|
+
puts indent_text("Error: #{Console.color(:red, actual.inspect)}", 4)
|
328
321
|
end
|
329
322
|
|
330
323
|
# Show difference if both are strings
|
@@ -345,10 +338,10 @@ class Tryouts
|
|
345
338
|
end
|
346
339
|
|
347
340
|
def file_header_visual(file_path)
|
348
|
-
pretty_path
|
341
|
+
pretty_path = Console.pretty_path(file_path)
|
349
342
|
header_content = ">>>>> #{pretty_path} "
|
350
343
|
padding_length = [@line_width - header_content.length, 0].max
|
351
|
-
padding
|
344
|
+
padding = '<' * padding_length
|
352
345
|
|
353
346
|
[
|
354
347
|
indent_text('-' * @line_width, 1),
|
@@ -356,16 +349,6 @@ class Tryouts
|
|
356
349
|
indent_text('-' * @line_width, 1),
|
357
350
|
].join("\n")
|
358
351
|
end
|
359
|
-
|
360
|
-
def format_timing(elapsed_time)
|
361
|
-
if elapsed_time < 0.001
|
362
|
-
" (#{(elapsed_time * 1_000_000).round}μs)"
|
363
|
-
elsif elapsed_time < 1
|
364
|
-
" (#{(elapsed_time * 1000).round}ms)"
|
365
|
-
else
|
366
|
-
" (#{elapsed_time.round(2)}s)"
|
367
|
-
end
|
368
|
-
end
|
369
352
|
end
|
370
353
|
|
371
354
|
# Verbose formatter that only shows failures and errors
|
@@ -374,12 +357,27 @@ class Tryouts
|
|
374
357
|
super(options.merge(show_passed: false))
|
375
358
|
end
|
376
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
|
+
|
377
367
|
def test_result(result_packet)
|
378
368
|
# Only show failed/error tests, but with full source code
|
379
369
|
return if result_packet.passed?
|
380
370
|
|
381
371
|
super
|
382
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
|
383
381
|
end
|
384
382
|
end
|
385
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'
|