Initial work on summary output

This commit is contained in:
Michael Miller 2021-05-16 15:03:37 -06:00
parent ee294a3ec2
commit 3ecb04e293
No known key found for this signature in database
GPG key ID: FB9F12F7C646A4AD
10 changed files with 307 additions and 0 deletions

View file

@ -0,0 +1,8 @@
require "./components/*"
module Spectator::Formatting
# Namespace for snippets of text displayed in console output.
# These types are typically constructed and have `#to_s` called.
module Components
end
end

View file

@ -0,0 +1,18 @@
module Spectator::Formatting::Components
struct Comment(T)
private COLOR = :cyan
def initialize(@content : T)
end
def self.colorize(content)
new(content).colorize(COLOR)
end
def to_s(io)
io << '#'
io << ' '
io << @content
end
end
end

View file

@ -0,0 +1,13 @@
module Spectator::Formatting::Components
struct ExampleFilterCommand
def initialize(@example : Example)
end
def to_s(io)
io << "crystal spec "
io << @example.location
io << ' '
io << Comment.colorize(@example.to_s)
end
end
end

View file

@ -0,0 +1,32 @@
require "../../example"
require "./comment"
module Spectator::Formatting::Components
struct FailureBlock
private INDENT = 2
def initialize(@example : Example, @index : Int32)
@result = @example.result.as(FailResult)
end
def to_s(io)
2.times { io << ' ' }
io << @index
io << ')'
io << ' '
io.puts @example
indent = INDENT + index_digit_count + 2
indent.times { io << ' ' }
io << "Failure: ".colorize(:red)
io.puts @result.error.message
io.puts
# TODO: Expectation values
indent.times { io << ' ' }
io.puts Comment.colorize(@example.location) # TODO: Use location of failed expectation.
end
private def index_digit_count
(Math.log(@index.to_f + 1) / Math.log(10)).ceil.to_i
end
end
end

View file

@ -0,0 +1,28 @@
require "../../example"
require "./comment"
module Spectator::Formatting::Components
struct PendingBlock
private INDENT = 2
def initialize(@example : Example, @index : Int32)
end
def to_s(io)
2.times { io << ' ' }
io << @index
io << ')'
io << ' '
io.puts @example
indent = INDENT + index_digit_count + 2
indent.times { io << ' ' }
io.puts Comment.colorize("No reason given") # TODO: Get reason from result.
indent.times { io << ' ' }
io.puts Comment.colorize(@example.location) # TODO: Pending result could be triggered from another location.
end
private def index_digit_count
(Math.log(@index.to_f + 1) / Math.log(10)).ceil.to_i
end
end
end

View file

@ -0,0 +1,69 @@
module Spectator::Formatting::Components
# Presents a human readable time span.
struct Runtime
# Creates the component.
def initialize(@span : Time::Span)
end
# Appends the elapsed time to the output.
# The text will be formatted as follows, depending on the magnitude:
# ```text
# ## microseconds
# ## milliseconds
# ## seconds
# #:##
# #:##:##
# # days #:##:##
# ```
def to_s(io)
millis = @span.total_milliseconds
return format_micro(io, millis * 1000) if millis < 1
seconds = @span.total_seconds
return format_millis(io, millis) if seconds < 1
return format_seconds(io, seconds) if seconds < 60
minutes, seconds = seconds.divmod(60)
return format_minutes(io, minutes, seconds) if minutes < 60
hours, minutes = minutes.divmod(60)
return format_hours(io, hours, minutes, seconds) if hours < 24
days, hours = hours.divmod(24)
format_days(io, days, hours, minutes, seconds)
end
# Formats for microseconds.
private def format_micro(io, micros)
io << micros.round.to_i
io << " microseconds"
end
# Formats for milliseconds.
private def format_millis(io, millis)
io << millis.round(2)
io << " milliseconds"
end
# Formats for seconds.
private def format_seconds(io, seconds)
io << seconds.round(2)
io << " seconds"
end
# Formats for minutes.
private def format_minutes(io, minutes, seconds)
io.printf("%i:%02i", minutes, seconds)
end
# Formats for hours.
private def format_hours(io, hours, minutes, seconds)
io.printf("%i:%02i:%02i", hours, minutes, seconds)
end
# Formats for days.
private def format_days(io, days, hours, minutes, seconds)
io.printf("%i days %i:%02i:%02i", days, hours, minutes, seconds)
end
end
end

View file

@ -0,0 +1,38 @@
require "./example_filter_command"
require "./runtime"
require "./totals"
module Spectator::Formatting::Components
# Summary information displayed at the end of a run.
struct SummaryBlock
def initialize(@report : Report)
end
def to_s(io)
timing_line(io)
totals_line(io)
unless (failures = @report.failures).empty?
io.puts
failures_block(io, failures)
end
end
private def timing_line(io)
io << "Finished in "
io.puts Runtime.new(@report.runtime)
end
private def totals_line(io)
io.puts Totals.colorize(@report.counts)
end
private def failures_block(io, failures)
io.puts "Failed examples:"
io.puts
failures.each do |failure|
io.puts ExampleFilterCommand.new(failure).colorize(:red)
end
end
end
end

View file

@ -0,0 +1,35 @@
module Spectator::Formatting::Components
struct Totals
def initialize(@examples : Int32, @failures : Int32, @errors : Int32, @pending : Int32)
end
def initialize(counts)
@examples = counts.run
@failures = counts.fail
@errors = counts.error
@pending = counts.pending
end
def self.colorize(counts)
totals = new(counts)
if counts.fail > 0
totals.colorize(:red)
elsif counts.pending > 0
totals.colorize(:yellow)
else
totals.colorize(:green)
end
end
def to_s(io)
io << @examples
io << " examples, "
io << @failures
io << " failures, "
io << @errors
io << " errors, "
io << @pending
io << " pending"
end
end
end

View file

@ -1,15 +1,21 @@
require "colorize"
require "./formatter"
require "./summary"
module Spectator::Formatting
# Output formatter that produces a single character for each test as it completes.
# A '.' indicates a pass, 'F' a failure, 'E' an error, and '*' a skipped or pending test.
class ProgressFormatter < Formatter
include Summary
@pass_char : Colorize::Object(Char) = '.'.colorize(:green)
@fail_char : Colorize::Object(Char) = 'F'.colorize(:red)
@error_char : Colorize::Object(Char) = 'E'.colorize(:red)
@skip_char : Colorize::Object(Char) = '*'.colorize(:yellow)
# Output stream to write results to.
private getter io : IO
# Creates the formatter.
def initialize(@io : IO = STDOUT)
end
@ -33,5 +39,10 @@ module Spectator::Formatting
def example_pending(_notification)
@skip_char.to_s(@io)
end
# Produces a new line after the tests complete.
def stop(_notification)
@io.puts
end
end
end

View file

@ -0,0 +1,55 @@
require "./components"
module Spectator::Formatting
# Mix-in providing common output for summarized results.
# Implements the following methods:
# `Formatter#start_dump`, `Formatter#dump_pending`, `Formatter#dump_failures`,
# `Formatter#dump_summary`, and `Formatter#dump_profile`.
# Classes including this module must implement `#io`.
module Summary
# Stream to write results to.
private abstract def io : IO
def start_dump
io.puts
end
# Invoked after testing completes with a list of pending examples.
# This method will be called with an empty list if there were no pending (skipped) examples.
# Called after `#start_dump` and before `#dump_failures`.
def dump_pending(notification)
return if (examples = notification.examples).empty?
io.puts "Pending:"
io.puts
examples.each_with_index do |example, index|
io.puts Components::PendingBlock.new(example, index + 1)
end
end
# Invoked after testing completes with a list of failed examples.
# This method will be called with an empty list if there were no failures.
# Called after `#dump_pending` and before `#dump_summary`.
def dump_failures(notification)
return if (examples = notification.examples).empty?
io.puts "Failures:"
io.puts
examples.each_with_index do |example, index|
io.puts Components::FailureBlock.new(example, index + 1)
end
end
# Invoked after testing completes with summarized information from the test suite.
# Called after `#dump_failures` and before `#dump_profile`.
def dump_summary(notification)
io.puts Components::SummaryBlock.new(notification.report)
end
# Invoked after testing completes with profiling information.
# This method is only called if profiling is enabled.
# Called after `#dump_summary` and before `#close`.
def dump_profile(_notification)
end
end
end