require 'tins/terminal'
require 'term/ansicolor'
begin
require 'rspec/core'
require 'rspec/core/formatters'
rescue LoadError => e
$DEBUG and warn "Caught #{e.class}: #{e}"
else
module Utils
class LineFormatter
::RSpec::Core::Formatters.register self, :start, :close,
:example_passed, :example_pending, :example_failed, :dump_summary
# The initialize method sets up the error logging system by creating a
# file handle for writing error messages and ensuring the output is
# synchronized.
#
# @param output [ IO ] the output stream to be used for logging errors
def initialize(output)
@output = output
@output.sync = true
filename = 'errors.lst'
@errors_lst = File.new(filename, 'w')
@errors_lst.sync = true
end
# The output reader method provides access to the output storage.
#
# @return [ Array ] the array containing the collected output items
attr_reader :output
# The start method initializes the error logging output.
#
# This method writes a header message to the output indicating where the
# error list will be stored, followed by a separator line made of dashes
# that spans the width of the terminal.
#
# @param _ignore [ Object ] this parameter is ignored and exists for
# interface compatibility
def start(_ignore)
output.puts "Storing error list in #{@errors_lst.path.inspect}: "
output.puts ?- * Tins::Terminal.columns
end
# The close method closes the error log file handle.
#
# This method is responsible for properly closing the file handle
# associated with the error log file, ensuring that all buffered data is
# written and system resources are released.
#
# @param _ignore [ Object ] ignored parameter, present for interface
# compatibility
def close(_ignore)
@errors_lst.close
end
# The dump_summary method outputs a formatted summary line to both the
# errors log and the standard output.
#
# This method generates a summary line using the summary_line method,
# then writes it to both the error log file and the standard output, with
# decorative lines of equals signs for visual separation in each output
# stream.
#
# @param summary [ Object ] the summary object to be processed and displayed
def dump_summary(summary)
line = summary_line(summary)
@errors_lst.puts ?= * 80, line
output.puts ?= * Tins::Terminal.columns, line
end
# The example_passed method outputs a formatted line for a passed test
# example.
#
# This method takes a test example and formats it using the internal format_line method,
# then writes the formatted output to the designated output stream.
#
# @param example [ Object ] the test example that has passed
def example_passed(example)
output.puts format_line(example)
end
# The example_pending method outputs a formatted line for a pending test
# example.
#
# This method takes a test example that is pending and formats it using
# the internal format_line method before outputting it to the console.
#
# @param example [ Object ] the test example that is pending
def example_pending(example)
output.puts format_line(example)
end
# The example_failed method handles the processing of failed test
# examples.
#
# This method manages the logging and output of failed test results by
# first writing the failure details to an error file, then formatting and
# displaying the failure information to the output stream, and finally
# dumping the full failure details.
#
# @param example [ Object ] the test example that failed
def example_failed(example)
dump_failure_to_error_file(example)
output.puts format_line(example)
dump_failure(example)
end
private
# The summary_line method formats a summary string containing test
# execution statistics.
#
# This method takes a summary object and generates a formatted string
# that includes the number of failed tests, total tests, failure
# percentage, pending tests, pending percentage, and the total execution
# duration.
#
# @param summary [ Object ] an object containing test execution statistics
#
# @return [ String ] a formatted summary string with test statistics and
# timing information
def summary_line(summary)
failure_percentage = 100 * summary.failure_count.to_f / summary.example_count
failure_percentage.nan? and failure_percentage = 0.0
pending_percentage = 100 * summary.pending_count.to_f / summary.example_count
pending_percentage.nan? and pending_percentage = 0.0
"%u of %u (%.2f %%) failed, %u pending (%.2f %%) in %.3f seconds" % [
summary.failure_count,
summary.example_count,
failure_percentage,
summary.pending_count,
pending_percentage,
summary.duration,
]
end
# The dump_failure method outputs detailed information about a test
# failure.
#
# This method displays the description of the failing example along with
# the specific failure details for debugging purposes.
#
# @param example [ Object ] the test example that failed
def dump_failure(example)
output.puts(
description(example, full: true),
dump_failure_for_example(example)
)
end
# The read_failed_line method returns an empty string after stripping
# whitespace.
#
# This method is intended to provide a placeholder implementation for
# retrieving the failed line information from a test example, currently
# returning an empty stripped string regardless of the input.
#
# @param example [ Object ] the test example object
#
# @return [ String ] an empty string with whitespace stripped
def read_failed_line(example)
''.strip
end
# The dump_failure_for_example method constructs a formatted failure
# message for a test example.
#
# This method generates a detailed error report that includes the failing
# line of code, the exception class name, and the exception message. It
# processes the execution result
# of a test example to extract relevant information about why the test failed.
#
# @param example [ Object ] the test example object containing execution results
#
# @return [ String ] a formatted string containing the failure details including
# the failing line, exception class, and exception message
def dump_failure_for_example(example)
result = ''
exception = execution_result(example).exception
exception_class_name = exception.class.name
result << "Failure/Error: #{read_failed_line(example)}\n"
result << "#{exception_class_name}:\n" unless exception_class_name =~ /RSpec/
if m = exception.message
m.to_s.split("\n").each { |line| result << " #{line}\n" }
end
result
end
# The format_backtrace method processes and formats the backtrace
# information for an example.
#
# This method extracts the backtrace from the exception associated with
# an example, applies optional filtering based on a limit, and formats
# each line using relative paths. It can optionally wrap the formatted
# backtrace with folding markers when requested.
#
# @param example [ Object ] the example object containing the exception with backtrace
# @param folding [ TrueClass, FalseClass ] whether to wrap the backtrace with folding markers
# @param limit [ Integer, nil ] the maximum number of backtrace lines to include
#
# @return [ String ] the formatted backtrace as a string with lines separated by newlines
def format_backtrace(example, folding: false, limit: nil)
backtrace = execution_result(example).exception.backtrace
backtrace.nil? and return ''
if limit
backtrace = backtrace[0, limit]
end
result = []
folding and result << '{{{'
for line in backtrace
result << RSpec::Core::Metadata::relative_path(line)
end
folding and result << '}}}'
result * ?\n
end
# The dump_failure_to_error_file method writes failure information to an
# error log file.
#
# This method records detailed information about a failed test example,
# including the location, runtime, description, failure details, and
# backtrace. It uses file locking to ensure thread-safe writing to the
# error log file.
#
# @param example [ Object ] the test example that failed
def dump_failure_to_error_file(example)
@errors_lst.flock File::LOCK_EX
@errors_lst.puts "%s\n%3.3fs %s\n%s\n%s" % [
location(example), run_time(example), description(example, full: true),
dump_failure_for_example(example), format_backtrace(example, folding: true)
]
ensure
@errors_lst.flock File::LOCK_UN
end
# The execution_result method retrieves the execution result metadata
# from an example.
#
# This method accesses the metadata hash associated with the example's
# underlying test case to extract the execution result information that
# was stored during test execution.
#
# @param example [ Object ] the example object containing test metadata
#
# @return [ Object ] the execution result metadata stored in the example's metadata hash
def execution_result(example)
example.example.metadata[:execution_result]
end
# The description method retrieves either the full or abbreviated
# description of an example.
#
# @param example [ Object ] the example object containing description
# information
# @param full [ TrueClass, FalseClass ] determines whether to return the
# full description or abbreviated version
#
# @return [ String ] the appropriate description based on the full parameter value
def description(example, full: ENV['VERBOSE'].to_i == 1)
if full
example.example.full_description
else
example.example.description
end
end
# The run_time method retrieves the execution time of a test example.
#
# This method accesses the execution result of a given test example and
# returns the run time associated with that execution.
#
# @param example [ Object ] the test example object to get run time for
#
# @return [ Float, nil ] the execution time of the example or nil if not available
def run_time(example)
execution_result(example).run_time
end
# The format_line method formats and colors test execution results for
# display.
#
# This method takes a test example and creates a formatted string that
# includes the location, run time, and description of the test. It
# applies color coding based on the test result status and ensures the
# output fits within the terminal width.
#
# @param example [ Object ] the test example to format
#
# @return [ String ] the formatted and colorized test result string
def format_line(example)
args = [ location(example), run_time(example), description(example) ]
uncolored = "%s # S %3.3fs %s" % args
uncolored = uncolored[0, Tins::Terminal.columns]
case execution_result(example).status
when :passed
success_color(uncolored)
when :failed
failure_color(uncolored)
when :pending
pending_color(uncolored)
else
uncolored % args
end
end
# The success_color method applies green color formatting to the provided
# text.
#
# This method wraps the input text with ANSI color codes to display it in
# green, which is typically used to indicate successful or positive
# outcomes in terminal output.
#
# @param text [ String ] the text to be formatted with green color
#
# @return [ String ] the input text wrapped with green color ANSI escape codes
def success_color(text)
Term::ANSIColor.green(text)
end
# The failure_color method applies red color formatting to the provided
# text.
#
# This method wraps the input text with red color codes using the
# Term::ANSIColor library, making the text appear in red when displayed
# in compatible terminals.
#
# @param text [ String ] the text to be colorized in red
#
# @return [ String ] the colorized text wrapped with red formatting codes
def failure_color(text)
Term::ANSIColor.red(text)
end
# The pending_color method applies yellow color formatting to the
# provided text.
#
# This method wraps the input text with yellow color codes using the
# Term::ANSIColor library, making the text appear in yellow when
# displayed in compatible terminals.
#
# @param text [ String ] the text to be colorized
#
# @return [ String ] the colorized text with yellow formatting applied
def pending_color(text)
Term::ANSIColor.yellow(text)
end
# The location method extracts and formats the file path and line number
# for an RSpec example.
#
# This method retrieves the location information from the example's
# metadata, handling cases where the location might be nested within the
# example group. It then processes the location to return a relative path
# using RSpec's metadata helper.
#
# @param example [ Object ] the RSpec example object containing metadata
#
# @return [ String ] the relative file path and line number for the example
def location(example)
location = example.example.metadata[:location]
unless location.include?(?/)
location = example.example.metadata[:example_group][:location]
end
RSpec::Core::Metadata::relative_path(location)
end
end
end
end