# lib/tryouts/cli/formatters/verbose.rb
class Tryouts
class CLI
# Detailed formatter with comprehensive output and clear visual hierarchy
class VerboseFormatter
include FormatterInterface
def initialize(options = {})
super
@line_width = options.fetch(:line_width, 70)
@show_passed = options.fetch(:show_passed, true)
@show_debug = options.fetch(:debug, false)
@show_trace = options.fetch(:trace, false)
end
# Phase-level output
def phase_header(message, file_count: nil)
return if message.include?('EXECUTING') # Skip execution phase headers
header_line = message.center(@line_width)
separator_line = '=' * @line_width
puts(separator_line)
puts(header_line)
puts(separator_line)
end
# File-level operations
def file_start(file_path, context_info: {})
puts(file_header_visual(file_path))
end
def file_parsed(_file_path, test_count:, setup_present: false, teardown_present: false)
message = ''
extras = []
extras << 'setup' if setup_present
extras << 'teardown' if teardown_present
message += " (#{extras.join(', ')})" unless extras.empty?
puts(indent_text(message, 2))
end
def file_execution_start(_file_path, test_count:, context_mode:)
message = "Running #{test_count} tests with #{context_mode} context"
puts(indent_text(message, 1))
end
# Summary operations - show detailed failure summary
def batch_summary(failure_collector)
return unless failure_collector.any_failures?
puts
write '=' * 50
puts Console.color(:red, 'Failed Tests:')
failure_collector.failures_by_file.each do |file_path, failures|
failures.each_with_index do |failure, index|
pretty_path = Console.pretty_path(file_path)
# Include line number with file path for easy copying/clicking
if failure.line_number > 0
location = "#{pretty_path}:#{failure.line_number}"
else
location = pretty_path
end
puts
puts Console.color(:yellow, location)
puts " #{index + 1}) #{failure.description}"
puts " #{Console.color(:red, 'Failure:')} #{failure.failure_reason}"
# Show source context in verbose mode
if failure.source_context.any?
puts " #{Console.color(:cyan, 'Source:')}"
failure.source_context.each do |line|
puts " #{line.strip}"
end
end
puts
end
end
end
def file_result(_file_path, total_tests:, failed_count:, error_count:, elapsed_time: nil)
issues_count = failed_count + error_count
passed_count = total_tests - issues_count
details = ["#{passed_count} passed"]
puts
if issues_count > 0
details << "#{failed_count} failed" if failed_count > 0
details << "#{error_count} errors" if error_count > 0
details_str = details.join(', ')
color = :red
time_str = elapsed_time ? " (#{elapsed_time.round(2)}s)" : ''
message = "✗ Out of #{total_tests} tests: #{details_str}#{time_str}"
puts indent_text(Console.color(color, message), 2)
else
message = "#{total_tests} tests passed"
color = :green
puts indent_text(Console.color(color, "✓ #{message}"), 2)
end
return unless elapsed_time
time_msg = "Completed in #{format_timing(elapsed_time).strip.tr('()', '')}"
puts indent_text(Console.color(:dim, time_msg), 2)
end
# Test-level operations
def test_start(test_case:, index:, total:)
desc = test_case.description.to_s
desc = 'Unnamed test' if desc.empty?
message = "Test #{index}/#{total}: #{desc}"
puts indent_text(Console.color(:dim, message), 2)
end
def test_result(result_packet)
should_show = @show_passed || !result_packet.passed?
return unless should_show
status_line = case result_packet.status
when :passed
Console.color(:green, 'PASSED')
when :failed
Console.color(:red, 'FAILED')
when :error
Console.color(:red, 'ERROR')
when :skipped
Console.color(:yellow, 'SKIPPED')
else
'UNKNOWN'
end
test_case = result_packet.test_case
location = "#{Console.pretty_path(test_case.path)}:#{test_case.first_expectation_line + 1}"
puts
puts indent_text("#{status_line} @ #{location}", 2)
# Show source code for verbose mode
show_test_source_code(test_case)
# Show failure details for failed tests
if result_packet.failed? || result_packet.error?
show_failure_details(test_case, result_packet.actual_results, result_packet.expected_results)
# Show exception details for passed exception expectations
elsif result_packet.passed? && has_exception_expectations?(test_case)
show_exception_details(test_case, result_packet.actual_results, result_packet.expected_results)
end
end
def test_output(test_case:, output_text:, result_packet:)
return if output_text.nil? || output_text.strip.empty?
puts indent_text('Test Output:', 3)
puts indent_text(Console.color(:dim, '--- BEGIN OUTPUT ---'), 3)
output_text.lines.each do |line|
puts indent_text(line.chomp, 4)
end
puts indent_text(Console.color(:dim, '--- END OUTPUT ---'), 3)
puts
end
# Setup/teardown operations
def setup_start(line_range:)
message = "Executing global setup (lines #{line_range.first}..#{line_range.last})"
puts indent_text(Console.color(:cyan, message), 2)
end
def setup_output(output_text)
return if output_text.strip.empty?
output_text.lines.each do |line|
puts indent_text(line.chomp, 0)
end
end
def teardown_start(line_range:)
message = "Executing teardown (lines #{line_range.first}..#{line_range.last})"
puts indent_text(Console.color(:cyan, message), 2)
puts
end
def teardown_output(output_text)
return if output_text.strip.empty?
output_text.lines.each do |line|
puts indent_text(line.chomp, 0)
end
end
def grand_total(total_tests:, failed_count:, error_count:, successful_files:, total_files:, elapsed_time:)
puts
puts '=' * @line_width
puts 'Grand Total:'
issues_count = failed_count + error_count
time_str = if elapsed_time < 2.0
" (#{(elapsed_time * 1000).round}ms)"
else
" (#{elapsed_time.round(2)}s)"
end
if issues_count > 0
passed = [total_tests - issues_count, 0].max # Ensure passed never goes negative
details = []
details << "#{failed_count} failed" if failed_count > 0
details << "#{error_count} errors" if error_count > 0
puts "#{details.join(', ')}, #{passed} passed#{time_str}"
else
puts "#{total_tests} tests passed#{time_str}"
end
puts "Files: #{successful_files} of #{total_files} successful"
puts '=' * @line_width
end
# Debug and diagnostic output
def debug_info(message, level: 0)
return unless @show_debug
prefix = Console.color(:cyan, 'INFO ')
puts
puts indent_text("#{prefix} #{message}", level + 1)
end
def trace_info(message, level: 0)
return unless @show_trace
prefix = Console.color(:dim, 'TRACE')
puts indent_text("#{prefix} #{message}", level + 1)
end
def error_message(message, backtrace: nil)
error_msg = Console.color(:red, "ERROR: #{message}")
puts indent_text(error_msg, 1)
return unless backtrace && @show_debug
puts indent_text('Details:', 2)
# Show first 10 lines of backtrace to avoid overwhelming output
backtrace.first(10).each do |line|
puts indent_text(line, 3)
end
puts indent_text("... (#{backtrace.length - 10} more lines)", 3) if backtrace.length > 10
end
def live_status_capabilities
{
supports_coordination: true, # Verbose can work with coordinated output
output_frequency: :high, # Outputs frequently for each test
requires_tty: false, # Works without TTY
}
end
private
def has_exception_expectations?(test_case)
test_case.expectations.any? { |exp| exp.type == :exception }
end
def show_exception_details(test_case, actual_results, expected_results = [])
return if actual_results.empty?
puts indent_text('Exception Details:', 4)
actual_results.each_with_index do |actual, idx|
expected = expected_results[idx] if expected_results && idx < expected_results.length
expectation = test_case.expectations[idx] if test_case.expectations
if expectation&.type == :exception
puts indent_text("Caught: #{Console.color(:blue, actual.inspect)}", 5)
puts indent_text("Expectation: #{Console.color(:green, expectation.content)}", 5)
puts indent_text("Result: #{Console.color(:green, expected.inspect)}", 5) if expected
end
end
puts
end
def show_test_source_code(test_case)
# Use pre-captured source lines from parsing
start_line = test_case.line_range.first
test_case.source_lines.each_with_index do |line_content, index|
line_num = start_line + index
line_display = format('%3d: %s', line_num + 1, line_content)
# Highlight expectation lines by checking if this line contains any expectation syntax
if line_content.match?(%r{^\s*#\s*=(!|<|=|/=|\||:|~|%|\d+)?>\s*})
line_display = Console.color(:yellow, line_display)
end
puts indent_text(line_display, 4)
end
puts
end
def show_failure_details(test_case, actual_results, expected_results = [])
return if actual_results.empty?
actual_results.each_with_index do |actual, idx|
expected = expected_results[idx] if expected_results && idx < expected_results.length
expected_line = test_case.expectations[idx] if test_case.expectations
if !expected.nil?
# Use the evaluated expected value from the evaluator
puts indent_text("Expected: #{Console.color(:green, expected.inspect)}", 4)
puts indent_text("Actual: #{Console.color(:red, actual.inspect)}", 4)
elsif expected_line && !expected_results.empty?
# Only show raw expectation content if we have expected_results (non-error case)
puts indent_text("Expected: #{Console.color(:green, expected_line.content)}", 4)
puts indent_text("Actual: #{Console.color(:red, actual.inspect)}", 4)
else
# For error cases (empty expected_results), just show the error
puts indent_text("Error: #{Console.color(:red, actual.inspect)}", 4)
end
# Show difference if both are strings
if !expected.nil? && actual.is_a?(String) && expected.is_a?(String)
show_string_diff(expected, actual)
end
puts
end
end
def show_string_diff(expected, actual)
return if expected == actual
puts indent_text('Difference:', 4)
puts indent_text("- #{Console.color(:red, actual)}", 5)
puts indent_text("+ #{Console.color(:green, expected)}", 5)
end
def file_header_visual(file_path)
pretty_path = Console.pretty_path(file_path)
header_content = ">>>>> #{pretty_path} "
padding_length = [@line_width - header_content.length, 0].max
padding = '<' * padding_length
[
indent_text('-' * @line_width, 1),
indent_text(header_content + padding, 1),
indent_text('-' * @line_width, 1),
].join("\n")
end
end
# Verbose formatter that only shows failures and errors
class VerboseFailsFormatter < VerboseFormatter
def initialize(options = {})
super(options.merge(show_passed: false))
end
def test_output(test_case:, output_text:, result_packet:)
# Only show output for failed tests
return if result_packet.passed?
super
end
def test_result(result_packet)
# Only show failed/error tests, but with full source code
return if result_packet.passed?
super
end
def live_status_capabilities
{
supports_coordination: true, # Verbose can work with coordinated output
output_frequency: :high, # Outputs frequently for each test
requires_tty: false, # Works without TTY
}
end
end
end
end