Initial code for HTML formatter

This commit is contained in:
Michael Miller 2021-08-18 17:50:09 -06:00
parent f4fc599a1d
commit dd0ef01369
No known key found for this signature in database
GPG key ID: F9A0C5C65B162436
6 changed files with 178 additions and 1 deletions

View file

@ -15,7 +15,7 @@ Spectator.describe Spectator do
it "handles multiple lines and examples" do
# Offset is important.
expect(location.line).to eq(__LINE__ - 2)
expect(location.line).to eq(__LINE__ - 3)
# This line fails, refer to https://github.com/crystal-lang/crystal/issues/10562
# expect(location.end_line).to eq(__LINE__ + 2)
# Offset is still important.

View file

@ -218,6 +218,15 @@ module Spectator
end
end
# Adds the HTML output option to the parser.
private def junit_option(parser, builder)
parser.on("--html_output OUTPUT_DIR", "Generate HTML output") do |output_dir|
Log.debug { "Setting output format to HTML (--html_output '#{output_dir}')" }
formatter = Formatting::HTMLFormatter.new(output_dir)
builder.add_formatter(formatter)
end
end
# Adds the "no color" output option to the parser.
private def no_color_option(parser, builder)
parser.on("--no-color", "Disable colored output") do

View file

@ -0,0 +1,85 @@
<div id="summary" class="<%= summary_result(report) %>">
<h1>Test Results</h1>
<span class="result <%= summary_result(report) %>"><% escape(Components::Totals.new(report.counts)) %></span>
<span class="elapsed" title="<% escape(report.runtime) %>"><% escape(runtime(report.runtime)) %></span>
</div>
<%- if report.counts.fail > 0 -%>
<div id="failure-list">
<h2>Failures</h2>
<%- report.failures.each do |example| -%>
<div class="example fail">
<a href="#example-<%= example.object_id %>" title="Jump to result">
<h3 class="full-name"><% escape(example) %></h3>
</a>
</div>
<%- end -%>
</div>
<%- end -%>
<%- if report.counts.pending > 0 -%>
<div id="pending-list">
<h2>Pending</h2>
<%- report.pending.each do |example| -%>
<div class="example pending">
<a href="#example-<%= example.object_id %>" title="Jump to result">
<h3 class="full-name"><% escape(example) %></h3>
</a>
</div>
<%- end -%>
</div>
<%- end -%>
<div id="example-list">
<h2>Examples</h2>
<%- report.examples.each do |example| -%>
<div class="example <%= example.result %>">
<h3 id="#example-<%= example.object_id %>" class="full-name"><% escape(example) %></h3>
<span class="result <%= example.result %>"><%= example.result %></span>
<span class="elapsed" title="<% escape(example.result.elapsed) %>"><% escape(runtime(example.result.elapsed)) %></span>
<% if result = example.result.as?(PendingResult) %><span class="message"><% escape(result.reason) -%></span>
<%- elsif result = example.result.as?(ErrorResult) -%>
<span class="error">
<em><% escape(result.error.class) %></em>
<% escape(result.error.message) %>
</span>
<%- if backtrace = result.error.backtrace? -%>
<div class="stacktrace">
<%- backtrace.each do |line| -%>
<code class="backtrace-line<% unless line.starts_with?(/(src|spec)\//) %> muted<% end %>"><% escape(line) %></code>
<%- end -%>
</div>
<%- end -%>
<%- elsif result = example.result.as?(FailResult) -%>
<span class="error"><% escape(result.error.message) %></span>
<%- end -%>
<%- if example.result.expectations.empty? -%>
<span class="muted">No expectations reported</span>
<%- else -%>
<div class="expectation-list">
<%- example.result.expectations.each do |expectation| -%>
<div class="expectation <%= expectation.satisfied? ? "pass" : "fail" %>">
<h4><% escape(expectation.description) %></h4>
<%- if expectation.satisfied? -%>
<span class="result">pass</span>
<%- else -%>
<span class="result">fail</span>
<span class="message"><% escape(expectation.failure_message) %></span>
<dl class="expectation-values">
<%- expectation.values.each do |key, value| -%>
<dt><% escape(key) %></dt>
<dd><% escape(value) %></dd>
<%- end -%>
</dl>
<%- end -%>
<% if location = expectation.location? %><span class="location"><% escape(location) %></span><% end %>
</div>
<%- end -%>
</div>
<%- end -%>
</div>
<%- end -%>
</div>

View file

@ -0,0 +1,2 @@
</body>
</html>

View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Test Results</title>
<meta name="generator" content="Spectator">
</head>
<body>

View file

@ -0,0 +1,69 @@
require "ecr"
require "html"
require "./formatter"
module Spectator::Formatting
# Produces an HTML document with results of the test suite.
class HTMLFormatter < Formatter
# Default HTML file name.
private OUTPUT_FILE = "output.html"
# Output stream for the HTML file.
private getter! io : IO
# Creates the formatter.
# The *output_path* can be a directory or path of an HTML file.
# If the former, then an "output.html" file will be generated in the specified directory.
def initialize(output_path = OUTPUT_FILE)
@output_path = if output_path.ends_with?(".html")
output_path
else
File.join(output_path, OUTPUT_FILE)
end
end
# Prepares the formatter for writing.
def start(_notification)
@io = File.open(@output_path, "w")
ECR.embed(__DIR__ + "/html/head.ecr", io)
end
# Invoked after testing completes with summarized information from the test suite.
# All results are gathered at the end, then the report is generated.
def dump_summary(notification)
report = notification.report # ameba:disable Lint/UselessAssign
ECR.embed(__DIR__ + "/html/body.ecr", io)
end
# Invoked at the end of the program.
# Allows the formatter to perform any cleanup and teardown.
def close
ECR.embed(__DIR__ + "/html/foot.ecr", io)
io.flush
io.close
end
private def escape(string)
HTML.escape(string.to_s, io)
end
private def runtime(span)
Components::Runtime.new(span).to_s
end
private def totals(report)
Components::Totals.new(report.counts)
end
private def summary_result(report)
counts = report.counts
if counts.fail > 0
"fail"
elsif counts.pending > 0
"pending"
else
"pass"
end
end
end
end