Hack together result output

This commit is contained in:
Michael Miller 2021-01-30 19:42:46 -07:00
parent f5713efc62
commit 9d139dfeed
No known key found for this signature in database
GPG key ID: FB9F12F7C646A4AD
25 changed files with 168 additions and 138 deletions

View file

@ -67,7 +67,7 @@ module Spectator
# Build the spec and run it. # Build the spec and run it.
DSL::Builder.config = config DSL::Builder.config = config
spec = DSL::Builder.build spec = DSL::Builder.build
spec.run spec.run(config.example_filter)
true true
rescue ex rescue ex
# Catch all unhandled exceptions here. # Catch all unhandled exceptions here.

View file

@ -10,9 +10,19 @@ module Spectator
visitor.error visitor.error
end end
# Calls the `error` method on *visitor*.
def accept(visitor)
visitor.error(yield self)
end
# One-word description of the result. # One-word description of the result.
def to_s(io) def to_s(io)
io << "error" io << "error"
end end
# TODO
def to_json(builder)
builder.string("ERROR")
end
end end
end end

View file

@ -17,7 +17,10 @@ module Spectator
getter? finished : Bool = false getter? finished : Bool = false
# Retrieves the result of the last time the example ran. # Retrieves the result of the last time the example ran.
getter result : Result = PendingResult.new def result : Result
# TODO: Set to pending immediately (requires circular dependency between Example <-> Result removed).
@result ||= PendingResult.new(self)
end
# Creates the example. # Creates the example.
# An instance to run the test code in is given by *context*. # An instance to run the test code in is given by *context*.
@ -59,7 +62,7 @@ module Spectator
if pending? if pending?
Log.debug { "Skipping example #{self} - marked pending" } Log.debug { "Skipping example #{self} - marked pending" }
return @result = PendingResult.new return @result = PendingResult.new(self)
end end
previous_example = @@current previous_example = @@current
@ -146,6 +149,11 @@ module Spectator
io << result io << result
end end
# TODO
def to_json(builder)
builder.string("EXAMPLE")
end
# Wraps an example to behave like a `Proc`. # Wraps an example to behave like a `Proc`.
# This is typically used for an *around_each* hook. # This is typically used for an *around_each* hook.
# Invoking `#call` or `#run` will run the example. # Invoking `#call` or `#run` will run the example.

View file

@ -11,8 +11,8 @@ module Spectator
# Creates a failure result. # Creates a failure result.
# The *elapsed* argument is the length of time it took to run the example. # The *elapsed* argument is the length of time it took to run the example.
# The *error* is the exception raised that caused the failure. # The *error* is the exception raised that caused the failure.
def initialize(elapsed, @error) def initialize(example, elapsed, @error, expectations = [] of Expectation)
super(elapsed) super(example, elapsed, expectations)
end end
# Calls the `failure` method on *visitor*. # Calls the `failure` method on *visitor*.
@ -20,9 +20,19 @@ module Spectator
visitor.failure visitor.failure
end end
# Calls the `failure` method on *visitor*.
def accept(visitor)
visitor.failure(yield self)
end
# One-word description of the result. # One-word description of the result.
def to_s(io) def to_s(io)
io << "fail" io << "fail"
end end
# TODO
def to_json(builder)
builder.string("FAIL")
end
end end
end end

View file

@ -15,7 +15,7 @@ module Spectator::Formatting
} }
# Colorizes some text with the success color. # Colorizes some text with the success color.
def success(text) def pass(text)
text.colorize(COLORS[:success]) text.colorize(COLORS[:success])
end end

View file

@ -29,7 +29,7 @@ module Spectator::Formatting
# Produces a single character output based on a result. # Produces a single character output based on a result.
def end_example(result) def end_example(result)
@previous_hierarchy.size.times { @io.print INDENT } @previous_hierarchy.size.times { @io.print INDENT }
@io.puts result.call(Color) { result.example.description } @io.puts result.accept(Color) { result.example.name }
end end
# Produces a list of groups making up the hierarchy for an example. # Produces a list of groups making up the hierarchy for an example.
@ -38,7 +38,7 @@ module Spectator::Formatting
group = example.group group = example.group
while group.is_a?(ExampleGroup) while group.is_a?(ExampleGroup)
hierarchy << group hierarchy << group
group = group.parent group = group.group?
end end
hierarchy.reverse hierarchy.reverse
end end
@ -57,7 +57,7 @@ module Spectator::Formatting
private def print_sub_hierarchy(index, sub_hierarchy) private def print_sub_hierarchy(index, sub_hierarchy)
sub_hierarchy.each do |group| sub_hierarchy.each do |group|
index.times { @io.print INDENT } index.times { @io.print INDENT }
@io.puts group.description @io.puts group.name
index += 1 index += 1
end end
end end

View file

@ -21,7 +21,7 @@ module Spectator::Formatting
# Produces a single character output based on a result. # Produces a single character output based on a result.
def end_example(result) def end_example(result)
@io.print result.call(Character) @io.print result.accept(Character)
end end
# Interface for `Result` to pick a character for output. # Interface for `Result` to pick a character for output.
@ -37,8 +37,8 @@ module Spectator::Formatting
} }
# Character output for a successful example. # Character output for a successful example.
def success def pass
Color.success(CHARACTERS[:success]) Color.pass(CHARACTERS[:success])
end end
# Character output for a failed example. # Character output for a failed example.

View file

@ -7,7 +7,7 @@ module Spectator::Formatting
private getter result private getter result
# Creates the JUnit test case. # Creates the JUnit test case.
def initialize(@result : ErroredResult) def initialize(@result : ErrorResult)
end end
# Adds the exception to the XML block. # Adds the exception to the XML block.

View file

@ -16,7 +16,7 @@ module Spectator::Formatting
# Creates the failure block. # Creates the failure block.
# The *index* uniquely identifies the failure in the output. # The *index* uniquely identifies the failure in the output.
# The *result* is the outcome of the failed example. # The *result* is the outcome of the failed example.
def initialize(@index : Int32, @result : FailedResult) def initialize(@index : Int32, @result : FailResult)
end end
# Creates the block of text describing the failure. # Creates the block of text describing the failure.
@ -47,12 +47,12 @@ module Spectator::Formatting
# then an error stacktrace if an error occurred. # then an error stacktrace if an error occurred.
private def content(indent) private def content(indent)
unsatisfied_expectations(indent) unsatisfied_expectations(indent)
error_stacktrace(indent) if @result.is_a?(ErroredResult) error_stacktrace(indent) if @result.is_a?(ErrorResult)
end end
# Produces a list of unsatisfied expectations and their values. # Produces a list of unsatisfied expectations and their values.
private def unsatisfied_expectations(indent) private def unsatisfied_expectations(indent)
@result.expectations.each_unsatisfied do |expectation| @result.expectations.reject(&.satisfied?).each do |expectation|
indent.line(Color.failure(LabeledText.new("Failure", expectation.failure_message))) indent.line(Color.failure(LabeledText.new("Failure", expectation.failure_message)))
indent.line indent.line
indent.increase do indent.increase do
@ -66,7 +66,7 @@ module Spectator::Formatting
private def matcher_values(indent, expectation) private def matcher_values(indent, expectation)
MatchDataValues.new(expectation.values).each do |pair| MatchDataValues.new(expectation.values).each do |pair|
colored_pair = if expectation.satisfied? colored_pair = if expectation.satisfied?
Color.success(pair) Color.pass(pair)
else else
Color.failure(pair) Color.failure(pair)
end end

View file

@ -13,7 +13,7 @@ module Spectator::Formatting
# Colorizes the command instance based on the result. # Colorizes the command instance based on the result.
def self.color(result) def self.color(result)
result.call(Color) { new(result.example) } result.accept(Color) { new(result.example) }
end end
end end
end end

View file

@ -7,7 +7,7 @@ module Spectator::Formatting
private getter result private getter result
# Creates the JUnit test case. # Creates the JUnit test case.
def initialize(@result : FailedResult) def initialize(@result : FailResult)
end end
# Status string specific to the result type. # Status string specific to the result type.
@ -18,7 +18,7 @@ module Spectator::Formatting
# Adds the failed expectations to the XML block. # Adds the failed expectations to the XML block.
private def content(xml) private def content(xml)
super super
@result.expectations.each_unsatisfied do |expectation| @result.expectations.reject(&.satisfied?).each do |expectation|
xml.element("failure", message: expectation.failure_message) do xml.element("failure", message: expectation.failure_message) do
expectation_values(expectation.values, xml) expectation_values(expectation.values, xml)
end end

View file

@ -25,7 +25,7 @@ module Spectator::Formatting
# Adds the test case elements to the XML. # Adds the test case elements to the XML.
private def add_test_cases(xml) private def add_test_cases(xml)
@report.each do |result| @report.each do |result|
test_case = result.call(JUnitTestCaseSelector) { |r| r } test_case = result.accept(JUnitTestCaseSelector) { |r| r }
test_case.to_xml(xml) test_case.to_xml(xml)
end end
end end
@ -50,18 +50,18 @@ module Spectator::Formatting
extend self extend self
# Creates a successful JUnit test case. # Creates a successful JUnit test case.
def success(result) def pass(result)
SuccessfulJUnitTestCase.new(result.as(SuccessfulResult)) SuccessfulJUnitTestCase.new(result.as(PassResult))
end end
# Creates a failure JUnit test case. # Creates a failure JUnit test case.
def failure(result) def failure(result)
FailureJUnitTestCase.new(result.as(FailedResult)) FailureJUnitTestCase.new(result.as(FailResult))
end end
# Creates an error JUnit test case. # Creates an error JUnit test case.
def error(result) def error(result)
ErrorJUnitTestCase.new(result.as(ErroredResult)) ErrorJUnitTestCase.new(result.as(ErrorResult))
end end
# Creates a skipped JUnit test case. # Creates a skipped JUnit test case.

View file

@ -20,7 +20,7 @@ module Spectator::Formatting
elsif @pending > 0 elsif @pending > 0
Color.pending(self) Color.pending(self)
else else
Color.success(self) Color.pass(self)
end end
end end

View file

@ -5,7 +5,7 @@ module Spectator::Formatting
private getter result private getter result
# Creates the JUnit test case. # Creates the JUnit test case.
def initialize(@result : SuccessfulResult) def initialize(@result : PassResult)
end end
# Status string specific to the result type. # Status string specific to the result type.

View file

@ -17,7 +17,7 @@ module Spectator::Formatting
# The text "ok" or "not ok" depending on the result. # The text "ok" or "not ok" depending on the result.
private def status private def status
@result.is_a?(FailedResult) ? "not ok" : "ok" @result.is_a?(FailResult) ? "not ok" : "ok"
end end
# The example that was tested. # The example that was tested.

View file

@ -91,13 +91,14 @@ module Spectator
# Takes the *elapsed* time and a possible *error* from the test. # Takes the *elapsed* time and a possible *error* from the test.
# Returns a type of `Result`. # Returns a type of `Result`.
private def translate(elapsed, error) : Result private def translate(elapsed, error) : Result
example = Example.current # TODO: Remove this.
case error case error
when nil when nil
PassResult.new(elapsed) PassResult.new(example, elapsed)
when ExpectationFailed when ExpectationFailed
FailResult.new(elapsed, error) FailResult.new(example, elapsed, error)
else else
ErrorResult.new(elapsed, error) ErrorResult.new(example, elapsed, error)
end end
end end

View file

@ -47,7 +47,6 @@ require "./pending_result"
require "./profile" require "./profile"
require "./report" require "./report"
require "./result" require "./result"
require "./runner"
require "./source" require "./source"
require "./source_example_filter" require "./source_example_filter"
require "./spec" require "./spec"

View file

@ -8,9 +8,19 @@ module Spectator
visitor.pass visitor.pass
end end
# Calls the `pass` method on *visitor*.
def accept(visitor)
visitor.pass(yield self)
end
# One-word description of the result. # One-word description of the result.
def to_s(io) def to_s(io)
io << "pass" io << "pass"
end end
# TODO
def to_json(builder)
builder.string("PASS")
end
end end
end end

View file

@ -7,7 +7,7 @@ module Spectator
class PendingResult < Result class PendingResult < Result
# Creates the result. # Creates the result.
# *elapsed* is the length of time it took to run the example. # *elapsed* is the length of time it took to run the example.
def initialize(elapsed = Time::Span::ZERO) def initialize(example, elapsed = Time::Span::ZERO, expectations = [] of Expectation)
super super
end end
@ -16,9 +16,19 @@ module Spectator
visitor.pending visitor.pending
end end
# Calls the `pending` method on the *visitor*.
def accept(visitor)
visitor.pending(yield self)
end
# One-word description of the result. # One-word description of the result.
def to_s(io) def to_s(io)
io << "pending" io << "pending"
end end
# TODO
def to_json(builder)
builder.string("PENDING")
end
end end
end end

View file

@ -8,7 +8,7 @@ module Spectator
# Creates the profiling information. # Creates the profiling information.
# The *slowest* results must already be sorted, longest time first. # The *slowest* results must already be sorted, longest time first.
private def initialize(@slowest : Array(FinishedResult), @total_time) private def initialize(@slowest : Array(Result), @total_time)
end end
# Number of results in the profile. # Number of results in the profile.
@ -33,7 +33,7 @@ module Spectator
# Produces the profile from a report. # Produces the profile from a report.
def self.generate(report, size = 10) def self.generate(report, size = 10)
results = report.compact_map(&.as?(FinishedResult)) results = report.to_a
sorted_results = results.sort_by(&.elapsed) sorted_results = results.sort_by(&.elapsed)
slowest = sorted_results.last(size).reverse slowest = sorted_results.last(size).reverse
self.new(slowest, report.example_runtime) self.new(slowest, report.example_runtime)

View file

@ -37,12 +37,12 @@ module Spectator
def initialize(@results : Array(Result), @runtime, @remaining_count = 0, @fail_blank = false, @random_seed = nil) def initialize(@results : Array(Result), @runtime, @remaining_count = 0, @fail_blank = false, @random_seed = nil)
@results.each do |result| @results.each do |result|
case result case result
when SuccessfulResult when PassResult
@successful_count += 1 @successful_count += 1
when ErroredResult when ErrorResult
@error_count += 1 @error_count += 1
@failed_count += 1 @failed_count += 1
when FailedResult when FailResult
@failed_count += 1 @failed_count += 1
when PendingResult when PendingResult
@pending_count += 1 @pending_count += 1
@ -58,7 +58,7 @@ module Spectator
# The *results* are from running the examples in the test suite. # The *results* are from running the examples in the test suite.
# The runtime is calculated from the *results*. # The runtime is calculated from the *results*.
def initialize(results : Array(Result)) def initialize(results : Array(Result))
runtime = results.each.compact_map(&.as?(FinishedResult)).sum(&.elapsed) runtime = results.sum(&.elapsed)
initialize(results, runtime) initialize(results, runtime)
end end
@ -92,19 +92,19 @@ module Spectator
# Returns a set of results for all failed examples. # Returns a set of results for all failed examples.
def failures def failures
@results.each.compact_map(&.as?(FailedResult)) @results.each.compact_map(&.as?(FailResult))
end end
# Returns a set of results for all errored examples. # Returns a set of results for all errored examples.
def errors def errors
@results.each.compact_map(&.as?(ErroredResult)) @results.each.compact_map(&.as?(ErrorResult))
end end
# Length of time it took to run just example code. # Length of time it took to run just example code.
# This does not include hooks, # This does not include hooks,
# but it does include pre- and post-conditions. # but it does include pre- and post-conditions.
def example_runtime def example_runtime
@results.each.compact_map(&.as?(FinishedResult)).sum(&.elapsed) @results.sum(&.elapsed)
end end
# Length of time spent in framework processes and hooks. # Length of time spent in framework processes and hooks.

View file

@ -2,15 +2,19 @@ module Spectator
# Base class that represents the outcome of running an example. # Base class that represents the outcome of running an example.
# Sub-classes contain additional information specific to the type of result. # Sub-classes contain additional information specific to the type of result.
abstract class Result abstract class Result
# Example that generated the result.
# TODO: Remove this.
getter example : Example
# Length of time it took to run the example. # Length of time it took to run the example.
getter elapsed : Time::Span getter elapsed : Time::Span
# The assertions checked in the example. # The assertions checked in the example.
# getter assertions : Enumerable(Assertion) # TODO: Implement Assertion type. getter expectations : Enumerable(Expectation)
# Creates the result. # Creates the result.
# *elapsed* is the length of time it took to run the example. # *elapsed* is the length of time it took to run the example.
def initialize(@elapsed) def initialize(@example, @elapsed, @expectations = [] of Expectation)
end end
# Calls the corresponding method for the type of result. # Calls the corresponding method for the type of result.

View file

@ -1,81 +0,0 @@
require "./harness"
module Spectator
# Main driver for executing tests and feeding results to formatters.
class Runner
# Creates the test suite runner.
# Specify the test *suite* to run and any additonal configuration.
def initialize(@suite : TestSuite, @config : Config)
end
# Runs the test suite.
# This will run the selected examples
# and invoke the formatter to output results.
# True will be returned if the test suite ran successfully,
# or false if there was at least one failure.
def run : Bool
# Indicate the suite is starting.
@config.each_formatter(&.start_suite(@suite))
# Run all examples and capture the results.
results = Array(Result).new(@suite.size)
elapsed = Time.measure do
collect_results(results)
end
# Generate a report and pass it along to the formatter.
remaining = @suite.size - results.size
seed = (@config.random_seed? if @config.randomize?)
report = Report.new(results, elapsed, remaining, @config.fail_blank?, seed)
@config.each_formatter(&.end_suite(report, profile(report)))
!report.failed?
end
# Runs all examples and adds results to a list.
private def collect_results(results)
example_order.each do |example|
result = run_example(example).as(Result)
results << result
if @config.fail_fast? && result.is_a?(FailedResult)
example.group.context.run_after_all_hooks(example.group, ignore_unfinished: true)
break
end
end
end
# Retrieves an enumerable for the examples to run.
# The order of examples is randomized
# if specified by the configuration.
private def example_order
@suite.to_a.tap do |examples|
examples.shuffle!(@config.random) if @config.randomize?
end
end
# Runs a single example and returns the result.
# The formatter is given the example and result information.
private def run_example(example)
@config.each_formatter(&.start_example(example))
result = if @config.dry_run? && example.is_a?(RunnableExample)
dry_run_result(example)
else
Harness.run(example)
end
@config.each_formatter(&.end_example(result))
result
end
# Creates a fake result for an example.
private def dry_run_result(example)
expectations = [] of Expectations::Expectation
example_expectations = Expectations::ExampleExpectations.new(expectations)
SuccessfulResult.new(example, Time::Span.zero, example_expectations)
end
# Generates and returns a profile if one should be displayed.
private def profile(report)
Profile.generate(report) if @config.profile?
end
end
end

View file

@ -1,7 +1,7 @@
require "./config" require "./config"
require "./example" require "./example"
require "./example_group" require "./example_group"
require "./example_iterator" require "./test_suite"
module Spectator module Spectator
# Contains examples to be tested. # Contains examples to be tested.
@ -9,15 +9,9 @@ module Spectator
def initialize(@root : ExampleGroup, @config : Config) def initialize(@root : ExampleGroup, @config : Config)
end end
def run def run(filter : ExampleFilter)
Runner.new(examples).run suite = TestSuite.new(@root, filter)
end Runner.new(suite, @config).run
# Generates a list of examples to run.
# The order of the examples are also sorted based on the configuration.
private def examples
examples = ExampleIterator.new(@root).to_a
@config.shuffle!(examples)
end end
end end
end end

View file

@ -3,11 +3,76 @@ require "../example"
module Spectator module Spectator
class Spec class Spec
private struct Runner private struct Runner
def initialize(@examples : Array(Example)) def initialize(@suite : TestSuite, @config : Config)
end end
def run # Runs the test suite.
@examples.each(&.run) # This will run the selected examples
# and invoke the formatter to output results.
# True will be returned if the test suite ran successfully,
# or false if there was at least one failure.
def run : Bool
# Indicate the suite is starting.
@config.each_formatter(&.start_suite(@suite))
# Run all examples and capture the results.
results = Array(Result).new(@suite.size)
elapsed = Time.measure do
collect_results(results)
end
# Generate a report and pass it along to the formatter.
remaining = @suite.size - results.size
seed = (@config.random_seed if @config.randomize?)
report = Report.new(results, elapsed, remaining, @config.fail_blank?, seed)
@config.each_formatter(&.end_suite(report, profile(report)))
!report.failed?
end
# Runs all examples and adds results to a list.
private def collect_results(results)
example_order.each do |example|
result = run_example(example).as(Result)
results << result
if @config.fail_fast? && result.is_a?(FailResult)
example.group.call_once_after_all
break
end
end
end
# Retrieves an enumerable for the examples to run.
# The order of examples is randomized
# if specified by the configuration.
private def example_order
@suite.to_a.tap do |examples|
@config.shuffle!(examples)
end
end
# Runs a single example and returns the result.
# The formatter is given the example and result information.
private def run_example(example)
@config.each_formatter(&.start_example(example))
result = if @config.dry_run?
dry_run_result(example)
else
example.run
end
@config.each_formatter(&.end_example(result))
result
end
# Creates a fake result for an example.
private def dry_run_result(example)
expectations = [] of Expectation
PassResult.new(example, Time::Span.zero, expectations)
end
# Generates and returns a profile if one should be displayed.
private def profile(report)
Profile.generate(report) if @config.profile?
end end
end end
end end