diff --git a/src/spectator/error_result.cr b/src/spectator/error_result.cr index 989b180..4babc2a 100644 --- a/src/spectator/error_result.cr +++ b/src/spectator/error_result.cr @@ -20,28 +20,9 @@ module Spectator io << "error" end - # Adds the common JSON fields for all result types - # and fields specific to errored results. - private def add_json_fields(json : ::JSON::Builder) - super - json.field("exceptions") do - exception = error - json.array do - while exception - error_to_json(exception, json) if exception - exception = error.cause - end - end - end - end - - # Adds a single exception to a JSON builder. - private def error_to_json(error : Exception, json : ::JSON::Builder) - json.object do - json.field("type", error.class.to_s) - json.field("message", error.message) - json.field("backtrace", error.backtrace) - end + # String used for the JSON status field. + private def json_status + "error" end end end diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 3cca701..040b2ca 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -165,8 +165,14 @@ module Spectator # Creates the JSON representation of the example, # which is just its name. - def to_json(json : ::JSON::Builder) - json.string(to_s) + def to_json(json : JSON::Builder) + json.object do + json.field("description", name? || "") + json.field("full_description", to_s) + json.field("file_path", location.path) + json.field("line_number", location.line) + @result.to_json(json) if @finished + end end # Creates a procsy from this example and the provided block. diff --git a/src/spectator/expectation.cr b/src/spectator/expectation.cr index e7324dc..55623d5 100644 --- a/src/spectator/expectation.cr +++ b/src/spectator/expectation.cr @@ -1,3 +1,4 @@ +require "json" require "./expression" require "./location" @@ -53,7 +54,7 @@ module Spectator end # Creates the JSON representation of the expectation. - def to_json(json : ::JSON::Builder) + def to_json(json : JSON::Builder) json.object do json.field("location") { @location.to_json(json) } json.field("satisfied", satisfied?) @@ -64,7 +65,7 @@ module Spectator end # Adds failure information to a JSON structure. - private def failed_to_json(failed : Matchers::FailedMatchData, json : ::JSON::Builder) + private def failed_to_json(failed : Matchers::FailedMatchData, json : JSON::Builder) json.field("failure", failed.failure_message) json.field("values") do json.object do diff --git a/src/spectator/fail_result.cr b/src/spectator/fail_result.cr index 5bc0f06..41a4aba 100644 --- a/src/spectator/fail_result.cr +++ b/src/spectator/fail_result.cr @@ -1,3 +1,4 @@ +require "json" require "./result" module Spectator @@ -40,10 +41,23 @@ module Spectator io << "fail" end - # Adds all of the JSON fields for finished results and failed results. - private def add_json_fields(json : ::JSON::Builder) + # Creates a JSON object from the result information. + def to_json(json : JSON::Builder) super - json.field("error", error.message) + json.field("status", json_status) + json.field("exception") do + json.object do + json.field("class", @error.class.name) + json.field("message", @error.message) + json.field("backtrace", @error.backtrace) + end + end + end + + # String used for the JSON status field. + # Necessary for the error result to override the status, but nothing else from `#to_json`. + private def json_status + "failed" end end end diff --git a/src/spectator/formatting/json_formatter.cr b/src/spectator/formatting/json_formatter.cr index 996e94a..46c8a88 100644 --- a/src/spectator/formatting/json_formatter.cr +++ b/src/spectator/formatting/json_formatter.cr @@ -1,6 +1,141 @@ +require "json" require "./formatter" module Spectator::Formatting + # Produces a JSON document with results of the test suite. class JSONFormatter < Formatter + # Creates the formatter. + # By default, output is sent to STDOUT. + def initialize(io = STDOUT) + @json = JSON::Builder.new(io) + end + + # Begins the JSON document output. + def start(_notification) + @json.start_document + @json.start_object + @json.field("version", Spectator::VERSION) + + # Start examples array. + @json.string("examples") + @json.start_array + end + + # Begins an example object and adds common fields known before running the example. + def example_started(notification) + example = notification.example + + @json.start_object + @json.field("description", example.name? || "") + @json.field("full_description", example) + @json.field("file_path", example.location.path) + @json.field("line_number", example.location.line) + end + + # Adds fields to the example object for all result types known after the example completes. + def example_finished(notification) + example = notification.example + result = example.result + + @json.field("run_time", result.elapsed.total_seconds) + @json.field("expectations") do + @json.array do + result.expectations.each(&.to_json(@json)) + end + end + end + + # Adds success-specific fields to an example object and closes it. + def example_passed(_notification) + @json.field("status", "passed") + @json.end_object # End example object. + end + + # Adds pending-specific fields to an example object and closes it. + def example_pending(_notification) + @json.field("status", "pending") + @json.field("pending_message", "Not implemented") # TODO: Fetch pending message from result. + @json.end_object # End example object. + end + + # Adds failure-specific fields to an example object and closes it. + def example_failed(notification) + example = notification.example + result = example.result + + @json.field("status", "failed") + build_exception_object(result.error) if result.responds_to?(:error) + @json.end_object # End example object. + end + + # Adds error-specific fields to an example object and closes it. + def example_error(notification) + example = notification.example + result = example.result + + @json.field("status", "error") + build_exception_object(result.error) if result.responds_to?(:error) + @json.end_object # End example object. + end + + # Adds an exception field and object to the JSON document. + private def build_exception_object(error) + @json.field("exception") do + @json.object do + @json.field("class", error.class.name) + @json.field("message", error.message) + @json.field("backtrace", error.backtrace) + end + end + end + + # Marks the end of the examples array. + def stop + @json.end_array # Close examples array. + end + + # Adds the profiling information to the document. + def dump_profile(notification) + profile = notification.profile + + @json.field("profile") do + @json.object do + @json.field("examples") do + @json.array do + profile.each(&.to_json(@json)) + end + end + + @json.field("slowest", profile.max_of(&.result.elapsed).total_seconds) + @json.field("total", profile.time.total_seconds) + @json.field("percentage", profile.percentage) + end + end + end + + # Adds the summary object to the document. + def dump_summary(notification) + report = notification.report + + @json.field("summary") do + @json.object do + @json.field("duration", report.runtime.total_seconds) + @json.field("example_count", report.counts.total) + @json.field("failure_count", report.counts.fail) + @json.field("error_count", report.counts.error) + @json.field("pending_count", report.counts.pending) + end + end + + totals = Components::Totals.new(report.counts) + @json.field("summary_line", totals.to_s) + end + + # Ends the JSON document and flushes output. + def close + @json.end_object + @json.end_document + @json.flush + end end end diff --git a/src/spectator/pass_result.cr b/src/spectator/pass_result.cr index eb2bfcd..2b62383 100644 --- a/src/spectator/pass_result.cr +++ b/src/spectator/pass_result.cr @@ -27,5 +27,11 @@ module Spectator def to_s(io) io << "pass" end + + # Creates a JSON object from the result information. + def to_json(json : JSON::Builder) + super + json.field("status", "passed") + end end end diff --git a/src/spectator/pending_result.cr b/src/spectator/pending_result.cr index d1d6f2c..11ae93e 100644 --- a/src/spectator/pending_result.cr +++ b/src/spectator/pending_result.cr @@ -35,5 +35,12 @@ module Spectator def to_s(io) io << "pending" end + + # Creates a JSON object from the result information. + def to_json(json : JSON::Builder) + super + json.field("status", "pending") + json.field("pending_message", "Not implemented") # TODO: Provide pending message. + end end end diff --git a/src/spectator/result.cr b/src/spectator/result.cr index 49ed947..2da334d 100644 --- a/src/spectator/result.cr +++ b/src/spectator/result.cr @@ -1,3 +1,6 @@ +require "json" +require "./expectation" + module Spectator # Base class that represents the outcome of running an example. # Sub-classes contain additional information specific to the type of result. @@ -29,19 +32,13 @@ module Spectator end # Creates a JSON object from the result information. - def to_json(json : ::JSON::Builder, example) - json.object do - add_json_fields(json, example) + def to_json(json : JSON::Builder) + json.field("run_time", @elapsed.total_seconds) + json.field("expectations") do + json.array do + @expectations.each(&.to_json(json)) + end end end - - # Adds the common fields for a result to a JSON builder. - private def add_json_fields(json : ::JSON::Builder, example) - json.field("name", example) - json.field("location", example.location) - json.field("result", to_s) - json.field("time", elapsed.total_seconds) - json.field("expectations", expectations) - end end end