mirror of
https://gitea.invidious.io/iv-org/shard-spectator.git
synced 2024-08-15 00:53:35 +00:00
Initial work on summary output
This commit is contained in:
parent
ee294a3ec2
commit
3ecb04e293
10 changed files with 307 additions and 0 deletions
8
src/spectator/formatting/components.cr
Normal file
8
src/spectator/formatting/components.cr
Normal 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
|
18
src/spectator/formatting/components/comment.cr
Normal file
18
src/spectator/formatting/components/comment.cr
Normal 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
|
|
@ -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
|
32
src/spectator/formatting/components/failure_block.cr
Normal file
32
src/spectator/formatting/components/failure_block.cr
Normal 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
|
28
src/spectator/formatting/components/pending_block.cr
Normal file
28
src/spectator/formatting/components/pending_block.cr
Normal 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
|
69
src/spectator/formatting/components/runtime.cr
Normal file
69
src/spectator/formatting/components/runtime.cr
Normal 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
|
38
src/spectator/formatting/components/summary_block.cr
Normal file
38
src/spectator/formatting/components/summary_block.cr
Normal 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
|
35
src/spectator/formatting/components/totals.cr
Normal file
35
src/spectator/formatting/components/totals.cr
Normal 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
|
|
@ -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
|
||||
|
|
55
src/spectator/formatting/summary.cr
Normal file
55
src/spectator/formatting/summary.cr
Normal 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
|
Loading…
Reference in a new issue