From dd0ef01369475d1d5d99b31dd57ff7abf584515c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 18 Aug 2021 17:50:09 -0600 Subject: [PATCH] Initial code for HTML formatter --- spec/line_number_spec.cr | 2 +- .../config/cli_arguments_applicator.cr | 9 ++ src/spectator/formatting/html/body.ecr | 85 +++++++++++++++++++ src/spectator/formatting/html/foot.ecr | 2 + src/spectator/formatting/html/head.ecr | 12 +++ src/spectator/formatting/html_formatter.cr | 69 +++++++++++++++ 6 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 src/spectator/formatting/html/body.ecr create mode 100644 src/spectator/formatting/html/foot.ecr create mode 100644 src/spectator/formatting/html/head.ecr create mode 100644 src/spectator/formatting/html_formatter.cr diff --git a/spec/line_number_spec.cr b/spec/line_number_spec.cr index 5439ea0..90491a5 100644 --- a/spec/line_number_spec.cr +++ b/spec/line_number_spec.cr @@ -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. diff --git a/src/spectator/config/cli_arguments_applicator.cr b/src/spectator/config/cli_arguments_applicator.cr index 1e2dae0..a775568 100644 --- a/src/spectator/config/cli_arguments_applicator.cr +++ b/src/spectator/config/cli_arguments_applicator.cr @@ -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 diff --git a/src/spectator/formatting/html/body.ecr b/src/spectator/formatting/html/body.ecr new file mode 100644 index 0000000..4266fb5 --- /dev/null +++ b/src/spectator/formatting/html/body.ecr @@ -0,0 +1,85 @@ +
+

Test Results

+ <% escape(Components::Totals.new(report.counts)) %> + <% escape(runtime(report.runtime)) %> +
+ +<%- if report.counts.fail > 0 -%> +
+

Failures

+ <%- report.failures.each do |example| -%> + + <%- end -%> +
+<%- end -%> + +<%- if report.counts.pending > 0 -%> +
+

Pending

+ <%- report.pending.each do |example| -%> + + <%- end -%> +
+<%- end -%> + +
+

Examples

+ <%- report.examples.each do |example| -%> +
+

<% escape(example) %>

+ <%= example.result %> + <% escape(runtime(example.result.elapsed)) %> + <% if result = example.result.as?(PendingResult) %><% escape(result.reason) -%> + + <%- elsif result = example.result.as?(ErrorResult) -%> + + <% escape(result.error.class) %> + <% escape(result.error.message) %> + + <%- if backtrace = result.error.backtrace? -%> +
+ <%- backtrace.each do |line| -%> + <% escape(line) %> + <%- end -%> +
+ <%- end -%> + + <%- elsif result = example.result.as?(FailResult) -%> + <% escape(result.error.message) %> + <%- end -%> + + <%- if example.result.expectations.empty? -%> + No expectations reported + <%- else -%> +
+ <%- example.result.expectations.each do |expectation| -%> +
"> +

<% escape(expectation.description) %>

+ <%- if expectation.satisfied? -%> + pass + <%- else -%> + fail + <% escape(expectation.failure_message) %> +
+ <%- expectation.values.each do |key, value| -%> +
<% escape(key) %>
+
<% escape(value) %>
+ <%- end -%> +
+ <%- end -%> + <% if location = expectation.location? %><% escape(location) %><% end %> +
+ <%- end -%> +
+ <%- end -%> +
+ <%- end -%> +
diff --git a/src/spectator/formatting/html/foot.ecr b/src/spectator/formatting/html/foot.ecr new file mode 100644 index 0000000..308b1d0 --- /dev/null +++ b/src/spectator/formatting/html/foot.ecr @@ -0,0 +1,2 @@ + + diff --git a/src/spectator/formatting/html/head.ecr b/src/spectator/formatting/html/head.ecr new file mode 100644 index 0000000..68570b3 --- /dev/null +++ b/src/spectator/formatting/html/head.ecr @@ -0,0 +1,12 @@ + + + + + + + + Test Results + + + + diff --git a/src/spectator/formatting/html_formatter.cr b/src/spectator/formatting/html_formatter.cr new file mode 100644 index 0000000..a4744c0 --- /dev/null +++ b/src/spectator/formatting/html_formatter.cr @@ -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