mirror of
https://gitea.invidious.io/iv-org/shard-spectator.git
synced 2024-08-15 00:53:35 +00:00
Implement JUnit formatter
This commit is contained in:
parent
e30d5c1981
commit
fa3e9dd34d
6 changed files with 277 additions and 2 deletions
|
@ -1,4 +1,4 @@
|
||||||
require "./components/*"
|
require "./components/**"
|
||||||
|
|
||||||
module Spectator::Formatting
|
module Spectator::Formatting
|
||||||
# Namespace for snippets of text displayed in console output.
|
# Namespace for snippets of text displayed in console output.
|
||||||
|
|
39
src/spectator/formatting/components/junit/root.cr
Normal file
39
src/spectator/formatting/components/junit/root.cr
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
require "./test_suite"
|
||||||
|
|
||||||
|
module Spectator::Formatting::Components::JUnit
|
||||||
|
# Root node of the JUnit XML document.
|
||||||
|
# This is the "testsuites" element and all of its children.
|
||||||
|
struct Root
|
||||||
|
# Creates the root element.
|
||||||
|
def initialize(@runtime : Time::Span, @suites : Array(TestSuite), *,
|
||||||
|
@total : Int32, @failures : Int32, @errors : Int32)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Constructs the element from a report.
|
||||||
|
def self.from_report(report)
|
||||||
|
hostname = System.hostname
|
||||||
|
counts = report.counts
|
||||||
|
suites = report.examples.group_by(&.location.file)
|
||||||
|
suites = suites.map do |file, examples|
|
||||||
|
TestSuite.from_examples(file, examples, hostname)
|
||||||
|
end
|
||||||
|
|
||||||
|
new(report.runtime, suites,
|
||||||
|
total: counts.total,
|
||||||
|
failures: counts.fail,
|
||||||
|
errors: counts.error)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Produces the XML fragment.
|
||||||
|
def to_xml(xml)
|
||||||
|
xml.element("testsuites",
|
||||||
|
tests: @total,
|
||||||
|
failures: @failures,
|
||||||
|
errors: @errors,
|
||||||
|
time: @runtime.total_seconds,
|
||||||
|
name: "Spec") do
|
||||||
|
@suites.each(&.to_xml(xml))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
86
src/spectator/formatting/components/junit/test_case.cr
Normal file
86
src/spectator/formatting/components/junit/test_case.cr
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
require "xml"
|
||||||
|
require "../../../example"
|
||||||
|
|
||||||
|
module Spectator::Formatting::Components::JUnit
|
||||||
|
# Test case node of the JUnit XML document.
|
||||||
|
# This is the "testsuite" element and all of its children.
|
||||||
|
struct TestCase
|
||||||
|
# Creates the test case element.
|
||||||
|
def initialize(@class_name : String, @example : Example)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Produces the XML fragment.
|
||||||
|
def to_xml(xml)
|
||||||
|
result = @example.result
|
||||||
|
xml.element("testcase",
|
||||||
|
name: @example,
|
||||||
|
assertions: result.expectations.size,
|
||||||
|
classname: @class_name,
|
||||||
|
status: result.accept(StatusVisitor),
|
||||||
|
time: result.elapsed.total_seconds) do
|
||||||
|
visitor = ElementVisitor.new(xml)
|
||||||
|
result.accept(visitor)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Picks the status string for a result.
|
||||||
|
private module StatusVisitor
|
||||||
|
extend self
|
||||||
|
|
||||||
|
# Returns "PASS".
|
||||||
|
def pass(_result)
|
||||||
|
"PASS"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns "FAIL".
|
||||||
|
def fail(_result)
|
||||||
|
"FAIL"
|
||||||
|
end
|
||||||
|
|
||||||
|
# :ditto:
|
||||||
|
def error(result)
|
||||||
|
fail(result)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns "TODO".
|
||||||
|
def pending(_result)
|
||||||
|
"TODO"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Result visitor that adds elements to the test case node depending on the result.
|
||||||
|
private struct ElementVisitor
|
||||||
|
# Creates the visitor.
|
||||||
|
def initialize(@xml : XML::Builder)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Does nothing.
|
||||||
|
def pass(_result)
|
||||||
|
# ...
|
||||||
|
end
|
||||||
|
|
||||||
|
# Adds a failure element to the test case node.
|
||||||
|
def fail(result)
|
||||||
|
error = result.error
|
||||||
|
@xml.element("failure", message: error.message, type: error.class) do
|
||||||
|
# TODO: Add match-data as text to node.
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Adds an error element to the test case node.
|
||||||
|
def error(result)
|
||||||
|
error = result.error
|
||||||
|
@xml.element("error", message: error.message, type: error.class) do
|
||||||
|
if backtrace = error.backtrace
|
||||||
|
@xml.text(backtrace.join("\n"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Adds a skipped element to the test case node.
|
||||||
|
def pending(_result)
|
||||||
|
@xml.element("skipped") # TODO: Populate message attribute with reason from result.
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
104
src/spectator/formatting/components/junit/test_suite.cr
Normal file
104
src/spectator/formatting/components/junit/test_suite.cr
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
require "./test_case"
|
||||||
|
|
||||||
|
module Spectator::Formatting::Components::JUnit
|
||||||
|
# Test suite node of the JUnit XML document.
|
||||||
|
# This is the "testsuite" element and all of its children.
|
||||||
|
struct TestSuite
|
||||||
|
# Amounts for each type of test result.
|
||||||
|
record Counts, total : Int32, failures : Int32, errors : Int32, skipped : Int32
|
||||||
|
|
||||||
|
# Creates the test suite element.
|
||||||
|
def initialize(@package : String, @name : String, @cases : Array(TestCase),
|
||||||
|
@time : Time::Span, @counts : Counts, @hostname : String)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Constructs the test suite element from a collection of tests.
|
||||||
|
# The *examples* should all come from the same *file*.
|
||||||
|
def self.from_examples(file, examples, hostname)
|
||||||
|
package, name = package_name_from_file(file)
|
||||||
|
counts = count_examples(examples)
|
||||||
|
time = examples.sum(&.result.elapsed)
|
||||||
|
cases = examples.map { |example| TestCase.new(name, example) }
|
||||||
|
new(package, name, cases, time, counts, hostname)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Constructs a package and suite name from a file path.
|
||||||
|
private def self.package_name_from_file(file)
|
||||||
|
path = Path.new(file.to_s)
|
||||||
|
name = path.stem
|
||||||
|
directory = path.relative_to(Dir.current).dirname
|
||||||
|
package = directory.gsub(File::SEPARATOR, '.')
|
||||||
|
{package, name}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Counts the number of examples for each result type.
|
||||||
|
private def self.count_examples(examples)
|
||||||
|
visitor = CountVisitor.new
|
||||||
|
|
||||||
|
# Iterate through each example and count the number of each type of result.
|
||||||
|
# Don't count examples that haven't run (indicated by `Node#finished?`).
|
||||||
|
# This typically happens in fail-fast mode.
|
||||||
|
examples.each do |example|
|
||||||
|
example.result.accept(visitor) if example.finished?
|
||||||
|
end
|
||||||
|
|
||||||
|
visitor.counts
|
||||||
|
end
|
||||||
|
|
||||||
|
# Produces the XML fragment.
|
||||||
|
def to_xml(xml)
|
||||||
|
xml.element("testsuite",
|
||||||
|
package: @package,
|
||||||
|
name: @name,
|
||||||
|
tests: @counts.total,
|
||||||
|
failures: @counts.failures,
|
||||||
|
errors: @counts.errors,
|
||||||
|
skipped: @counts.skipped,
|
||||||
|
time: @time.total_seconds,
|
||||||
|
hostname: @hostname) do
|
||||||
|
@cases.each(&.to_xml(xml))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Totals up the number of each type of result.
|
||||||
|
# Defines methods for the different types of results.
|
||||||
|
# Call `#counts` to retrieve the `Counts` instance.
|
||||||
|
private class CountVisitor
|
||||||
|
@pass = 0
|
||||||
|
@failures = 0
|
||||||
|
@errors = 0
|
||||||
|
@skipped = 0
|
||||||
|
|
||||||
|
# Increments the number of passing examples.
|
||||||
|
def pass(_result)
|
||||||
|
@pass += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# Increments the number of failing (non-error) examples.
|
||||||
|
def fail(_result)
|
||||||
|
@failures += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# Increments the number of error (and failed) examples.
|
||||||
|
def error(result)
|
||||||
|
fail(result)
|
||||||
|
@errors += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# Increments the number of pending (skipped) examples.
|
||||||
|
def pending(_result)
|
||||||
|
@skipped += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# Produces the total counts.
|
||||||
|
def counts
|
||||||
|
Counts.new(
|
||||||
|
total: @pass + @failures + @skipped,
|
||||||
|
failures: @failures,
|
||||||
|
errors: @errors,
|
||||||
|
skipped: @skipped
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,8 +1,51 @@
|
||||||
|
require "xml"
|
||||||
require "./formatter"
|
require "./formatter"
|
||||||
|
|
||||||
module Spectator::Formatting
|
module Spectator::Formatting
|
||||||
|
# Produces a JUnit compatible XML file containing the test results.
|
||||||
class JUnitFormatter < Formatter
|
class JUnitFormatter < Formatter
|
||||||
def initialize(output_dir)
|
# Default XML file name.
|
||||||
|
private OUTPUT_FILE = "output.xml"
|
||||||
|
|
||||||
|
# XML builder for the entire document.
|
||||||
|
private getter! xml : XML::Builder
|
||||||
|
|
||||||
|
# Output stream for the XML file.
|
||||||
|
private getter! io : IO
|
||||||
|
|
||||||
|
# Creates the formatter.
|
||||||
|
# The *output_path* can be a directory or path of an XML file.
|
||||||
|
# If the former, then an "output.xml" file will be generated in the specified directory.
|
||||||
|
def initialize(output_path = OUTPUT_FILE)
|
||||||
|
@output_path = if output_path.ends_with?(".xml")
|
||||||
|
output_path
|
||||||
|
else
|
||||||
|
File.join(output_path, OUTPUT_FILE)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Prepares the formatter for writing.
|
||||||
|
def start(_notification)
|
||||||
|
@io = io = File.open(@output_path, "w")
|
||||||
|
@xml = xml = XML::Builder.new(io)
|
||||||
|
xml.start_document("1.0", "UTF-8")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Invoked after testing completes with summarized information from the test suite.
|
||||||
|
# Unfortunately, the JUnit specification is not conducive to streaming data.
|
||||||
|
# All results are gathered at the end, then the report is generated.
|
||||||
|
def dump_summary(notification)
|
||||||
|
report = notification.report
|
||||||
|
root = Components::JUnit::Root.from_report(report)
|
||||||
|
root.to_xml(xml)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Invoked at the end of the program.
|
||||||
|
# Allows the formatter to perform any cleanup and teardown.
|
||||||
|
def close
|
||||||
|
xml.end_document
|
||||||
|
xml.flush
|
||||||
|
io.close
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,6 +22,9 @@ module Spectator
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Retrieves all examples that were planned to run as part of the suite.
|
||||||
|
getter examples : Array(Example)
|
||||||
|
|
||||||
# Total length of time it took to execute the test suite.
|
# Total length of time it took to execute the test suite.
|
||||||
# This includes examples, hooks, and framework processes.
|
# This includes examples, hooks, and framework processes.
|
||||||
getter runtime : Time::Span
|
getter runtime : Time::Span
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue