From f679c3d5eaf512d0d697aced6381e8f196e1e539 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 6 Jul 2020 21:51:51 -0600 Subject: [PATCH 001/399] Update Crystal version --- shard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shard.yml b/shard.yml index b34a636..d0038ce 100644 --- a/shard.yml +++ b/shard.yml @@ -6,7 +6,7 @@ description: | authors: - Michael Miller -crystal: 0.34.0 +crystal: 0.35.1 license: MIT From 53c9dd0445530cb5198364fb950309abb23d88b5 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 Aug 2020 10:00:04 -0600 Subject: [PATCH 002/399] Display first line only after "Error:" --- src/spectator/formatting/failure_block.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/spectator/formatting/failure_block.cr b/src/spectator/formatting/failure_block.cr index b542775..5ee5aaa 100644 --- a/src/spectator/formatting/failure_block.cr +++ b/src/spectator/formatting/failure_block.cr @@ -77,7 +77,8 @@ module Spectator::Formatting # Produces the stack trace for an errored result. private def error_stacktrace(indent) error = @result.error - indent.line(Color.error(LabeledText.new("Error", error))) + first_line = error.message.try(&.lines).try(&.first) + indent.line(Color.error(LabeledText.new("Error", first_line))) indent.line indent.increase do loop do From 5688e58025ab9da725c2030af65fff5998fb1f87 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 Aug 2020 10:59:15 -0600 Subject: [PATCH 003/399] Initial runtime test compilation Allows for compiling single examples at runtime. --- spec/helpers/example.cr | 71 ++++++++++++++++++++++++++++++++++++ spec/helpers/example.ecr | 5 +++ spec/helpers/result.cr | 59 ++++++++++++++++++++++++++++++ spec/runtime_example_spec.cr | 42 +++++++++++++++++++++ spec/spec_helper.cr | 15 ++++++++ 5 files changed, 192 insertions(+) create mode 100644 spec/helpers/example.cr create mode 100644 spec/helpers/example.ecr create mode 100644 spec/helpers/result.cr create mode 100644 spec/runtime_example_spec.cr diff --git a/spec/helpers/example.cr b/spec/helpers/example.cr new file mode 100644 index 0000000..8ad8d46 --- /dev/null +++ b/spec/helpers/example.cr @@ -0,0 +1,71 @@ +require "ecr" +require "json" +require "./result" + +module Spectator::SpecHelpers + # Wrapper for compiling and running an example at runtime and getting a result. + class Example + # Creates the example. + # The *spec_helper_path* is the path to spec_helper.cr file. + # The name or ID of the example is given by *example_id*. + # Lastly, the source code for the example is given by *example_code*. + def initialize(@spec_helper_path : String, @example_id : String, @example_code : String) + end + + # Instructs the Crystal compiler to compile the test. + # Returns an instance of `JSON::Any`. + # This will be the outcome and information about the test. + # Output will be surpressed for the test. + # If an error occurs while attempting to compile and run the test, an error will be raised. + def compile + # Create a temporary file containing the test. + with_tempfile do |source_file| + args = ["run", "--no-color", source_file, "--", "--json"] + Process.run(crystal_executable, args) do |process| + JSON.parse(process.output) + rescue JSON::ParseException + raise "Compilation of example #{@example_id} failed\n\n#{process.error.gets_to_end}" + end + end + end + + # Same as `#compile`, but returns the result of the first example in the test. + # Returns a `SpectatorHelpers::Result` instance. + def result + output = compile + example = output["examples"][0] + Result.from_json_any(example) + end + + # Constructs the string representation of the example. + # This produces the Crystal source code. + # *io* is the file handle to write to. + # The *dir* is the directory of the file being written to. + # This is needed to resolve the relative path to the spec_helper.cr file. + private def write(io, dir) + spec_helper_path = Path[@spec_helper_path].relative_to(dir) + ECR.embed(__DIR__ + "/example.ecr", io) + end + + # Creates a temporary file containing the compilable example code. + # Yields the path of the temporary file. + # Ensures the file is deleted after it is done being used. + private def with_tempfile + tempfile = File.tempfile("_#{@example_id}_spec.cr") do |file| + dir = File.dirname(file.path) + write(file, dir) + end + + begin + yield tempfile.path + ensure + tempfile.delete + end + end + + # Attempts to find the Crystal compiler on the system or raises an error. + private def crystal_executable + Process.find_executable("crystal") || raise("Could not find Crystal compiler") + end + end +end diff --git a/spec/helpers/example.ecr b/spec/helpers/example.ecr new file mode 100644 index 0000000..53355bf --- /dev/null +++ b/spec/helpers/example.ecr @@ -0,0 +1,5 @@ +require "<%= spec_helper_path %>" + +Spectator.describe "<%= @example_id %>" do + <%= @example_code %> +end diff --git a/spec/helpers/result.cr b/spec/helpers/result.cr new file mode 100644 index 0000000..76aa48d --- /dev/null +++ b/spec/helpers/result.cr @@ -0,0 +1,59 @@ +module Spectator::SpecHelpers + # Information about an example compiled and run at runtime. + class Result + # Status of the example after running. + enum Outcome + Success + Failure + Error + Unknown + end + + # Full name and description of the example. + getter name : String + + # Status of the example after running. + getter outcome : Outcome + + # Creates the result. + def initialize(@name, @outcome) + end + + # Checks if the example was successful. + def success? + outcome.success? + end + + # :ditto: + def successful? + outcome.success? + end + + # Checks if the example failed, but did not error. + def failure? + outcome.failure? + end + + # Checks if the example encountered an error. + def error? + outcome.error? + end + + # Extracts the result information from a `JSON::Any` object. + def self.from_json_any(object : JSON::Any) + name = object["name"].as_s + outcome = parse_outcome_string(object["result"].as_s) + new(name, outcome) + end + + # Converts a result string, such as "fail" to an enum value. + private def self.parse_outcome_string(string) + case string + when /success/i then Outcome::Success + when /fail/i then Outcome::Failure + when /error/i then Outcome::Error + else Outcome::Unknown + end + end + end +end diff --git a/spec/runtime_example_spec.cr b/spec/runtime_example_spec.cr new file mode 100644 index 0000000..a6a4ecd --- /dev/null +++ b/spec/runtime_example_spec.cr @@ -0,0 +1,42 @@ +require "./spec_helper" + +# This is a meta test that ensures specs can be compiled and run at runtime. +# The purpose of this is to report an error if this process fails. +# Other tests will fail, but display a different name/description of the test. +# This clearly indicates that runtime testing failed. +# +# Runtime compilation is used to get output of tests as well as check syntax. +# Some specs are too complex to be ran normally. +# Additionally, this allows examples to easily check specific failure cases. +# Plus, it makes testing user-reported issues easy. +Spectator.describe "Runtime compilation" do + given_example passing_example do + it "does something" do + expect(true).to be_true + end + end + + it "can compile and retrieve the result of an example" do + expect(passing_example).to be_successful + end + + given_example failing_example do + it "does something" do + expect(true).to be_false + end + end + + it "detects failed examples" do + expect(failing_example).to be_failure + end + + given_example malformed_example do + it "does something" do + asdf + end + end + + it "raises on compilation errors" do + expect { malformed_example }.to raise_error(/compilation/i) + end +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index f8047da..865a6b3 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -1,4 +1,5 @@ require "../src/spectator" +require "./helpers/**" macro it_fails(description = nil, &block) it {{description}} do @@ -11,3 +12,17 @@ end macro specify_fails(description = nil, &block) it_fails {{description}} {{block}} end + +# Defines an example ("it" block) that is lazily compiled. +# When the example is referenced with *id*, it will be compiled and the results retrieved. +# The value returned by *id* will be a `Spectator::SpecHelpers::Result`. +# This allows the test result to be inspected. +macro given_example(id, &block) + let({{id}}) do + ::Spectator::SpecHelpers::Example.new( + {{__FILE__}}, + {{id.id.stringify}}, + {{block.body.stringify}} + ).result + end +end From 14608c8b2dbcb85d69061354635237ea2c271210 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 Aug 2020 11:00:46 -0600 Subject: [PATCH 004/399] Change to struct --- spec/helpers/result.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/helpers/result.cr b/spec/helpers/result.cr index 76aa48d..f36f104 100644 --- a/spec/helpers/result.cr +++ b/spec/helpers/result.cr @@ -1,6 +1,6 @@ module Spectator::SpecHelpers # Information about an example compiled and run at runtime. - class Result + struct Result # Status of the example after running. enum Outcome Success From fab216419c72387eeb4a86b8970872dfe6339521 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 Aug 2020 11:39:54 -0600 Subject: [PATCH 005/399] Capture expectations --- spec/helpers/expectation.cr | 28 ++++++++++++++++++++++++++++ spec/helpers/result.cr | 18 +++++++++++++----- spec/runtime_example_spec.cr | 4 ++++ 3 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 spec/helpers/expectation.cr diff --git a/spec/helpers/expectation.cr b/spec/helpers/expectation.cr new file mode 100644 index 0000000..fd4d84d --- /dev/null +++ b/spec/helpers/expectation.cr @@ -0,0 +1,28 @@ +module Spectator::SpecHelpers + # Information about an `expect` call in an example. + struct Expectation + # Indicates whether the expectation passed or failed. + getter? satisfied : Bool + + # Message when the expectation failed. + # Only available when `#satisfied?` is false. + getter! message : String + + # Additional information about the expectation. + # Only available when `#satisfied?` is false. + getter! values : Hash(String, String) + + # Creates the expectation outcome. + def initialize(@satisfied, @message, @values) + end + + # Extracts the expectation information from a `JSON::Any` object. + def self.from_json_any(object : JSON::Any) + satisfied = object["satisfied"].as_bool + message = object["failure"]?.try(&.as_s?) + values = object["values"]?.try(&.as_h?) + values = values.transform_values(&.as_s) if values + new(satisfied, message, values) + end + end +end diff --git a/spec/helpers/result.cr b/spec/helpers/result.cr index f36f104..aeabf3d 100644 --- a/spec/helpers/result.cr +++ b/spec/helpers/result.cr @@ -15,8 +15,11 @@ module Spectator::SpecHelpers # Status of the example after running. getter outcome : Outcome + # List of expectations ran in the example. + getter expectations : Array(Expectation) + # Creates the result. - def initialize(@name, @outcome) + def initialize(@name, @outcome, @expectations) end # Checks if the example was successful. @@ -43,16 +46,21 @@ module Spectator::SpecHelpers def self.from_json_any(object : JSON::Any) name = object["name"].as_s outcome = parse_outcome_string(object["result"].as_s) - new(name, outcome) + expectations = if (list = object["expectations"].as_a?) + list.map { |e| Expectation.from_json_any(e) } + else + [] of Expectation + end + new(name, outcome, expectations) end # Converts a result string, such as "fail" to an enum value. private def self.parse_outcome_string(string) case string when /success/i then Outcome::Success - when /fail/i then Outcome::Failure - when /error/i then Outcome::Error - else Outcome::Unknown + when /fail/i then Outcome::Failure + when /error/i then Outcome::Error + else Outcome::Unknown end end end diff --git a/spec/runtime_example_spec.cr b/spec/runtime_example_spec.cr index a6a4ecd..9fedc28 100644 --- a/spec/runtime_example_spec.cr +++ b/spec/runtime_example_spec.cr @@ -20,6 +20,10 @@ Spectator.describe "Runtime compilation" do expect(passing_example).to be_successful end + it "can retrieve expectations" do + expect(passing_example.expectations).to_not be_empty + end + given_example failing_example do it "does something" do expect(true).to be_false From 62fd289b0f37a53f225a20a1da0a1590c68f306a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 Aug 2020 12:04:45 -0600 Subject: [PATCH 006/399] Add ability to test expectations directly --- spec/runtime_example_spec.cr | 12 ++++++++++++ spec/spec_helper.cr | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/spec/runtime_example_spec.cr b/spec/runtime_example_spec.cr index 9fedc28..324f7ea 100644 --- a/spec/runtime_example_spec.cr +++ b/spec/runtime_example_spec.cr @@ -28,6 +28,10 @@ Spectator.describe "Runtime compilation" do it "does something" do expect(true).to be_false end + + it "doesn't run" do + expect(true).to be_false + end end it "detects failed examples" do @@ -43,4 +47,12 @@ Spectator.describe "Runtime compilation" do it "raises on compilation errors" do expect { malformed_example }.to raise_error(/compilation/i) end + + given_expectation satisfied_expectation do + expect(true).to be_true + end + + it "can compile and retrieve expectations" do + expect(satisfied_expectation).to be_satisfied + end end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 865a6b3..736ac91 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -26,3 +26,21 @@ macro given_example(id, &block) ).result end end + +# Defines an example ("it" block) that is lazily compiled. +# The "it" block must be omitted, as the block provided to this macro will be wrapped in one. +# When the expectation is referenced with *id*, it will be compiled and the result retrieved. +# The value returned by *id* will be a `Spectator::SpecHelpers::Expectation`. +# This allows an expectation to be inspected. +# Only the last expectation performed will be returned. +# An error is raised if no expectations ran. +macro given_expectation(id, &block) + let({{id}}) do + result = ::Spectator::SpecHelpers::Example.new( + {{__FILE__}}, + {{id.id.stringify}}, + {{"it do\n" + block.body.stringify + "\nend"}} + ).result + result.expectations.last || raise("No expectations found from {{id.id}}") + end +end From d31b8f40935904a1c46fd5764045b73ec674472e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 Aug 2020 12:10:32 -0600 Subject: [PATCH 007/399] Disable false warning from Ameba spec_helper_path is actually used by the ECR template. --- spec/helpers/example.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/helpers/example.cr b/spec/helpers/example.cr index 8ad8d46..dca1042 100644 --- a/spec/helpers/example.cr +++ b/spec/helpers/example.cr @@ -43,7 +43,7 @@ module Spectator::SpecHelpers # The *dir* is the directory of the file being written to. # This is needed to resolve the relative path to the spec_helper.cr file. private def write(io, dir) - spec_helper_path = Path[@spec_helper_path].relative_to(dir) + spec_helper_path = Path[@spec_helper_path].relative_to(dir) # ameba:disable Lint/UselessAssign ECR.embed(__DIR__ + "/example.ecr", io) end From 9c6502234ba93c79aa2354dff12297c010024c76 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 5 Sep 2020 14:55:49 -0600 Subject: [PATCH 008/399] Define test context types --- src/spectator/context.cr | 8 ++++++++ src/spectator/context_delegate.cr | 18 ++++++++++++++++++ src/spectator/context_method.cr | 10 ++++++++++ src/spectator_contnext.cr | 7 +++++++ 4 files changed, 43 insertions(+) create mode 100644 src/spectator/context.cr create mode 100644 src/spectator/context_delegate.cr create mode 100644 src/spectator/context_method.cr create mode 100644 src/spectator_contnext.cr diff --git a/src/spectator/context.cr b/src/spectator/context.cr new file mode 100644 index 0000000..d2c5d55 --- /dev/null +++ b/src/spectator/context.cr @@ -0,0 +1,8 @@ +require "../spectator_context" + +module Spectator + # Base class that all test cases run in. + # This type is used to store all test case contexts as a single type. + # The instance must be downcast to the correct type before calling a context method. + alias Context = ::SpectatorContext +end diff --git a/src/spectator/context_delegate.cr b/src/spectator/context_delegate.cr new file mode 100644 index 0000000..1beef83 --- /dev/null +++ b/src/spectator/context_delegate.cr @@ -0,0 +1,18 @@ +require "./context" +require "./context_method" + +module Spectator + # Stores a test context and a method to call within it. + struct ContextDelegate + # Creates the delegate. + # The *context* is the instance of the test context. + # The *method* is proc that downcasts *context* and calls a method on it. + def initialize(@context : Context, @method : ContextMethod) + end + + # Invokes a method in the test context. + def call + @method.call(@context) + end + end +end diff --git a/src/spectator/context_method.cr b/src/spectator/context_method.cr new file mode 100644 index 0000000..e1c9aa8 --- /dev/null +++ b/src/spectator/context_method.cr @@ -0,0 +1,10 @@ +require "./context" + +module Spectator + # Encapsulates a method in a context. + # This could be used to invoke a test case or hook method. + # The context is passed as an argument. + # The proc should downcast the context instance to the desired type + # and call a method on that context. + alias ContextMethod = Context -> +end diff --git a/src/spectator_contnext.cr b/src/spectator_contnext.cr new file mode 100644 index 0000000..36ff503 --- /dev/null +++ b/src/spectator_contnext.cr @@ -0,0 +1,7 @@ +# Base class that all test cases run in. +# This type is used to store all test case contexts as a single type. +# The instance must be downcast to the correct type before calling a context method. +# This type is intentionally outside the `Spectator` module. +# The reason for this is to prevent name collision when using the DSL to define a spec. +abstract class SpectatorContext +end From 55900ebecd03f5ee4c48cd28c20b90733bd0d531 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 5 Sep 2020 15:01:22 -0600 Subject: [PATCH 009/399] Initial rework of example type structure --- src/spectator/example.cr | 97 ++++++--------------- src/spectator/example_base.cr | 85 +++++++++++++++++++ src/spectator/example_component.cr | 26 ------ src/spectator/example_group.cr | 131 ----------------------------- 4 files changed, 109 insertions(+), 230 deletions(-) create mode 100644 src/spectator/example_base.cr delete mode 100644 src/spectator/example_component.cr delete mode 100644 src/spectator/example_group.cr diff --git a/src/spectator/example.cr b/src/spectator/example.cr index cc955d1..33ac453 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -1,83 +1,34 @@ -require "./example_component" -require "./test_wrapper" +require "./context_delegate" +require "./example_base" +require "./example_group" +require "./result" +require "./source" module Spectator - # Base class for all types of examples. - # Concrete types must implement the `#run_impl` method. - abstract class Example < ExampleComponent - @finished = false - @description : String? = nil + # Standard example that runs a test case. + class Example < ExampleBase + # Indicates whether the example already ran. + getter? finished : Bool = false - protected setter description + # Retrieves the result of the last time the example ran. + getter! result : Result - # Indicates whether the example has already been run. - def finished? : Bool - @finished + # Creates the example. + # The *delegate* contains the test context and method that runs the test case. + # The *name* describes the purpose of the example. + # It can be a `Symbol` to describe a type. + # The *source* tracks where the example exists in source code. + # The example will be assigned to *group* if it is provided. + def initialize(@delegate : ContextDelegate, + name : String | Symbol? = nil, source : Source? = nil, group : ExampleGroup? = nil) + super(name, source, group) end - # Group that the example belongs to. - getter group : ExampleGroup - - # Retrieves the internal wrapped instance. - protected getter test_wrapper : TestWrapper - - # Source where the example originated from. - def source : Source - @test_wrapper.source - end - - def description : String | Symbol - @description || @test_wrapper.description - end - - def symbolic? : Bool - return false unless @test_wrapper.description? - - description = @test_wrapper.description - description.starts_with?('#') || description.starts_with?('.') - end - - abstract def run_impl - - # Runs the example code. - # A result is returned, which represents the outcome of the test. - # An example can be run only once. - # An exception is raised if an attempt is made to run it more than once. + # Executes the test case. + # Returns the result of the execution. + # The result will also be stored in `#result`. def run : Result - raise "Attempted to run example more than once (#{self})" if finished? - run_impl - ensure - @finished = true - end - - # Creates the base of the example. - # The group should be the example group the example belongs to. - def initialize(@group, @test_wrapper) - end - - # Indicates there is only one example to run. - def example_count : Int - 1 - end - - # Retrieve the current example. - def [](index : Int) : Example - self - end - - # String representation of the example. - # This consists of the groups the example is in and the description. - # The string can be given to end-users to identify the example. - def to_s(io) - @group.to_s(io) - io << ' ' unless symbolic? && @group.symbolic? - io << description - end - - # Creates the JSON representation of the example, - # which is just its name. - def to_json(json : ::JSON::Builder) - json.string(to_s) + raise NotImplementedError.new("#run") end end end diff --git a/src/spectator/example_base.cr b/src/spectator/example_base.cr new file mode 100644 index 0000000..ef2a23b --- /dev/null +++ b/src/spectator/example_base.cr @@ -0,0 +1,85 @@ +require "./example_group" +require "./result" +require "./source" + +module Spectator + # Common base type for all examples. + abstract class ExampleBase + # Location of the example in source code. + getter! source : Source + + # User-provided name or description of the test. + # This does not include the group name or descriptions. + # Use `#to_s` to get the full name. + # + # This value will be nil if no name was provided. + # In this case, the name should be set + # to the description of the first matcher that runs in the example. + # + # If this value is a `Symbol`, the user specified a type for the name. + getter! name : String | Symbol + + # Group the example belongs to. + # Hooks are used from this group. + getter! group : ExampleGroup + + # Assigns the group the example belongs to. + # If the example already belongs to a group, + # it will be removed from the previous group before adding it to the new group. + def group=(group : ExampleGroup?) + if (previous = @group) + previous.remove_example(self) + end + group.add_example(self) if group + @group = group + end + + # Creates the base of the example. + # The *name* describes the purpose of the example. + # It can be a `Symbol` to describe a type. + # The *source* tracks where the example exists in source code. + # The example will be assigned to *group* if it is provided. + def initialize(@name : String | Symbol? = nil, @source : Source? = nil, group : ExampleGroup? = nil) + # Ensure group is linked. + self.group = group + end + + # Indicates whether the example already ran. + abstract def finished? : Bool + + # Retrieves the result of the last time the example ran. + # This will be nil if the example hasn't run, + # and should not be nil if it has. + abstract def result? : Result? + + # Retrieves the result of the last time the example ran. + # Raises an error if the example hasn't run. + def result : Result + result? || raise(NilAssertionError("Example has no result")) + end + + # Constructs the full name or description of the example. + # This prepends names of groups this example is part of. + def to_s(io) + name = @name + + # Prefix with group's full name if the example belongs to a group. + if (group = @group) + group.to_s(io) + + # Add padding between the group name and example name, + # only if the names appear to be symbolic. + if group.name.is_a?(Symbol) && name.is_a?(String) + io << ' ' unless name.starts_with?('#') || name.starts_with?('.') + end + end + + name.to_s(io) + end + + # Exposes information about the example useful for debugging. + def inspect(io) + raise NotImplementedError.new("#inspect") + end + end +end diff --git a/src/spectator/example_component.cr b/src/spectator/example_component.cr deleted file mode 100644 index 9260f95..0000000 --- a/src/spectator/example_component.cr +++ /dev/null @@ -1,26 +0,0 @@ -module Spectator - # Abstract base for all examples and collections of examples. - # This is used as the base node type for the composite design pattern. - abstract class ExampleComponent - # Text that describes the context or test. - abstract def description : Symbol | String - - def full_description - to_s - end - - abstract def source : Source - - # Indicates whether the example (or group) has been completely run. - abstract def finished? : Bool - - # The number of examples in this instance. - abstract def example_count : Int - - # Lookup the example with the specified index. - abstract def [](index : Int) : Example - - # Indicates that the component references a type or method. - abstract def symbolic? : Bool - end -end diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr deleted file mode 100644 index ae8a5a8..0000000 --- a/src/spectator/example_group.cr +++ /dev/null @@ -1,131 +0,0 @@ -require "./example_component" - -module Spectator - # Shared base class for groups of examples. - # - # Represents a collection of examples and other groups. - # Use the `#each` methods to iterate through each child. - # However, these methods do not recurse into sub-groups. - # If you need that functionality, see `ExampleIterator`. - # Additionally, the indexer method (`#[]`) will index into sub-groups. - # - # This class also stores hooks to be associated with all examples in the group. - # The hooks can be invoked by running the `#run_before_hooks` and `#run_after_hooks` methods. - abstract class ExampleGroup < ExampleComponent - include Enumerable(ExampleComponent) - include Iterable(ExampleComponent) - - @example_count = 0 - - # Retrieves the children in the group. - # This only returns the direct descends (non-recursive). - # The children must be set (with `#children=`) prior to calling this method. - getter! children : Array(ExampleComponent) - - # Sets the children of the group. - # This should be called only from a builder in the `DSL` namespace. - # The children can be set only once - - # attempting to set more than once will raise an error. - # All sub-groups' children should be set before setting this group's children. - def children=(children : Array(ExampleComponent)) - raise "Attempted to reset example group children" if @children - @children = children - # Recursively count the number of examples. - # This won't work if a sub-group hasn't had their children set (is still nil). - @example_count = children.sum(&.example_count) - end - - def double(id, sample_values) - @doubles[id].build(sample_values) - end - - getter context - - def initialize(@context : TestContext) - end - - # Yields each direct descendant. - def each - children.each do |child| - yield child - end - end - - # Returns an iterator for each direct descendant. - def each : Iterator(ExampleComponent) - children.each - end - - # Number of examples in this group and all sub-groups. - def example_count : Int - @example_count - end - - # Retrieves an example by its index. - # This recursively searches for an example. - # - # Positive and negative indices can be used. - # Any value out of range will raise an `IndexError`. - # - # Examples are indexed as if they are in a flattened tree. - # For instance: - # ``` - # examples = [0, 1, [2, 3, 4], [5, [6, 7], 8], 9, [10]].flatten - # ``` - # The arrays symbolize groups, - # and the numbers are the index of the example in that slot. - def [](index : Int) : Example - offset = check_bounds(index) - find_nested(offset) - end - - # Checks whether an index is within acceptable bounds. - # If the index is negative, - # it will be converted to its positive equivalent. - # If the index is out of bounds, an `IndexError` is raised. - # If the index is in bounds, - # the positive index is returned. - private def check_bounds(index) - if index < 0 - raise IndexError.new if index < -example_count - index + example_count - else - raise IndexError.new if index >= example_count - index - end - end - - # Finds the example with the specified index in the children. - # The *index* must be positive and within bounds (use `#check_bounds`). - private def find_nested(index) - offset = index - # Loop through each child - # until one is found to contain the index. - found = children.each do |child| - count = child.example_count - # Example groups consider their range to be [0, example_count). - # Each child is offset by the total example count of the previous children. - # The group exposes them in this way: - # 1. [0, example_count of group 1) - # 2. [example_count of group 1, example_count of group 2) - # 3. [example_count of group n, example_count of group n + 1) - # To iterate through children, the offset is tracked. - # Each iteration removes the previous child's count. - # This way the child receives the expected range. - break child if offset < count - offset -= count - end - # The remaining offset is passed along to the child. - # If it's an `Example`, it returns itself. - # Otherwise, the indexer repeats the process for the next child. - # It should be impossible to get nil here, - # provided the bounds check and example counts are correct. - found.not_nil![offset] - end - - # Checks whether all examples in the group have been run. - def finished? : Bool - children.all?(&.finished?) - end - end -end From 4debebb8f09319cbac8394c4de1608d3ebe83225 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 5 Sep 2020 15:55:28 -0600 Subject: [PATCH 010/399] Formatting --- src/spectator/example.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 33ac453..88c333c 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -20,7 +20,7 @@ module Spectator # The *source* tracks where the example exists in source code. # The example will be assigned to *group* if it is provided. def initialize(@delegate : ContextDelegate, - name : String | Symbol? = nil, source : Source? = nil, group : ExampleGroup? = nil) + name : String | Symbol? = nil, source : Source? = nil, group : ExampleGroup? = nil) super(name, source, group) end From 0f9c1ad09c43c76736289d5664c8e6ea342db8e6 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 5 Sep 2020 16:36:12 -0600 Subject: [PATCH 011/399] Add require for json --- src/spectator/source.cr | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/spectator/source.cr b/src/spectator/source.cr index fa8935d..628db29 100644 --- a/src/spectator/source.cr +++ b/src/spectator/source.cr @@ -1,3 +1,5 @@ +require "json" + module Spectator # Define the file and line number something originated from. struct Source From fbf574b0b94e27c52bc4a47dd76ff8a51436a1e8 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 5 Sep 2020 16:47:40 -0600 Subject: [PATCH 012/399] Create ExampleGroup and use shared ExampleNode type --- src/spectator/example_base.cr | 66 ++-------------------------------- src/spectator/example_group.cr | 38 ++++++++++++++++++++ src/spectator/example_node.cr | 64 +++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 64 deletions(-) create mode 100644 src/spectator/example_group.cr create mode 100644 src/spectator/example_node.cr diff --git a/src/spectator/example_base.cr b/src/spectator/example_base.cr index ef2a23b..0944cbb 100644 --- a/src/spectator/example_base.cr +++ b/src/spectator/example_base.cr @@ -1,52 +1,9 @@ -require "./example_group" +require "./example_node" require "./result" -require "./source" module Spectator # Common base type for all examples. - abstract class ExampleBase - # Location of the example in source code. - getter! source : Source - - # User-provided name or description of the test. - # This does not include the group name or descriptions. - # Use `#to_s` to get the full name. - # - # This value will be nil if no name was provided. - # In this case, the name should be set - # to the description of the first matcher that runs in the example. - # - # If this value is a `Symbol`, the user specified a type for the name. - getter! name : String | Symbol - - # Group the example belongs to. - # Hooks are used from this group. - getter! group : ExampleGroup - - # Assigns the group the example belongs to. - # If the example already belongs to a group, - # it will be removed from the previous group before adding it to the new group. - def group=(group : ExampleGroup?) - if (previous = @group) - previous.remove_example(self) - end - group.add_example(self) if group - @group = group - end - - # Creates the base of the example. - # The *name* describes the purpose of the example. - # It can be a `Symbol` to describe a type. - # The *source* tracks where the example exists in source code. - # The example will be assigned to *group* if it is provided. - def initialize(@name : String | Symbol? = nil, @source : Source? = nil, group : ExampleGroup? = nil) - # Ensure group is linked. - self.group = group - end - - # Indicates whether the example already ran. - abstract def finished? : Bool - + abstract class ExampleBase < ExampleNode # Retrieves the result of the last time the example ran. # This will be nil if the example hasn't run, # and should not be nil if it has. @@ -58,25 +15,6 @@ module Spectator result? || raise(NilAssertionError("Example has no result")) end - # Constructs the full name or description of the example. - # This prepends names of groups this example is part of. - def to_s(io) - name = @name - - # Prefix with group's full name if the example belongs to a group. - if (group = @group) - group.to_s(io) - - # Add padding between the group name and example name, - # only if the names appear to be symbolic. - if group.name.is_a?(Symbol) && name.is_a?(String) - io << ' ' unless name.starts_with?('#') || name.starts_with?('.') - end - end - - name.to_s(io) - end - # Exposes information about the example useful for debugging. def inspect(io) raise NotImplementedError.new("#inspect") diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr new file mode 100644 index 0000000..01b64ce --- /dev/null +++ b/src/spectator/example_group.cr @@ -0,0 +1,38 @@ +module Spectator + # Collection of examples and sub-groups. + class ExampleGroup < ExampleNode + include Enumerable(ExampleNode) + + @nodes = [] of ExampleNode + + # Removes the specified *node* from the group. + # The node will be unassigned from this group. + def delete(node : ExampleNode) + # Only remove from the group if it is associated with this group. + return unless node.group == self + + node.group = nil + @nodes.delete(node) + end + + # Yields each node (example and sub-group). + def each + @nodes.each { |node| yield node } + end + + # Adds the specified *node* to the group. + # Assigns the node to this group. + # If the node already belongs to a group, + # it will be removed from the previous group before adding it to this group. + def <<(node : ExampleNode) + # Remove from existing group if the node is part of one. + if (previous = node.group?) + previous.delete(node) + end + + # Add the node to this group and associate with it. + @nodes << node + node.group = self + end + end +end diff --git a/src/spectator/example_node.cr b/src/spectator/example_node.cr new file mode 100644 index 0000000..5baaae1 --- /dev/null +++ b/src/spectator/example_node.cr @@ -0,0 +1,64 @@ +require "./source" + +module Spectator + # A single example or collection (group) of examples in an example tree. + abstract class ExampleNode + # Location of the node in source code. + getter! source : Source + + # User-provided name or description of the test. + # This does not include the group name or descriptions. + # Use `#to_s` to get the full name. + # + # This value will be nil if no name was provided. + # In this case, and the node is a runnable example, + # the name should be set to the description + # of the first matcher that runs in the test case. + # + # If this value is a `Symbol`, the user specified a type for the name. + getter! name : String | Symbol + + # Updates the name of the node. + protected def name=(@name : String) + end + + # Group the node belongs to. + getter! group : ExampleGroup + + # Assigns the node to the specified *group*. + # This is an internal method and should only be called from `ExampleGroup`. + # `ExampleGroup` manages the association of nodes to groups. + protected setter group : ExampleGroup? + + # Creates the node. + # The *name* describes the purpose of the node. + # It can be a `Symbol` to describe a type. + # The *source* tracks where the node exists in source code. + # The node will be assigned to *group* if it is provided. + def initialize(@name : String | Symbol? = nil, @source : Source? = nil, group : ExampleGroup? = nil) + # Ensure group is linked. + group << self if group + end + + # Indicates whether the node has completed. + abstract def finished? : Bool + + # Constructs the full name or description of the node. + # This prepends names of groups this node is part of. + def to_s(io) + name = @name + + # Prefix with group's full name if the node belongs to a group. + if (group = @group) + io << group + + # Add padding between the node names + # only if the names don't appear to be symbolic. + io << ' ' unless group.name.is_a?(Symbol) && name.is_a?(String) && + (name.starts_with?('#') || name.starts_with?('.')) + end + + io << name + end + end +end From 3a5dd7632496b5e00404abd190503de189221eef Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 5 Sep 2020 19:54:55 -0600 Subject: [PATCH 013/399] Remove ExampleBase Pending/skip functionality will be merged into Example or extend from it. --- src/spectator/example.cr | 9 +++++++-- src/spectator/example_base.cr | 23 ----------------------- 2 files changed, 7 insertions(+), 25 deletions(-) delete mode 100644 src/spectator/example_base.cr diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 88c333c..6f810ab 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -1,12 +1,12 @@ require "./context_delegate" -require "./example_base" require "./example_group" +require "./example_node" require "./result" require "./source" module Spectator # Standard example that runs a test case. - class Example < ExampleBase + class Example < ExampleNode # Indicates whether the example already ran. getter? finished : Bool = false @@ -30,5 +30,10 @@ module Spectator def run : Result raise NotImplementedError.new("#run") end + + # Exposes information about the example useful for debugging. + def inspect(io) + raise NotImplementedError.new("#inspect") + end end end diff --git a/src/spectator/example_base.cr b/src/spectator/example_base.cr deleted file mode 100644 index 0944cbb..0000000 --- a/src/spectator/example_base.cr +++ /dev/null @@ -1,23 +0,0 @@ -require "./example_node" -require "./result" - -module Spectator - # Common base type for all examples. - abstract class ExampleBase < ExampleNode - # Retrieves the result of the last time the example ran. - # This will be nil if the example hasn't run, - # and should not be nil if it has. - abstract def result? : Result? - - # Retrieves the result of the last time the example ran. - # Raises an error if the example hasn't run. - def result : Result - result? || raise(NilAssertionError("Example has no result")) - end - - # Exposes information about the example useful for debugging. - def inspect(io) - raise NotImplementedError.new("#inspect") - end - end -end From 3f7e0d7882ba87732a888b98cb49f4d2ae3dc897 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 5 Sep 2020 19:55:46 -0600 Subject: [PATCH 014/399] Add missing require statement --- src/spectator/example_group.cr | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index 01b64ce..82bbbfa 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -1,3 +1,5 @@ +require "./example_node" + module Spectator # Collection of examples and sub-groups. class ExampleGroup < ExampleNode From b866bc7e08b7d99b4df2ae9e1b431ba6badf0ebb Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 6 Sep 2020 10:31:23 -0600 Subject: [PATCH 015/399] Create example context variants --- src/spectator/context_method.cr | 2 +- src/spectator/example.cr | 4 ++-- src/spectator/example_context_delegate.cr | 20 ++++++++++++++++++++ src/spectator/example_context_method.cr | 11 +++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 src/spectator/example_context_delegate.cr create mode 100644 src/spectator/example_context_method.cr diff --git a/src/spectator/context_method.cr b/src/spectator/context_method.cr index e1c9aa8..30cce6d 100644 --- a/src/spectator/context_method.cr +++ b/src/spectator/context_method.cr @@ -1,7 +1,7 @@ require "./context" module Spectator - # Encapsulates a method in a context. + # Encapsulates a method in a test context. # This could be used to invoke a test case or hook method. # The context is passed as an argument. # The proc should downcast the context instance to the desired type diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 6f810ab..c392fab 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -1,4 +1,4 @@ -require "./context_delegate" +require "./example_context_delegate" require "./example_group" require "./example_node" require "./result" @@ -19,7 +19,7 @@ module Spectator # It can be a `Symbol` to describe a type. # The *source* tracks where the example exists in source code. # The example will be assigned to *group* if it is provided. - def initialize(@delegate : ContextDelegate, + def initialize(@delegate : ExampleContextDelegate, name : String | Symbol? = nil, source : Source? = nil, group : ExampleGroup? = nil) super(name, source, group) end diff --git a/src/spectator/example_context_delegate.cr b/src/spectator/example_context_delegate.cr new file mode 100644 index 0000000..bd5aa9c --- /dev/null +++ b/src/spectator/example_context_delegate.cr @@ -0,0 +1,20 @@ +require "./context" +require "./example_context_method" + +module Spectator + # Stores a test context and a method to call within it. + # This is a variant of `ContextDelegate` that accepts the current running example. + struct ExampleContextDelegate + # Creates the delegate. + # The *context* is the instance of the test context. + # The *method* is proc that downcasts *context* and calls a method on it. + def initialize(@context : Context, @method : ExampleContextMethod) + end + + # Invokes a method in the test context. + # The *example* is the current running example. + def call(example : Example) + @method.call(example, @context) + end + end +end diff --git a/src/spectator/example_context_method.cr b/src/spectator/example_context_method.cr new file mode 100644 index 0000000..a3e3dfc --- /dev/null +++ b/src/spectator/example_context_method.cr @@ -0,0 +1,11 @@ +require "./context" + +module Spectator + # Encapsulates a method in a test context. + # This could be used to invoke a test case or hook method. + # The context is passed as an argument. + # The proc should downcast the context instance to the desired type + # and call a method on that context. + # The current example is also passed as an argument. + alias ExampleContextMethod = Example, Context -> +end From cce17ad55fa6462f1dbfb1cc3fa8cf7c0301028b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Sep 2020 14:34:24 -0600 Subject: [PATCH 016/399] Fix filename --- src/{spectator_contnext.cr => spectator_context.cr} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{spectator_contnext.cr => spectator_context.cr} (100%) diff --git a/src/spectator_contnext.cr b/src/spectator_context.cr similarity index 100% rename from src/spectator_contnext.cr rename to src/spectator_context.cr From a08c87dd5ddd489a0939844fb705f3c18f7e4ade Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Sep 2020 15:28:55 -0600 Subject: [PATCH 017/399] Remove workaround https://github.com/icy-arctic-fox/spectator/issues/1 should be resolved by https://github.com/crystal-lang/crystal/pull/8234 --- src/spectator/includes.cr | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/spectator/includes.cr b/src/spectator/includes.cr index 4425f99..894a045 100644 --- a/src/spectator/includes.cr +++ b/src/spectator/includes.cr @@ -4,12 +4,6 @@ # Including all files with a wildcard would accidentally enable should-syntax. # Unfortunately, that leads to the existence of this file to include everything but that file. -# FIXME: Temporary (hopefully) require statement to workaround Crystal issue #7060. -# https://github.com/crystal-lang/crystal/issues/7060 -# The primary issue seems to be around OpenSSL. -# By explicitly including it before Spectator functionality, we workaround the issue. -require "openssl" - # First the sub-modules. require "./dsl" require "./expectations" From ea6c1542245bc4abddd52d1ef29a0183df8cd5b0 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Sep 2020 15:43:28 -0600 Subject: [PATCH 018/399] Change version to 0.10.0 Prepare for next minor release. --- shard.yml | 2 +- src/spectator.cr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shard.yml b/shard.yml index a6ba14f..e624ed0 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: spectator -version: 0.9.22 +version: 0.10.0 description: | A feature-rich spec testing framework for Crystal with similarities to RSpec. diff --git a/src/spectator.cr b/src/spectator.cr index 77c333e..5e10e0d 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -6,7 +6,7 @@ module Spectator extend self # Current version of the Spectator library. - VERSION = "0.9.22" + VERSION = "0.10.0" # Top-level describe method. # All specs in a file must be wrapped in this call. From 8b205278ad42f60272d13850a090c0b28194d5fe Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Sep 2020 15:57:20 -0600 Subject: [PATCH 019/399] Change SpectatorTest to SpectatorContext --- src/spectator.cr | 4 ++-- src/spectator_test.cr | 25 ------------------------- 2 files changed, 2 insertions(+), 27 deletions(-) delete mode 100644 src/spectator_test.cr diff --git a/src/spectator.cr b/src/spectator.cr index 5e10e0d..c5f68a9 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -1,5 +1,5 @@ require "./spectator/includes" -require "./spectator_test" +require "./spectator_context" # Module that contains all functionality related to Spectator. module Spectator @@ -38,7 +38,7 @@ module Spectator # For more information on how the DSL works, see the `DSL` module. # Root-level class that contains all examples and example groups. - class SpectatorTest + class SpectatorContext # Pass off the description argument and block to `DSL::StructureDSL.describe`. # That method will handle creating a new group for this spec. describe({{description}}) {{block}} diff --git a/src/spectator_test.cr b/src/spectator_test.cr deleted file mode 100644 index a0a202d..0000000 --- a/src/spectator_test.cr +++ /dev/null @@ -1,25 +0,0 @@ -require "./spectator/dsl" - -# Root-level class that all tests inherit from and are contained in. -# This class is intentionally outside of the scope of Spectator, -# so that the namespace isn't leaked into tests unexpectedly. -class SpectatorTest - include ::Spectator::DSL - - def _spectator_implicit_subject - nil - end - - def subject - _spectator_implicit_subject - end - - def initialize(@spectator_test_values : ::Spectator::TestValues) - end - - # Prevent leaking internal values since their types may differ. - # Workaround for: https://gitlab.com/arctic-fox/spectator/-/issues/53 - def inspect(io) - io << self.class - end -end From 6e3ec79a14c5aad4f197f100634d52c581ecdf09 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Sep 2020 15:58:07 -0600 Subject: [PATCH 020/399] Remove SpecBuilder types --- src/spectator/spec_builder/example_builder.cr | 19 ----- .../spec_builder/example_group_builder.cr | 74 ------------------- .../spec_builder/example_group_stack.cr | 28 ------- .../nested_example_group_builder.cr | 18 ----- .../spec_builder/pending_example_builder.cr | 10 --- .../root_example_group_builder.cr | 15 ---- .../spec_builder/runnable_example_builder.cr | 10 --- .../sample_example_group_builder.cr | 30 -------- 8 files changed, 204 deletions(-) delete mode 100644 src/spectator/spec_builder/example_builder.cr delete mode 100644 src/spectator/spec_builder/example_group_builder.cr delete mode 100644 src/spectator/spec_builder/example_group_stack.cr delete mode 100644 src/spectator/spec_builder/nested_example_group_builder.cr delete mode 100644 src/spectator/spec_builder/pending_example_builder.cr delete mode 100644 src/spectator/spec_builder/root_example_group_builder.cr delete mode 100644 src/spectator/spec_builder/runnable_example_builder.cr delete mode 100644 src/spectator/spec_builder/sample_example_group_builder.cr diff --git a/src/spectator/spec_builder/example_builder.cr b/src/spectator/spec_builder/example_builder.cr deleted file mode 100644 index 378c024..0000000 --- a/src/spectator/spec_builder/example_builder.cr +++ /dev/null @@ -1,19 +0,0 @@ -require "../../spectator_test" -require "../test_values" -require "../test_wrapper" - -module Spectator::SpecBuilder - abstract class ExampleBuilder - alias FactoryMethod = TestValues -> ::SpectatorTest - - def initialize(@description : String?, @source : Source, @builder : FactoryMethod, @runner : TestMethod) - end - - abstract def build(group) : ExampleComponent - - private def build_test_wrapper(group) - test = @builder.call(group.context.values) - TestWrapper.new(@description, @source, test, @runner) - end - end -end diff --git a/src/spectator/spec_builder/example_group_builder.cr b/src/spectator/spec_builder/example_group_builder.cr deleted file mode 100644 index 950f99d..0000000 --- a/src/spectator/spec_builder/example_group_builder.cr +++ /dev/null @@ -1,74 +0,0 @@ -require "../test_context" -require "./example_builder" - -module Spectator::SpecBuilder - abstract class ExampleGroupBuilder - alias Child = NestedExampleGroupBuilder | ExampleBuilder - - private getter children = Deque(Child).new - - @before_each_hooks = Deque(TestMetaMethod).new - @after_each_hooks = Deque(TestMetaMethod).new - @before_all_hooks = Deque(->).new - @after_all_hooks = Deque(->).new - @around_each_hooks = Deque(::SpectatorTest, Proc(Nil) ->).new - @pre_conditions = Deque(TestMetaMethod).new - @post_conditions = Deque(TestMetaMethod).new - @default_stubs = {} of String => Deque(Mocks::MethodStub) - - def add_child(child : Child) - @children << child - end - - def add_before_each_hook(hook : TestMetaMethod) - @before_each_hooks << hook - end - - def add_after_each_hook(hook : TestMetaMethod) - @after_each_hooks << hook - end - - def add_before_all_hook(hook : ->) - @before_all_hooks << hook - end - - def add_after_all_hook(hook : ->) - @after_all_hooks << hook - end - - def add_around_each_hook(hook : ::SpectatorTest, Proc(Nil) ->) - @around_each_hooks << hook - end - - def add_pre_condition(hook : TestMetaMethod) - @pre_conditions << hook - end - - def add_post_condition(hook : TestMetaMethod) - @post_conditions << hook - end - - def add_default_stub(type : T.class, stub : Mocks::MethodStub) forall T - key = type.name - @default_stubs[key] = Deque(Mocks::MethodStub).new unless @default_stubs.has_key?(key) - @default_stubs[key].unshift(stub) - end - - private def build_hooks - ExampleHooks.new( - @before_all_hooks.to_a, - @before_each_hooks.to_a, - @after_all_hooks.to_a, - @after_each_hooks.to_a, - @around_each_hooks.to_a - ) - end - - private def build_conditions - ExampleConditions.new( - @pre_conditions.to_a, - @post_conditions.to_a - ) - end - end -end diff --git a/src/spectator/spec_builder/example_group_stack.cr b/src/spectator/spec_builder/example_group_stack.cr deleted file mode 100644 index 50385bb..0000000 --- a/src/spectator/spec_builder/example_group_stack.cr +++ /dev/null @@ -1,28 +0,0 @@ -require "./root_example_group_builder" -require "./nested_example_group_builder" - -module Spectator::SpecBuilder - struct ExampleGroupStack - getter root - - def initialize - @root = RootExampleGroupBuilder.new - @stack = Deque(ExampleGroupBuilder).new(1, @root) - end - - def current - @stack.last - end - - def push(group : NestedExampleGroupBuilder) - current.add_child(group) - @stack.push(group) - end - - def pop - raise "Attempted to pop root example group from stack" if current == root - - @stack.pop - end - end -end diff --git a/src/spectator/spec_builder/nested_example_group_builder.cr b/src/spectator/spec_builder/nested_example_group_builder.cr deleted file mode 100644 index 6d7bb77..0000000 --- a/src/spectator/spec_builder/nested_example_group_builder.cr +++ /dev/null @@ -1,18 +0,0 @@ -require "../test_context" -require "./example_group_builder" - -module Spectator::SpecBuilder - class NestedExampleGroupBuilder < ExampleGroupBuilder - def initialize(@description : String | Symbol, @source : Source) - end - - def build(parent_group) - context = TestContext.new(parent_group.context, build_hooks, build_conditions, parent_group.context.values, @default_stubs) - NestedExampleGroup.new(@description, @source, parent_group, context).tap do |group| - group.children = children.map do |child| - child.build(group).as(ExampleComponent) - end - end - end - end -end diff --git a/src/spectator/spec_builder/pending_example_builder.cr b/src/spectator/spec_builder/pending_example_builder.cr deleted file mode 100644 index 731b9ab..0000000 --- a/src/spectator/spec_builder/pending_example_builder.cr +++ /dev/null @@ -1,10 +0,0 @@ -require "./example_builder" - -module Spectator::SpecBuilder - class PendingExampleBuilder < ExampleBuilder - def build(group) : ExampleComponent - wrapper = build_test_wrapper(group) - PendingExample.new(group, wrapper).as(ExampleComponent) - end - end -end diff --git a/src/spectator/spec_builder/root_example_group_builder.cr b/src/spectator/spec_builder/root_example_group_builder.cr deleted file mode 100644 index 3dbbcf7..0000000 --- a/src/spectator/spec_builder/root_example_group_builder.cr +++ /dev/null @@ -1,15 +0,0 @@ -require "../test_values" -require "./example_group_builder" - -module Spectator::SpecBuilder - class RootExampleGroupBuilder < ExampleGroupBuilder - def build - context = TestContext.new(nil, build_hooks, build_conditions, TestValues.empty, {} of String => Deque(Mocks::MethodStub)) - RootExampleGroup.new(context).tap do |group| - group.children = children.map do |child| - child.build(group).as(ExampleComponent) - end - end - end - end -end diff --git a/src/spectator/spec_builder/runnable_example_builder.cr b/src/spectator/spec_builder/runnable_example_builder.cr deleted file mode 100644 index 8c22a15..0000000 --- a/src/spectator/spec_builder/runnable_example_builder.cr +++ /dev/null @@ -1,10 +0,0 @@ -require "./example_builder" - -module Spectator::SpecBuilder - class RunnableExampleBuilder < ExampleBuilder - def build(group) : ExampleComponent - wrapper = build_test_wrapper(group) - RunnableExample.new(group, wrapper).as(ExampleComponent) - end - end -end diff --git a/src/spectator/spec_builder/sample_example_group_builder.cr b/src/spectator/spec_builder/sample_example_group_builder.cr deleted file mode 100644 index 955ac7a..0000000 --- a/src/spectator/spec_builder/sample_example_group_builder.cr +++ /dev/null @@ -1,30 +0,0 @@ -require "./nested_example_group_builder" - -module Spectator::SpecBuilder - class SampleExampleGroupBuilder(T) < NestedExampleGroupBuilder - def initialize(description : String | Symbol, source : Source, @id : Symbol, @label : String, @collection_builder : TestValues -> Array(T)) - super(description, source) - end - - def build(parent_group) - values = parent_group.context.values - collection = @collection_builder.call(values) - context = TestContext.new(parent_group.context, build_hooks, build_conditions, values, @default_stubs) - NestedExampleGroup.new(@description, @source, parent_group, context).tap do |group| - group.children = collection.map do |element| - build_sub_group(group, element).as(ExampleComponent) - end - end - end - - private def build_sub_group(parent_group, element) - values = parent_group.context.values.add(@id, @description.to_s, element) - context = TestContext.new(parent_group.context, ExampleHooks.empty, ExampleConditions.empty, values, {} of String => Deque(Mocks::MethodStub)) - NestedExampleGroup.new("#{@label} = #{element.inspect}", @source, parent_group, context).tap do |group| - group.children = children.map do |child| - child.build(group).as(ExampleComponent) - end - end - end - end -end From e455708467fb0000ebd40314f513774505915f15 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Sep 2020 15:58:54 -0600 Subject: [PATCH 021/399] Add missing require statements --- src/spectator/command_line_arguments_config_source.cr | 1 + src/spectator/composite_example_filter.cr | 2 ++ src/spectator/config.cr | 3 +++ src/spectator/failed_result.cr | 2 +- src/spectator/finished_result.cr | 2 ++ src/spectator/formatting/document_formatter.cr | 1 + 6 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/spectator/command_line_arguments_config_source.cr b/src/spectator/command_line_arguments_config_source.cr index ce3c7e5..1631c49 100644 --- a/src/spectator/command_line_arguments_config_source.cr +++ b/src/spectator/command_line_arguments_config_source.cr @@ -1,4 +1,5 @@ require "option_parser" +require "./config_source" module Spectator # Generates configuration from the command-line arguments. diff --git a/src/spectator/composite_example_filter.cr b/src/spectator/composite_example_filter.cr index 7f776ba..1162c88 100644 --- a/src/spectator/composite_example_filter.cr +++ b/src/spectator/composite_example_filter.cr @@ -1,3 +1,5 @@ +require "./example_filter" + module Spectator # Filter that combines multiple other filters. class CompositeExampleFilter < ExampleFilter diff --git a/src/spectator/config.cr b/src/spectator/config.cr index a44564b..cde982d 100644 --- a/src/spectator/config.cr +++ b/src/spectator/config.cr @@ -1,3 +1,6 @@ +require "./example_filter" +require "./formatting" + module Spectator # Provides customization and describes specifics for how Spectator will run and report tests. class Config diff --git a/src/spectator/failed_result.cr b/src/spectator/failed_result.cr index 65f5c4e..43b2ea6 100644 --- a/src/spectator/failed_result.cr +++ b/src/spectator/failed_result.cr @@ -1,4 +1,4 @@ -require "./result" +require "./finished_result" module Spectator # Outcome that indicates running an example was a failure. diff --git a/src/spectator/finished_result.cr b/src/spectator/finished_result.cr index 87ec298..1e3dae8 100644 --- a/src/spectator/finished_result.cr +++ b/src/spectator/finished_result.cr @@ -1,3 +1,5 @@ +require "./result" + module Spectator # Abstract class for all results by examples abstract class FinishedResult < Result diff --git a/src/spectator/formatting/document_formatter.cr b/src/spectator/formatting/document_formatter.cr index c100e55..3441394 100644 --- a/src/spectator/formatting/document_formatter.cr +++ b/src/spectator/formatting/document_formatter.cr @@ -1,3 +1,4 @@ +require "../example_group" require "./formatter" require "./suite_summary" From b271028c1e30f786c058e2690c2362dae3e35f58 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Sep 2020 15:59:20 -0600 Subject: [PATCH 022/399] Remove most includes for now --- src/spectator/includes.cr | 49 ++------------------------------------- 1 file changed, 2 insertions(+), 47 deletions(-) diff --git a/src/spectator/includes.cr b/src/spectator/includes.cr index 894a045..f94f6e2 100644 --- a/src/spectator/includes.cr +++ b/src/spectator/includes.cr @@ -4,52 +4,7 @@ # Including all files with a wildcard would accidentally enable should-syntax. # Unfortunately, that leads to the existence of this file to include everything but that file. -# First the sub-modules. -require "./dsl" -require "./expectations" -require "./matchers" -require "./formatting" - -# Then all of the top-level types. -require "./spec_builder" -require "./example_component" -require "./example" -require "./runnable_example" -require "./pending_example" - -require "./example_conditions" -require "./example_hooks" -require "./example_group" -require "./nested_example_group" -require "./root_example_group" - -require "./mocks" - require "./config" require "./config_builder" -require "./config_source" -require "./command_line_arguments_config_source" - -require "./example_filter" -require "./source_example_filter" -require "./line_example_filter" -require "./name_example_filter" -require "./null_example_filter" -require "./composite_example_filter" - -require "./example_failed" -require "./expectation_failed" -require "./test_suite" -require "./report" -require "./profile" -require "./runner" - -require "./result" -require "./finished_result" -require "./successful_result" -require "./pending_result" -require "./failed_result" -require "./errored_result" - -require "./source" -require "./example_iterator" +require "./dsl" +require "./spec_builder" From 1d329467600ef2464e30e593c0f0341e45a6df2f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Sep 2020 15:59:45 -0600 Subject: [PATCH 023/399] Note about docs in macros --- src/spectator/dsl.cr | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/spectator/dsl.cr b/src/spectator/dsl.cr index 0d13112..6a98e85 100644 --- a/src/spectator/dsl.cr +++ b/src/spectator/dsl.cr @@ -2,6 +2,10 @@ require "./dsl/*" module Spectator # Namespace containing methods representing the spec domain specific language. + # + # Note: Documentation inside macros is kept to a minimuum to reduce generated code. + # This also helps keep error traces small. + # Documentation only useful for debugging is included in generated code. module DSL end end From 225c358cb8e601889499e1e1eac5d3fc598b6044 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Sep 2020 16:01:29 -0600 Subject: [PATCH 024/399] Some initial work on cleaned up groups --- src/spectator/dsl/groups.cr | 100 ++++++++++++++++++++---------------- src/spectator_context.cr | 13 +++++ 2 files changed, 68 insertions(+), 45 deletions(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 0ad5f72..666d42e 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -1,55 +1,66 @@ -require "../spec_builder" - -module Spectator - module DSL - macro context(what, _source_file = __FILE__, _source_line = __LINE__, &block) - class Context%context < {{@type.id}} - {% - description = if what.is_a?(StringLiteral) - if what.starts_with?("#") || what.starts_with?(".") - what.id.symbolize - else - what - end - else - what.symbolize - end - %} +module Spectator::DSL + # DSL methods and macros for creating example groups. + # This module should be included as a mix-in. + module Groups + # Defines a new example group. + # The *what* argument is a name or description of the group. + # If it isn't a string literal, then it is symbolized for `ExampleNode#name`. + macro example_group(what, *, _source_file = __FILE__, _source_line = __LINE__, &block) + # Example group {{name.stringify}} + # Source: {{_source_file}}:{{_source_line}} + class Group%group < {{@type.id}} + _spectator_group_subject({{what}}) %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - ::Spectator::SpecBuilder.start_group({{description}}, %source) - - # Oddly, `#resolve?` can return a constant's value, which isn't a TypeNode. - # Ensure `described_class` and `subject` are only set for real types (is a `TypeNode`). - {% if (what.is_a?(Path) || what.is_a?(Generic)) && (described_type = what.resolve?).is_a?(TypeNode) %} - macro described_class - {{what}} - end - - subject do - {% if described_type < Reference || described_type < Value %} - described_class.new - {% else %} - described_class - {% end %} - end - {% else %} - def _spectator_implicit_subject(*args) - {{what}} - end - {% end %} + ::Spectator::DSL::Builder.start_group({{what.is_a?(StringLiteral) ? what : what.stringify}}, %source) {{block.body}} - ::Spectator::SpecBuilder.end_group + ::Spectator::DSL::Builder.end_group end end - macro describe(what, &block) - context({{what}}) {{block}} + macro describe(what, *, _source_file = __FILE__, _source_line = __LINE__, &block) + example_group({{what}}, _source_file: {{_source_file}}, _source_line: {{_source_line}}) {{block}} end - macro sample(collection, count = nil, _source_file = __FILE__, _source_line = __LINE__, &block) + macro context(what, *, _source_file = __FILE__, _source_line = __LINE__, &block) + example_group({{what}}, _source_file: {{_source_file}}, _source_line: {{_source_line}}) {{block}} + end + + # Defines the implicit subject for the test context. + # If *what* is a type, then the `described_class` method will be defined. + # Additionally, the implicit subject is set to an instance of *what* if it's not a module. + # + # There is no common macro type that has the `#resolve?` method. + # Also, `#responds_to?` can't be used in macros. + # So the large if statement in this macro is used to look for type signatures. + private macro _spectator_group_subject(what) + {% if (what.is_a?(Generic) || + what.is_a?(Path) || + what.is_a?(TypeNode) || + what.is_a?(Union)) && + (described_type = what.resolve?).is_a?(TypeNode) %} + private def described_class + {{described_type}} + end + + private def _spectator_implicit_subject + {% if described_type < Reference || described_type < Value %} + described_class.new + {% else %} + described_class + {% end %} + end + {% else %} + private def _spectator_implicit_subject + {{what}} + end + {% end %} + end + end + + macro sample(collection, count = nil, _source_file = __FILE__, _source_line = __LINE__, &block) {% name = block.args.empty? ? :value.id : block.args.first.id %} def %collection @@ -81,7 +92,7 @@ module Spectator end end - macro random_sample(collection, count = nil, _source_file = __FILE__, _source_line = __LINE__, &block) + macro random_sample(collection, count = nil, _source_file = __FILE__, _source_line = __LINE__, &block) {% name = block.args.empty? ? :value.id : block.args.first.id %} def %collection @@ -118,7 +129,7 @@ module Spectator end end - macro given(*assignments, &block) + macro given(*assignments, &block) context({{assignments.splat.stringify}}) do {% for assignment in assignments %} let({{assignment.target}}) { {{assignment.value}} } @@ -150,5 +161,4 @@ module Spectator {% end %} end end - end end diff --git a/src/spectator_context.cr b/src/spectator_context.cr index 36ff503..d29aa27 100644 --- a/src/spectator_context.cr +++ b/src/spectator_context.cr @@ -4,4 +4,17 @@ # This type is intentionally outside the `Spectator` module. # The reason for this is to prevent name collision when using the DSL to define a spec. abstract class SpectatorContext + # Initial implicit subject for tests. + # This method should be overridden by example groups when an object is described. + private def _spectator_implicit_subject + nil + end + + # Initial subject for tests. + # Returns the implicit subject. + # This method should be overridden when an explicit subject is defined by the DSL. + # TODO: Subject needs to be cached. + private def subject + _spectator_implicit_subject + end end From 3133717323d320783dafdc1152eb313a7b3cb1a5 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Sep 2020 16:01:52 -0600 Subject: [PATCH 025/399] Change NestedExampleGroup to ExampleGroup --- src/spectator/formatting/document_formatter.cr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/spectator/formatting/document_formatter.cr b/src/spectator/formatting/document_formatter.cr index 3441394..910c254 100644 --- a/src/spectator/formatting/document_formatter.cr +++ b/src/spectator/formatting/document_formatter.cr @@ -11,7 +11,7 @@ module Spectator::Formatting private INDENT = " " - @previous_hierarchy = [] of NestedExampleGroup + @previous_hierarchy = [] of ExampleGroup # Creates the formatter. # By default, output is sent to STDOUT. @@ -34,9 +34,9 @@ module Spectator::Formatting # Produces a list of groups making up the hierarchy for an example. private def group_hierarchy(example) - hierarchy = [] of NestedExampleGroup + hierarchy = [] of ExampleGroup group = example.group - while group.is_a?(NestedExampleGroup) + while group.is_a?(ExampleGroup) hierarchy << group group = group.parent end From 98f886d9d45824f0f838d982a5a947c2adb377e0 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Sep 2020 16:02:11 -0600 Subject: [PATCH 026/399] Implement finished? method --- src/spectator/example_group.cr | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index 82bbbfa..80b6672 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -22,6 +22,11 @@ module Spectator @nodes.each { |node| yield node } end + # Checks if all examples and sub-groups have finished. + def finished? : Bool + @nodes.all?(&.finished?) + end + # Adds the specified *node* to the group. # Assigns the node to this group. # If the node already belongs to a group, From 0190cc726053ef9a47dc3fe1e60d2319f827682b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Sep 2020 18:35:31 -0600 Subject: [PATCH 027/399] Fix leading whitespace with root group --- src/spectator/example_node.cr | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/spectator/example_node.cr b/src/spectator/example_node.cr index 5baaae1..42f970a 100644 --- a/src/spectator/example_node.cr +++ b/src/spectator/example_node.cr @@ -54,8 +54,9 @@ module Spectator # Add padding between the node names # only if the names don't appear to be symbolic. - io << ' ' unless group.name.is_a?(Symbol) && name.is_a?(String) && - (name.starts_with?('#') || name.starts_with?('.')) + io << ' ' unless !group.name? || # Skip blank group names (like the root group). + (group.name?.is_a?(Symbol) && name.is_a?(String) && + (name.starts_with?('#') || name.starts_with?('.'))) end io << name From 67ac06e4d6ae91e2d3fd639f0573d577a519e91e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Sep 2020 18:37:14 -0600 Subject: [PATCH 028/399] Some initial spec builder code --- src/spectator/dsl/builder.cr | 17 ++++++ src/spectator/includes.cr | 1 - src/spectator/spec.cr | 6 ++ src/spectator/spec/builder.cr | 61 +++++++++++++++++++ src/spectator/spec_builder.cr | 110 ---------------------------------- 5 files changed, 84 insertions(+), 111 deletions(-) create mode 100644 src/spectator/dsl/builder.cr create mode 100644 src/spectator/spec.cr create mode 100644 src/spectator/spec/builder.cr delete mode 100644 src/spectator/spec_builder.cr diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr new file mode 100644 index 0000000..de91e76 --- /dev/null +++ b/src/spectator/dsl/builder.cr @@ -0,0 +1,17 @@ +require "../spec/builder" + +module Spectator::DSL + module Builder + extend self + + @@builder = Spec::Builder.new + + def start_group(*args) + @@builder.start_group(*args) + end + + def end_group(*args) + @@builder.end_group(*args) + end + end +end diff --git a/src/spectator/includes.cr b/src/spectator/includes.cr index f94f6e2..200b7a0 100644 --- a/src/spectator/includes.cr +++ b/src/spectator/includes.cr @@ -7,4 +7,3 @@ require "./config" require "./config_builder" require "./dsl" -require "./spec_builder" diff --git a/src/spectator/spec.cr b/src/spectator/spec.cr new file mode 100644 index 0000000..ebe3f9d --- /dev/null +++ b/src/spectator/spec.cr @@ -0,0 +1,6 @@ +require "./spec/*" + +module Spectator + class Spec + end +end diff --git a/src/spectator/spec/builder.cr b/src/spectator/spec/builder.cr new file mode 100644 index 0000000..22e9324 --- /dev/null +++ b/src/spectator/spec/builder.cr @@ -0,0 +1,61 @@ +require "../example" +require "../example_group" + +module Spectator + class Spec::Builder + # Stack tracking the current group. + # The bottom of the stack (first element) is the root group. + # The root group should never be removed. + # The top of the stack (last element) is the current group. + # New examples should be added to the current group. + @group_stack : Deque(ExampleGroup) + + def initialize + root_group = ExampleGroup.new + @group_stack = Deque(ExampleGroup).new + @group_stack.push(root_group) + end + + def add_example + raise NotImplementedError.new("#add_example") + end + + def start_group(name, source = nil) : ExampleGroup + {% if flag?(:spectator_debug) %} + puts "Start group: #{name.inspect} @ #{source}" + {% end %} + ExampleGroup.new(name, source, current_group).tap do |group| + @group_stack << group + end + end + + def end_group + {% if flag?(:spectator_debug) %} + puts "End group: #{current_group}" + {% end %} + raise "Can't pop root group" if root? + + @group_stack.pop + end + + def build + raise NotImplementedError.new("#build") + end + + # Checks if the current group is the root group. + private def root? + @group_stack.size == 1 + end + + # Retrieves the root group. + private def root_group + @group_stack.first + end + + # Retrieves the current group. + # This is the group that new examples should be added to. + private def current_group + @group_stack.last + end + end +end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr deleted file mode 100644 index 39834be..0000000 --- a/src/spectator/spec_builder.cr +++ /dev/null @@ -1,110 +0,0 @@ -require "./spec_builder/*" - -module Spectator - # Global builder used to create the runtime instance of the spec. - # The DSL methods call into this module to generate parts of the spec. - # Once the DSL is done, the `#build` method can be invoked - # to create the entire spec as a runtime instance. - module SpecBuilder - extend self - - @@stack = ExampleGroupStack.new - - # Begins a new nested group in the spec. - # A corresponding `#end_group` call must be made - # when the group being started is finished. - # See `NestedExampleGroupBuilder#initialize` for the arguments - # as arguments to this method are passed directly to it. - def start_group(*args) : Nil - group = NestedExampleGroupBuilder.new(*args) - @@stack.push(group) - end - - # Begins a new sample group in the spec - - # that is, a group defined by the `StructureDSL#sample` macro in the DSL. - # A corresponding `#end_group` call must be made - # when the group being started is finished. - # See `SampleExampleGroupBuilder#initialize` for the arguments - # as arguments to this method are passed directly to it. - def start_sample_group(*args, &block : TestValues -> Array(T)) : Nil forall T - group = SampleExampleGroupBuilder(T).new(*args, block) - @@stack.push(group) - end - - # Marks the end of a group in the spec. - # This must be called for every `#start_group` and `#start_sample_group` call. - # It is also important to line up the start and end calls. - # Otherwise examples might get placed into wrong groups. - def end_group : Nil - @@stack.pop - end - - # Adds an example type to the current group. - # The class name of the example should be passed as an argument. - # The example will be instantiated later. - def add_example(description : String?, source : Source, - example_type : ::SpectatorTest.class, &runner : ::SpectatorTest ->) : Nil - builder = ->(values : TestValues) { example_type.new(values).as(::SpectatorTest) } - factory = RunnableExampleBuilder.new(description, source, builder, runner) - @@stack.current.add_child(factory) - end - - # Adds an example type to the current group. - # The class name of the example should be passed as an argument. - # The example will be instantiated later. - def add_pending_example(description : String?, source : Source, - example_type : ::SpectatorTest.class, &runner : ::SpectatorTest ->) : Nil - builder = ->(values : TestValues) { example_type.new(values).as(::SpectatorTest) } - factory = PendingExampleBuilder.new(description, source, builder, runner) - @@stack.current.add_child(factory) - end - - # Adds a block of code to run before all examples in the current group. - def add_before_all_hook(&block : ->) : Nil - @@stack.current.add_before_all_hook(block) - end - - # Adds a block of code to run before each example in the current group. - def add_before_each_hook(&block : TestMetaMethod) : Nil - @@stack.current.add_before_each_hook(block) - end - - # Adds a block of code to run after all examples in the current group. - def add_after_all_hook(&block : ->) : Nil - @@stack.current.add_after_all_hook(block) - end - - # Adds a block of code to run after each example in the current group. - def add_after_each_hook(&block : TestMetaMethod) : Nil - @@stack.current.add_after_each_hook(block) - end - - # Adds a block of code to run before and after each example in the current group. - # The block of code will be given another hook as an argument. - # It is expected that the block will call the hook. - def add_around_each_hook(&block : ::SpectatorTest, Proc(Nil) ->) : Nil - @@stack.current.add_around_each_hook(block) - end - - # Adds a pre-condition to run at the start of every example in the current group. - def add_pre_condition(&block : TestMetaMethod) : Nil - @@stack.current.add_pre_condition(block) - end - - # Adds a post-condition to run at the end of every example in the current group. - def add_post_condition(&block : TestMetaMethod) : Nil - @@stack.current.add_post_condition(block) - end - - def add_default_stub(*args) : Nil - @@stack.current.add_default_stub(*args) - end - - # Builds the entire spec and returns it as a test suite. - # This should be called only once after the entire spec has been defined. - protected def build(filter : ExampleFilter) : TestSuite - group = @@stack.root.build - TestSuite.new(group, filter) - end - end -end From 6752c7c25401af63fc658a787ce0a3d22ce311da Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Sep 2020 18:39:21 -0600 Subject: [PATCH 029/399] Move DSL-based code to subclass of SpectatorContext This resolves a circular dependency. --- src/spectator.cr | 4 ++-- src/spectator_context.cr | 13 ------------- src/spectator_test_context.cr | 23 +++++++++++++++++++++++ 3 files changed, 25 insertions(+), 15 deletions(-) create mode 100644 src/spectator_test_context.cr diff --git a/src/spectator.cr b/src/spectator.cr index c5f68a9..0c279ae 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -1,5 +1,5 @@ require "./spectator/includes" -require "./spectator_context" +require "./spectator_test_context" # Module that contains all functionality related to Spectator. module Spectator @@ -38,7 +38,7 @@ module Spectator # For more information on how the DSL works, see the `DSL` module. # Root-level class that contains all examples and example groups. - class SpectatorContext + class ::SpectatorTestContext # Pass off the description argument and block to `DSL::StructureDSL.describe`. # That method will handle creating a new group for this spec. describe({{description}}) {{block}} diff --git a/src/spectator_context.cr b/src/spectator_context.cr index d29aa27..36ff503 100644 --- a/src/spectator_context.cr +++ b/src/spectator_context.cr @@ -4,17 +4,4 @@ # This type is intentionally outside the `Spectator` module. # The reason for this is to prevent name collision when using the DSL to define a spec. abstract class SpectatorContext - # Initial implicit subject for tests. - # This method should be overridden by example groups when an object is described. - private def _spectator_implicit_subject - nil - end - - # Initial subject for tests. - # Returns the implicit subject. - # This method should be overridden when an explicit subject is defined by the DSL. - # TODO: Subject needs to be cached. - private def subject - _spectator_implicit_subject - end end diff --git a/src/spectator_test_context.cr b/src/spectator_test_context.cr new file mode 100644 index 0000000..208f5cb --- /dev/null +++ b/src/spectator_test_context.cr @@ -0,0 +1,23 @@ +require "./spectator_context" +require "./spectator/dsl" + +class SpectatorTestContext < SpectatorContext + include ::Spectator::DSL::Groups + + # Initial implicit subject for tests. + # This method should be overridden by example groups when an object is described. + private def _spectator_implicit_subject + nil + end + + # Initial subject for tests. + # Returns the implicit subject. + # This method should be overridden when an explicit subject is defined by the DSL. + # TODO: Subject needs to be cached. + private def subject + _spectator_implicit_subject + end + + # def initialize(@spectator_test_values : ::Spectator::TestValues) + # end +end From dad669686c693a97b8e6a8255772e73ff0752ff9 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Sep 2020 18:40:36 -0600 Subject: [PATCH 030/399] Temporarily disable running examples --- src/spectator.cr | 5 +++-- src/spectator/dsl.cr | 4 +++- src/spectator/dsl/values.cr | 19 ++++++++++--------- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/spectator.cr b/src/spectator.cr index 0c279ae..7dde698 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -101,8 +101,9 @@ module Spectator # Builds the tests and runs the framework. private def run # Build the test suite and run it. - suite = ::Spectator::SpecBuilder.build(config.example_filter) - Runner.new(suite, config).run + # suite = ::Spectator::SpecBuilder.build(config.example_filter) + # Runner.new(suite, config).run + true rescue ex # Catch all unhandled exceptions here. # Examples are already wrapped, so any exceptions they throw are caught. diff --git a/src/spectator/dsl.cr b/src/spectator/dsl.cr index 6a98e85..94d748a 100644 --- a/src/spectator/dsl.cr +++ b/src/spectator/dsl.cr @@ -1,4 +1,6 @@ -require "./dsl/*" +# require "./dsl/*" +require "./dsl/builder" +require "./dsl/groups" module Spectator # Namespace containing methods representing the spec domain specific language. diff --git a/src/spectator/dsl/values.cr b/src/spectator/dsl/values.cr index 6b1ba51..e4c41df 100644 --- a/src/spectator/dsl/values.cr +++ b/src/spectator/dsl/values.cr @@ -1,6 +1,8 @@ -module Spectator - module DSL - macro let(name, &block) +module Spectator::DSL + module Values + end + + macro let(name, &block) @%wrapper : ::Spectator::ValueWrapper? def {{name.id}} @@ -18,7 +20,7 @@ module Spectator end end - macro let!(name, &block) + macro let!(name, &block) @%wrapper : ::Spectator::ValueWrapper? def %wrapper @@ -34,7 +36,7 @@ module Spectator end end - macro subject(&block) + macro subject(&block) {% if block.is_a?(Nop) %} self.subject {% else %} @@ -42,7 +44,7 @@ module Spectator {% end %} end - macro subject(name, &block) + macro subject(name, &block) let({{name.id}}) {{block}} def subject @@ -50,16 +52,15 @@ module Spectator end end - macro subject!(&block) + macro subject!(&block) let!(:subject) {{block}} end - macro subject!(name, &block) + macro subject!(name, &block) let!({{name.id}}) {{block}} def subject {{name.id}} end end - end end From 7c44cba6675d3e8676418fb29417b501576a2659 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Sep 2020 18:40:56 -0600 Subject: [PATCH 031/399] Fix group creation via DSL --- src/spectator/dsl/groups.cr | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 666d42e..7c820b7 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -1,3 +1,6 @@ +require "../source" +require "./builder" + module Spectator::DSL # DSL methods and macros for creating example groups. # This module should be included as a mix-in. @@ -6,7 +9,7 @@ module Spectator::DSL # The *what* argument is a name or description of the group. # If it isn't a string literal, then it is symbolized for `ExampleNode#name`. macro example_group(what, *, _source_file = __FILE__, _source_line = __LINE__, &block) - # Example group {{name.stringify}} + # Example group: {{what.stringify}} # Source: {{_source_file}}:{{_source_line}} class Group%group < {{@type.id}} _spectator_group_subject({{what}}) From bc602d9b62ba9ae5f4ba9e5ebbf6e7a7607b0ba8 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 14 Sep 2020 13:55:07 -0600 Subject: [PATCH 032/399] Working example creation from DSL --- src/spectator/dsl.cr | 1 + src/spectator/dsl/builder.cr | 4 ++++ src/spectator/dsl/examples.cr | 28 ++++++++++++++++++++++++---- src/spectator/spec/builder.cr | 11 +++++++++++ src/spectator_test_context.cr | 1 + 5 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/spectator/dsl.cr b/src/spectator/dsl.cr index 94d748a..f4cd763 100644 --- a/src/spectator/dsl.cr +++ b/src/spectator/dsl.cr @@ -1,5 +1,6 @@ # require "./dsl/*" require "./dsl/builder" +require "./dsl/examples" require "./dsl/groups" module Spectator diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr index de91e76..13284a8 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -13,5 +13,9 @@ module Spectator::DSL def end_group(*args) @@builder.end_group(*args) end + + def add_example(*args, &block : Example, Context ->) + @@builder.add_example(*args, &block) + end end end diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index b356ea5..ba39adb 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -1,8 +1,29 @@ require "../source" -require "../spec_builder" +require "./builder" -module Spectator - module DSL +module Spectator::DSL + module Examples + macro example(what = nil, *, _source_file = __FILE__, _source_line = __LINE__, &block) + def %test + {{block.body}} + end + + %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) + ::Spectator::DSL::Builder.add_example( + {{what.is_a?(StringLiteral | NilLiteral) ? what : what.stringify}}, + %source, + {{@type.name}}.new + ) { |example, context| context.as({{@type.name}}).%test } + end + + macro it(what = nil, *, _source_file = __FILE__, _source_line = __LINE__, &block) + example({{what}}, _source_file: {{_source_file}}, _source_line: {{_source_line}}) {{block}} + end + + macro specify(what = nil, *, _source_file = __FILE__, _source_line = __LINE__, &block) + example({{what}}, _source_file: {{_source_file}}, _source_line: {{_source_line}}) {{block}} + end + end macro it(description = nil, _source_file = __FILE__, _source_line = __LINE__, &block) {% if block.is_a?(Nop) %} {% if description.is_a?(Call) %} @@ -60,5 +81,4 @@ module Spectator macro xit(description = nil, &block) pending({{description}}) {{block}} end - end end diff --git a/src/spectator/spec/builder.cr b/src/spectator/spec/builder.cr index 22e9324..27a97b9 100644 --- a/src/spectator/spec/builder.cr +++ b/src/spectator/spec/builder.cr @@ -1,4 +1,5 @@ require "../example" +require "../example_context_method" require "../example_group" module Spectator @@ -38,6 +39,16 @@ module Spectator @group_stack.pop end + def add_example(name, source, context, &block : Example, Context ->) + {% if flag?(:spectator_debug) %} + puts "Add example: #{name} @ #{source}" + puts "Context: #{context}" + {% end %} + delegate = ExampleContextDelegate.new(context, block) + Example.new(delegate, name, source, current_group) + # The example is added to the current group by `Example` initializer. + end + def build raise NotImplementedError.new("#build") end diff --git a/src/spectator_test_context.cr b/src/spectator_test_context.cr index 208f5cb..2031005 100644 --- a/src/spectator_test_context.cr +++ b/src/spectator_test_context.cr @@ -2,6 +2,7 @@ require "./spectator_context" require "./spectator/dsl" class SpectatorTestContext < SpectatorContext + include ::Spectator::DSL::Examples include ::Spectator::DSL::Groups # Initial implicit subject for tests. From 9103bfde0f52b78251ed3fc416536ddbec6e9eba Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 14 Sep 2020 20:00:17 -0600 Subject: [PATCH 033/399] Playing around with line numbers Trying to find some pattern in the line descripancies reported to the macros compared to the source file. --- src/spectator.cr | 17 +-------------- src/spectator/dsl/examples.cr | 40 ++++++++++++++++++++++++++++------- src/spectator/dsl/groups.cr | 16 +++++--------- src/spectator/spec/builder.cr | 1 - 4 files changed, 38 insertions(+), 36 deletions(-) diff --git a/src/spectator.cr b/src/spectator.cr index 7dde698..1b60f69 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -25,23 +25,8 @@ module Spectator # Actually, prefixing methods and macros with `Spectator` # most likely won't work and can cause compiler errors. macro describe(description, &block) - # This macro creates the foundation for all specs. - # Every group of examples is defined a separate module - `SpectatorExamples`. - # There's multiple reasons for this. - # - # The first reason is to provide namespace isolation. - # We don't want the spec code to accidentally pickup types and values from the `Spectator` module. - # Another reason is that we need a root module to put all examples and groups in. - # And lastly, the spec DSL needs to be given to the block of code somehow. - # The DSL is included in the `SpectatorTest` class. - # - # For more information on how the DSL works, see the `DSL` module. - - # Root-level class that contains all examples and example groups. class ::SpectatorTestContext - # Pass off the description argument and block to `DSL::StructureDSL.describe`. - # That method will handle creating a new group for this spec. - describe({{description}}) {{block}} + example_group({{description}}) {{block}} end end diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index ba39adb..cca80d1 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -3,12 +3,30 @@ require "./builder" module Spectator::DSL module Examples - macro example(what = nil, *, _source_file = __FILE__, _source_line = __LINE__, &block) + macro define_example(name) + macro {{name.id}}(what = nil) + def %test + \{{yield}} + end + + %source = ::Spectator::Source.new(__FILE__, __LINE__) + ::Spectator::DSL::Builder.add_example( + \{{what.is_a?(StringLiteral) || what.is_a?(NilLiteral) ? what : what.stringify}}, + %source, + \{{@type.name}}.new + ) { |example, context| context.as(\{{@type.name}}).%test } + end + end + + define_example(:example) + + macro it(what = nil, *, _source_file = __FILE__, _source_line = __LINE__, &block) + {% puts "#{_source_file}:#{_source_line}" %} def %test - {{block.body}} + {{yield}} end - %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) + %source = ::Spectator::Source.new(__FILE__, __LINE__) ::Spectator::DSL::Builder.add_example( {{what.is_a?(StringLiteral | NilLiteral) ? what : what.stringify}}, %source, @@ -16,12 +34,18 @@ module Spectator::DSL ) { |example, context| context.as({{@type.name}}).%test } end - macro it(what = nil, *, _source_file = __FILE__, _source_line = __LINE__, &block) - example({{what}}, _source_file: {{_source_file}}, _source_line: {{_source_line}}) {{block}} - end - macro specify(what = nil, *, _source_file = __FILE__, _source_line = __LINE__, &block) - example({{what}}, _source_file: {{_source_file}}, _source_line: {{_source_line}}) {{block}} + {% puts "#{_source_file}:#{_source_line}" %} + def %test + {{yield}} + end + + %source = ::Spectator::Source.new(__FILE__, __LINE__) + ::Spectator::DSL::Builder.add_example( + {{what.is_a?(StringLiteral | NilLiteral) ? what : what.stringify}}, + %source, + {{@type.name}}.new + ) { |example, context| context.as({{@type.name}}).%test } end end macro it(description = nil, _source_file = __FILE__, _source_line = __LINE__, &block) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 7c820b7..a029083 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -8,19 +8,13 @@ module Spectator::DSL # Defines a new example group. # The *what* argument is a name or description of the group. # If it isn't a string literal, then it is symbolized for `ExampleNode#name`. - macro example_group(what, *, _source_file = __FILE__, _source_line = __LINE__, &block) + + # Example group: {{what.stringify}} # Source: {{_source_file}}:{{_source_line}} - class Group%group < {{@type.id}} - _spectator_group_subject({{what}}) - - %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - ::Spectator::DSL::Builder.start_group({{what.is_a?(StringLiteral) ? what : what.stringify}}, %source) - - {{block.body}} - - ::Spectator::DSL::Builder.end_group - end + macro example_group(what, *, _source_file = __FILE__, _source_line = __LINE__, &block) + class Group%group < {{@type.id}}; _spectator_group_subject({{what}}); ::Spectator::DSL::Builder.start_group({{what.is_a?(StringLiteral) ? what : what.stringify}}, ::Spectator::Source.new(__FILE__, __LINE__)); {{block.body}}; ::Spectator::DSL::Builder.end_group; end + {% debug(false) %} end macro describe(what, *, _source_file = __FILE__, _source_line = __LINE__, &block) diff --git a/src/spectator/spec/builder.cr b/src/spectator/spec/builder.cr index 27a97b9..1eb16b3 100644 --- a/src/spectator/spec/builder.cr +++ b/src/spectator/spec/builder.cr @@ -42,7 +42,6 @@ module Spectator def add_example(name, source, context, &block : Example, Context ->) {% if flag?(:spectator_debug) %} puts "Add example: #{name} @ #{source}" - puts "Context: #{context}" {% end %} delegate = ExampleContextDelegate.new(context, block) Example.new(delegate, name, source, current_group) From 6363436afa4c227e3d5a801626294e670b94bbac Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 25 Sep 2020 21:44:17 -0600 Subject: [PATCH 034/399] Nested macros for defining DSL keywords --- src/spectator/dsl/examples.cr | 30 +++--------------------------- src/spectator/dsl/groups.cr | 33 +++++++++++++++++++-------------- 2 files changed, 22 insertions(+), 41 deletions(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index cca80d1..740260c 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -18,35 +18,11 @@ module Spectator::DSL end end - define_example(:example) + define_example :example - macro it(what = nil, *, _source_file = __FILE__, _source_line = __LINE__, &block) - {% puts "#{_source_file}:#{_source_line}" %} - def %test - {{yield}} - end + define_example :it - %source = ::Spectator::Source.new(__FILE__, __LINE__) - ::Spectator::DSL::Builder.add_example( - {{what.is_a?(StringLiteral | NilLiteral) ? what : what.stringify}}, - %source, - {{@type.name}}.new - ) { |example, context| context.as({{@type.name}}).%test } - end - - macro specify(what = nil, *, _source_file = __FILE__, _source_line = __LINE__, &block) - {% puts "#{_source_file}:#{_source_line}" %} - def %test - {{yield}} - end - - %source = ::Spectator::Source.new(__FILE__, __LINE__) - ::Spectator::DSL::Builder.add_example( - {{what.is_a?(StringLiteral | NilLiteral) ? what : what.stringify}}, - %source, - {{@type.name}}.new - ) { |example, context| context.as({{@type.name}}).%test } - end + define_example :specify end macro it(description = nil, _source_file = __FILE__, _source_line = __LINE__, &block) {% if block.is_a?(Nop) %} diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index a029083..5f034c9 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -5,25 +5,30 @@ module Spectator::DSL # DSL methods and macros for creating example groups. # This module should be included as a mix-in. module Groups - # Defines a new example group. - # The *what* argument is a name or description of the group. - # If it isn't a string literal, then it is symbolized for `ExampleNode#name`. + macro define_example_group(name) + # Defines a new example group. + # The *what* argument is a name or description of the group. + # If it isn't a string literal, then it is symbolized for `ExampleNode#name`. + macro {{name.id}}(what, &block) + class Group%group < \{{@type.id}}; _spectator_group_subject(\{{what}}) + # TODO: Handle string interpolation in examples and groups. + ::Spectator::DSL::Builder.start_group( + \{{what.is_a?(StringLiteral) ? what : what.stringify}}, + ::Spectator::Source.new(__FILE__, __LINE__) + ) + \{{block.body}} - # Example group: {{what.stringify}} - # Source: {{_source_file}}:{{_source_line}} - macro example_group(what, *, _source_file = __FILE__, _source_line = __LINE__, &block) - class Group%group < {{@type.id}}; _spectator_group_subject({{what}}); ::Spectator::DSL::Builder.start_group({{what.is_a?(StringLiteral) ? what : what.stringify}}, ::Spectator::Source.new(__FILE__, __LINE__)); {{block.body}}; ::Spectator::DSL::Builder.end_group; end - {% debug(false) %} + ::Spectator::DSL::Builder.end_group + end + end end - macro describe(what, *, _source_file = __FILE__, _source_line = __LINE__, &block) - example_group({{what}}, _source_file: {{_source_file}}, _source_line: {{_source_line}}) {{block}} - end + define_example_group :example_group - macro context(what, *, _source_file = __FILE__, _source_line = __LINE__, &block) - example_group({{what}}, _source_file: {{_source_file}}, _source_line: {{_source_line}}) {{block}} - end + define_example_group :describe + + define_example_group :context # Defines the implicit subject for the test context. # If *what* is a type, then the `described_class` method will be defined. From acb3b16496d4678832847e524133f2546d71be91 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 26 Sep 2020 18:14:22 -0600 Subject: [PATCH 035/399] Add some docs --- src/spectator/context.cr | 4 +++ src/spectator/dsl/builder.cr | 16 ++++++++++ src/spectator/spec/builder.cr | 59 ++++++++++++++++++++++++++++++----- 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/src/spectator/context.cr b/src/spectator/context.cr index d2c5d55..9a4607e 100644 --- a/src/spectator/context.cr +++ b/src/spectator/context.cr @@ -4,5 +4,9 @@ module Spectator # Base class that all test cases run in. # This type is used to store all test case contexts as a single type. # The instance must be downcast to the correct type before calling a context method. + # + # Nested contexts, such as those defined by `context` and `describe` in the DSL, can define their own methods. + # The intent is that a proc will downcast to the correct type and call one of those methods. + # This is how methods that contain test cases, hooks, and other context-specific code blocks get invoked. alias Context = ::SpectatorContext end diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr index 13284a8..d0d5040 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -1,19 +1,35 @@ require "../spec/builder" module Spectator::DSL + # Incrementally builds up a test spec from the DSL. + # This is intended to be used only by the Spectator DSL. module Builder extend self + # Underlying spec builder. @@builder = Spec::Builder.new + # Defines a new example group and pushes it onto the group stack. + # Examples and groups defined after calling this method will be nested under the new group. + # The group will be finished and popped off the stack when `#end_example` is called. + # + # See `Spec::Builder#start_group` for usage details. def start_group(*args) @@builder.start_group(*args) end + # Completes a previously defined example group and pops it off the group stack. + # Be sure to call `#start_group` and `#end_group` symmetically. + # + # See `Spec::Builder#end_group` for usage details. def end_group(*args) @@builder.end_group(*args) end + # Defines a new example. + # The example is added to the group currently on the top of the stack. + # + # See `Spec::Builder#add_example` for usage details. def add_example(*args, &block : Example, Context ->) @@builder.add_example(*args, &block) end diff --git a/src/spectator/spec/builder.cr b/src/spectator/spec/builder.cr index 1eb16b3..0ac3d93 100644 --- a/src/spectator/spec/builder.cr +++ b/src/spectator/spec/builder.cr @@ -3,6 +3,10 @@ require "../example_context_method" require "../example_group" module Spectator + # Progressively builds a test spec. + # + # A stack is used to track the current example group. + # Adding an example or group will nest it under the group at the top of the stack. class Spec::Builder # Stack tracking the current group. # The bottom of the stack (first element) is the root group. @@ -11,16 +15,26 @@ module Spectator # New examples should be added to the current group. @group_stack : Deque(ExampleGroup) + # Creates a new spec builder. + # A root group is pushed onto the group stack. def initialize root_group = ExampleGroup.new @group_stack = Deque(ExampleGroup).new @group_stack.push(root_group) end - def add_example - raise NotImplementedError.new("#add_example") - end - + # Defines a new example group and pushes it onto the group stack. + # Examples and groups defined after calling this method will be nested under the new group. + # The group will be finished and popped off the stack when `#end_example` is called. + # + # The *name* is the name or brief description of the group. + # This should be a symbol when describing a type - the type name is represented as a symbol. + # Otherwise, a string should be used. + # + # The *source* optionally defined where the group originates in source code. + # + # The newly created group is returned. + # It shouldn't be used outside of this class until a matching `#end_group` is called. def start_group(name, source = nil) : ExampleGroup {% if flag?(:spectator_debug) %} puts "Start group: #{name.inspect} @ #{source}" @@ -30,7 +44,13 @@ module Spectator end end - def end_group + # Completes a previously defined example group and pops it off the group stack. + # Be sure to call `#start_group` and `#end_group` symmetically. + # + # The completed group will be returned. + # At this point, it is safe to use the group. + # All of its examples and sub-groups have been populated. + def end_group : ExampleGroup {% if flag?(:spectator_debug) %} puts "End group: #{current_group}" {% end %} @@ -39,7 +59,25 @@ module Spectator @group_stack.pop end - def add_example(name, source, context, &block : Example, Context ->) + # Defines a new example. + # The example is added to the group currently on the top of the stack. + # + # The *name* is the name or brief description of the example. + # This should be a string or nil. + # When nil, the example's name will be populated by the first expectation run inside of the test code. + # + # The *source* optionally defined where the example originates in source code. + # + # The *context* is an instance of the context the test code should run in. + # See `Context` for more information. + # + # A block must be provided. + # It will be yielded two arguments - the example created by this method, and the *context* argument. + # The return value of the block is ignored. + # It is expected that the test code runs when the block is called. + # + # The newly created example is returned. + def add_example(name, source, context, &block : Example, Context ->) : Example {% if flag?(:spectator_debug) %} puts "Add example: #{name} @ #{source}" {% end %} @@ -48,7 +86,12 @@ module Spectator # The example is added to the current group by `Example` initializer. end - def build + # Constructs the test spec. + # Returns the spec instance. + # + # Raises an error if there were not symmetrical calls to `#start_group` and `#end_group`. + # This would indicate a logical error somewhere in Spectator or an extension of it. + def build : Spec raise NotImplementedError.new("#build") end @@ -62,7 +105,7 @@ module Spectator @group_stack.first end - # Retrieves the current group. + # Retrieves the current group, which is at the top of the stack. # This is the group that new examples should be added to. private def current_group @group_stack.last From 96a79898156c3c7a4da2202b0b30d1c150922084 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 26 Sep 2020 18:14:41 -0600 Subject: [PATCH 036/399] Remove unreferenced code --- src/spectator/dsl/examples.cr | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index beb07b6..fa7c0d0 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -24,32 +24,6 @@ module Spectator::DSL define_example :specify end - macro it(description = nil, _source_file = __FILE__, _source_line = __LINE__, &block) - {% if block.is_a?(Nop) %} - {% if description.is_a?(Call) %} - def %run - {{description}} - end - {% else %} - {% raise "Unrecognized syntax: `it #{description}` at #{_source_file}:#{_source_line}" %} - {% end %} - {% else %} - def %run - {{block.body}} - end - {% end %} - - %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - ::Spectator::SpecBuilder.add_example( - {{description.is_a?(StringLiteral) || description.is_a?(StringInterpolation) || description.is_a?(NilLiteral) ? description : description.stringify}}, - %source, - {{@type.name}} - ) { |test| test.as({{@type.name}}).%run } - end - - macro specify(description = nil, &block) - it({{description}}) {{block}} - end macro pending(description = nil, _source_file = __FILE__, _source_line = __LINE__, &block) {% if block.is_a?(Nop) %} From f4a05502f99b9764d73076f5b8f34d39c2082c35 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 26 Sep 2020 18:14:59 -0600 Subject: [PATCH 037/399] Example names can't be a symbol --- src/spectator/example.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index c392fab..b3c254e 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -20,7 +20,7 @@ module Spectator # The *source* tracks where the example exists in source code. # The example will be assigned to *group* if it is provided. def initialize(@delegate : ExampleContextDelegate, - name : String | Symbol? = nil, source : Source? = nil, group : ExampleGroup? = nil) + name : String? = nil, source : Source? = nil, group : ExampleGroup? = nil) super(name, source, group) end From f1ad476ae5eb341c8c7d65c4fccbbf4a10951d4d Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 26 Sep 2020 18:16:21 -0600 Subject: [PATCH 038/399] Trick to use one version string from shard.yml --- src/spectator.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator.cr b/src/spectator.cr index 1b60f69..81db191 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -6,7 +6,7 @@ module Spectator extend self # Current version of the Spectator library. - VERSION = "0.10.0" + VERSION = {{ `shards version #{__DIR__}`.stringify.chomp }} # Top-level describe method. # All specs in a file must be wrapped in this call. From cccfa8ea1d0970181ef7ebff8c7c2a99960bd67e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 26 Sep 2020 18:23:48 -0600 Subject: [PATCH 039/399] Formalize Spectator debug --- src/spectator.cr | 8 ++++++++ src/spectator/spec/builder.cr | 12 +++--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/spectator.cr b/src/spectator.cr index 81db191..0f23538 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -8,6 +8,14 @@ module Spectator # Current version of the Spectator library. VERSION = {{ `shards version #{__DIR__}`.stringify.chomp }} + # Inserts code to produce a debug message. + # The code will only be injected when `spectator_debug` is defined (`-Dspectator_debug`). + macro debug_out(message) + {% if flag?(:spectator_debug) %} + STDERR.puts {{message}} + {% end %} + end + # Top-level describe method. # All specs in a file must be wrapped in this call. # This takes an argument and a block. diff --git a/src/spectator/spec/builder.cr b/src/spectator/spec/builder.cr index 0ac3d93..9071b85 100644 --- a/src/spectator/spec/builder.cr +++ b/src/spectator/spec/builder.cr @@ -36,9 +36,7 @@ module Spectator # The newly created group is returned. # It shouldn't be used outside of this class until a matching `#end_group` is called. def start_group(name, source = nil) : ExampleGroup - {% if flag?(:spectator_debug) %} - puts "Start group: #{name.inspect} @ #{source}" - {% end %} + Spectator.debug_out("Start group: #{name.inspect} @ #{source}") ExampleGroup.new(name, source, current_group).tap do |group| @group_stack << group end @@ -51,9 +49,7 @@ module Spectator # At this point, it is safe to use the group. # All of its examples and sub-groups have been populated. def end_group : ExampleGroup - {% if flag?(:spectator_debug) %} - puts "End group: #{current_group}" - {% end %} + Spectator.debug_out("End group: #{current_group}") raise "Can't pop root group" if root? @group_stack.pop @@ -78,9 +74,7 @@ module Spectator # # The newly created example is returned. def add_example(name, source, context, &block : Example, Context ->) : Example - {% if flag?(:spectator_debug) %} - puts "Add example: #{name} @ #{source}" - {% end %} + Spectator.debug_out("Add example: #{name} @ #{source}") delegate = ExampleContextDelegate.new(context, block) Example.new(delegate, name, source, current_group) # The example is added to the current group by `Example` initializer. From 1ad41ac016bf3d5dc947f0f9b319ed98e69f89a6 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 26 Sep 2020 19:11:52 -0600 Subject: [PATCH 040/399] Cleanup group description handling --- src/spectator/dsl/groups.cr | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 5f034c9..1ff332b 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -13,8 +13,8 @@ module Spectator::DSL class Group%group < \{{@type.id}}; _spectator_group_subject(\{{what}}) # TODO: Handle string interpolation in examples and groups. ::Spectator::DSL::Builder.start_group( - \{{what.is_a?(StringLiteral) ? what : what.stringify}}, ::Spectator::Source.new(__FILE__, __LINE__) + _spectator_group_name(\{{what}}), ) \{{block.body}} @@ -24,6 +24,27 @@ module Spectator::DSL end end + # Inserts the correct representation of a group's name. + # If *what* appears to be a type name, it will be symbolized. + # If it's a string, then it is dropped in as-is. + # For anything else, it is stringified. + # This is intended to be used to convert a description from the spec DSL to `ExampleNode#name`. + private macro _spectator_group_name(what) + {% if (what.is_a?(Generic) || + what.is_a?(Path) || + what.is_a?(TypeNode) || + what.is_a?(Union)) && + (described_type = what.resolve?).is_a?(TypeNode) %} + {{what.symbolize}} + {% elsif what.is_a?(StringLiteral) || + what.is_a?(StringInterpolation) || + what.is_a?(NilLiteral) %} + {{what}} + {% else %} + {{what.stringify}} + {% end %} + end + define_example_group :example_group define_example_group :describe From 1d359efcb0af70908f6c62bc808c5be50d575779 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 26 Sep 2020 19:12:42 -0600 Subject: [PATCH 041/399] Improve source line detection --- src/spectator/dsl/examples.cr | 15 +++++++-------- src/spectator/dsl/groups.cr | 10 ++++++---- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index fa7c0d0..f3b5f46 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -4,15 +4,14 @@ require "./builder" module Spectator::DSL module Examples macro define_example(name) - macro {{name.id}}(what = nil) + macro {{name.id}}(what = nil, &block) def %test - \{{yield}} + \{{block.body}} end - %source = ::Spectator::Source.new(__FILE__, __LINE__) ::Spectator::DSL::Builder.add_example( - \{{what.is_a?(StringLiteral) || what.is_a?(NilLiteral) ? what : what.stringify}}, - %source, + \{{what.is_a?(StringLiteral) || what.is_a?(StringInterpolation) || what.is_a?(NilLiteral) ? what : what.stringify}}, + ::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}}), \{{@type.name}}.new ) { |example, context| context.as(\{{@type.name}}).%test } end @@ -25,7 +24,7 @@ module Spectator::DSL define_example :specify end - macro pending(description = nil, _source_file = __FILE__, _source_line = __LINE__, &block) + macro pending(description = nil, _source_file = __FILE__, _source_line = __LINE__, &block) {% if block.is_a?(Nop) %} {% if description.is_a?(Call) %} def %run @@ -48,11 +47,11 @@ module Spectator::DSL ) { |test| test.as({{@type.name}}).%run } end - macro skip(description = nil, &block) + macro skip(description = nil, &block) pending({{description}}) {{block}} end - macro xit(description = nil, &block) + macro xit(description = nil, &block) pending({{description}}) {{block}} end end diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 1ff332b..026b5dd 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -8,13 +8,15 @@ module Spectator::DSL macro define_example_group(name) # Defines a new example group. # The *what* argument is a name or description of the group. - # If it isn't a string literal, then it is symbolized for `ExampleNode#name`. + # + # TODO: Handle string interpolation in example and group names. macro {{name.id}}(what, &block) - class Group%group < \{{@type.id}}; _spectator_group_subject(\{{what}}) - # TODO: Handle string interpolation in examples and groups. + class Group%group < \{{@type.id}} + _spectator_group_subject(\{{what}}) + ::Spectator::DSL::Builder.start_group( - ::Spectator::Source.new(__FILE__, __LINE__) _spectator_group_name(\{{what}}), + ::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}}) ) \{{block.body}} From b8ba38152e987912c71db0ef4be3a870b77ac96b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 26 Sep 2020 19:14:08 -0600 Subject: [PATCH 042/399] Cleanup example description handling --- src/spectator/dsl/examples.cr | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index f3b5f46..82a5036 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -10,13 +10,27 @@ module Spectator::DSL end ::Spectator::DSL::Builder.add_example( - \{{what.is_a?(StringLiteral) || what.is_a?(StringInterpolation) || what.is_a?(NilLiteral) ? what : what.stringify}}, + _spectator_example_name(\{{what}}), ::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}}), \{{@type.name}}.new ) { |example, context| context.as(\{{@type.name}}).%test } end end + # Inserts the correct representation of a example's name. + # If *what* is a string, then it is dropped in as-is. + # For anything else, it is stringified. + # This is intended to be used to convert a description from the spec DSL to `ExampleNode#name`. + private macro _spectator_example_name(what) + {% if what.is_a?(StringLiteral) || + what.is_a?(StringInterpolation) || + what.is_a?(NilLiteral) %} + {{what}} + {% else %} + {{what.stringify}} + {% end %} + end + define_example :example define_example :it From 60795a371d34b5b1cce182506b8fbbaba7dfe1de Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 26 Sep 2020 19:15:34 -0600 Subject: [PATCH 043/399] Reorganize --- src/spectator/dsl/groups.cr | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 026b5dd..8dcbe81 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -47,12 +47,6 @@ module Spectator::DSL {% end %} end - define_example_group :example_group - - define_example_group :describe - - define_example_group :context - # Defines the implicit subject for the test context. # If *what* is a type, then the `described_class` method will be defined. # Additionally, the implicit subject is set to an instance of *what* if it's not a module. @@ -83,6 +77,12 @@ module Spectator::DSL end {% end %} end + + define_example_group :example_group + + define_example_group :describe + + define_example_group :context end macro sample(collection, count = nil, _source_file = __FILE__, _source_line = __LINE__, &block) From 99a9d7960a8638ad3382e55b95cb2a8b07e86019 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 26 Sep 2020 19:23:16 -0600 Subject: [PATCH 044/399] Formatting --- src/spectator/dsl/examples.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 82a5036..76edc15 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -23,8 +23,8 @@ module Spectator::DSL # This is intended to be used to convert a description from the spec DSL to `ExampleNode#name`. private macro _spectator_example_name(what) {% if what.is_a?(StringLiteral) || - what.is_a?(StringInterpolation) || - what.is_a?(NilLiteral) %} + what.is_a?(StringInterpolation) || + what.is_a?(NilLiteral) %} {{what}} {% else %} {{what.stringify}} From 4567162459be16959a8e796a543464213158952c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 26 Sep 2020 19:23:57 -0600 Subject: [PATCH 045/399] Prevent defining an example or group in a test --- src/spectator/dsl/examples.cr | 2 ++ src/spectator/dsl/groups.cr | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 76edc15..0082fc5 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -5,6 +5,8 @@ module Spectator::DSL module Examples macro define_example(name) macro {{name.id}}(what = nil, &block) + \{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %} + def %test \{{block.body}} end diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 8dcbe81..2d82ecf 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -11,6 +11,8 @@ module Spectator::DSL # # TODO: Handle string interpolation in example and group names. macro {{name.id}}(what, &block) + \{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %} + class Group%group < \{{@type.id}} _spectator_group_subject(\{{what}}) From 543df88d39993d7080862cbd972aafeb78f13f7c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 26 Sep 2020 22:25:04 -0600 Subject: [PATCH 046/399] Forward build method --- src/spectator/dsl/builder.cr | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr index d0d5040..fd3e42a 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -33,5 +33,14 @@ module Spectator::DSL def add_example(*args, &block : Example, Context ->) @@builder.add_example(*args, &block) end + + # Constructs the test spec. + # Returns the spec instance. + # + # Raises an error if there were not symmetrical calls to `#start_group` and `#end_group`. + # This would indicate a logical error somewhere in Spectator or an extension of it. + def build : Spec + @@builder.build + end end end From d663e82c366d78dcb002219269a0512eb06398d9 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 26 Sep 2020 22:25:53 -0600 Subject: [PATCH 047/399] Improve internal error handling output --- src/spectator.cr | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/src/spectator.cr b/src/spectator.cr index 0f23538..d3e63fe 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -103,7 +103,7 @@ module Spectator # But if an exception occurs outside an example, # it's likely the fault of the test framework (Spectator). # So we display a helpful error that could be reported and return non-zero. - display_error_stack(ex) + display_framework_error(ex) false end @@ -147,22 +147,11 @@ module Spectator # Displays a complete error stack. # Prints an error and everything that caused it. # Stacktrace is included. - private def display_error_stack(error) : Nil - puts - puts "Encountered an unexpected error in framework" - # Loop while there's a cause for the error. - # Print each error in the stack. - loop do - display_error(error) - error = error.cause - break unless error - end - end - - # Display a single error and its stacktrace. - private def display_error(error) : Nil - puts - puts "Caused by: #{error.message}" - puts error.backtrace.join("\n") + private def display_framework_error(error) : Nil + STDERR.puts + STDERR.puts "!> Spectator encountered an unexpected error." + STDERR.puts "!> This is probably a bug and should be reported." + STDERR.puts + error.inspect_with_backtrace(STDERR) end end From 579fcacfdea388a4d2df2b091f004fd3088c91b8 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 26 Sep 2020 22:51:58 -0600 Subject: [PATCH 048/399] Move spec builder --- src/spectator/dsl/builder.cr | 5 +++-- src/spectator/{spec/builder.cr => spec_builder.cr} | 12 +++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) rename src/spectator/{spec/builder.cr => spec_builder.cr} (95%) diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr index fd3e42a..638b39d 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -1,4 +1,5 @@ -require "../spec/builder" +require "../spec" +require "../spec_builder" module Spectator::DSL # Incrementally builds up a test spec from the DSL. @@ -7,7 +8,7 @@ module Spectator::DSL extend self # Underlying spec builder. - @@builder = Spec::Builder.new + @@builder = SpecBuilder.new # Defines a new example group and pushes it onto the group stack. # Examples and groups defined after calling this method will be nested under the new group. diff --git a/src/spectator/spec/builder.cr b/src/spectator/spec_builder.cr similarity index 95% rename from src/spectator/spec/builder.cr rename to src/spectator/spec_builder.cr index 9071b85..fad4f22 100644 --- a/src/spectator/spec/builder.cr +++ b/src/spectator/spec_builder.cr @@ -1,13 +1,13 @@ -require "../example" -require "../example_context_method" -require "../example_group" +require "./example" +require "./example_context_method" +require "./example_group" module Spectator # Progressively builds a test spec. # # A stack is used to track the current example group. # Adding an example or group will nest it under the group at the top of the stack. - class Spec::Builder + class SpecBuilder # Stack tracking the current group. # The bottom of the stack (first element) is the root group. # The root group should never be removed. @@ -86,7 +86,9 @@ module Spectator # Raises an error if there were not symmetrical calls to `#start_group` and `#end_group`. # This would indicate a logical error somewhere in Spectator or an extension of it. def build : Spec - raise NotImplementedError.new("#build") + raise "Mismatched start and end groups" unless root? + + Spec.new(root_group) end # Checks if the current group is the root group. From ec6018bed4ab024b5627fb0b55a57572a82de187 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 27 Sep 2020 09:10:27 -0600 Subject: [PATCH 049/399] Start reactivating runner --- src/spectator.cr | 4 ++-- src/spectator/example.cr | 2 ++ src/spectator/spec.cr | 17 ++++++++++++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/spectator.cr b/src/spectator.cr index d3e63fe..a6708af 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -93,8 +93,8 @@ module Spectator # Builds the tests and runs the framework. private def run - # Build the test suite and run it. - # suite = ::Spectator::SpecBuilder.build(config.example_filter) + # Build the test spec and run it. + spec = ::Spectator::DSL::Builder.build # Runner.new(suite, config).run true rescue ex diff --git a/src/spectator/example.cr b/src/spectator/example.cr index b3c254e..e5607e2 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -28,6 +28,8 @@ module Spectator # Returns the result of the execution. # The result will also be stored in `#result`. def run : Result + Spectator.debug_out("Running example #{example}") + @delegate.call(self) raise NotImplementedError.new("#run") end diff --git a/src/spectator/spec.cr b/src/spectator/spec.cr index ebe3f9d..90a2b6a 100644 --- a/src/spectator/spec.cr +++ b/src/spectator/spec.cr @@ -1,6 +1,21 @@ -require "./spec/*" +require "./example" +require "./example_group" module Spectator class Spec + include Enumerable(Example) + + def initialize(@group : ExampleGroup) + end + + def each + @group.each do |node| + if (example = node.as?(Example)) + yield example + elsif (group = node.as?(ExampleGroup)) + # TODO + end + end + end end end From 4974054de7e429a168eb3c8ebfb6ed7ae9beae4a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Oct 2020 11:23:51 -0600 Subject: [PATCH 050/399] Some code to run a spec --- src/spectator.cr | 2 +- src/spectator/example.cr | 2 +- src/spectator/example_group.cr | 6 ++++++ src/spectator/example_iterator.cr | 15 +++++++++------ src/spectator/spec.cr | 24 ++++++++++++++---------- 5 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/spectator.cr b/src/spectator.cr index a6708af..89c8306 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -95,7 +95,7 @@ module Spectator private def run # Build the test spec and run it. spec = ::Spectator::DSL::Builder.build - # Runner.new(suite, config).run + spec.run true rescue ex # Catch all unhandled exceptions here. diff --git a/src/spectator/example.cr b/src/spectator/example.cr index e5607e2..b395748 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -28,7 +28,7 @@ module Spectator # Returns the result of the execution. # The result will also be stored in `#result`. def run : Result - Spectator.debug_out("Running example #{example}") + Spectator.debug_out("Running example #{self}") @delegate.call(self) raise NotImplementedError.new("#run") end diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index 80b6672..d6d5f84 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -4,6 +4,7 @@ module Spectator # Collection of examples and sub-groups. class ExampleGroup < ExampleNode include Enumerable(ExampleNode) + include Iterable(ExampleNode) @nodes = [] of ExampleNode @@ -22,6 +23,11 @@ module Spectator @nodes.each { |node| yield node } end + # Returns an iterator for each (example and sub-group). + def each + @nodes.each + end + # Checks if all examples and sub-groups have finished. def finished? : Bool @nodes.all?(&.finished?) diff --git a/src/spectator/example_iterator.cr b/src/spectator/example_iterator.cr index b5dd3f3..217f0a0 100644 --- a/src/spectator/example_iterator.cr +++ b/src/spectator/example_iterator.cr @@ -1,3 +1,7 @@ +require "./example" +require "./example_group" +require "./example_node" + module Spectator # Iterates through all examples in a group and its nested groups. class ExampleIterator @@ -5,12 +9,12 @@ module Spectator # Stack that contains the iterators for each group. # A stack is used to track where in the tree this iterator is. - @stack : Array(Iterator(ExampleComponent)) + @stack : Array(Iterator(ExampleNode)) # Creates a new iterator. # The *group* is the example group to iterate through. - def initialize(@group : Iterable(ExampleComponent)) - iter = @group.each.as(Iterator(ExampleComponent)) + def initialize(@group : ExampleGroup) + iter = @group.each.as(Iterator(ExampleNode)) @stack = [iter] end @@ -22,8 +26,7 @@ module Spectator # b. the stack is empty. until @stack.empty? # Retrieve the next "thing". - # This could be an `Example`, - # or a group. + # This could be an `Example` or a group. item = advance # Return the item if it's an example. # Otherwise, advance and check the next one. @@ -36,7 +39,7 @@ module Spectator # Restart the iterator at the beginning. def rewind # Same code as `#initialize`, but return self. - iter = @group.each.as(Iterator(ExampleComponent)) + iter = @group.each.as(Iterator(ExampleNode)) @stack = [iter] self end diff --git a/src/spectator/spec.cr b/src/spectator/spec.cr index 90a2b6a..17218f9 100644 --- a/src/spectator/spec.cr +++ b/src/spectator/spec.cr @@ -1,20 +1,24 @@ require "./example" require "./example_group" +require "./example_iterator" module Spectator + # Contains examples to be tested. class Spec - include Enumerable(Example) - - def initialize(@group : ExampleGroup) + def initialize(@root : ExampleGroup) end - def each - @group.each do |node| - if (example = node.as?(Example)) - yield example - elsif (group = node.as?(ExampleGroup)) - # TODO - end + def run + examples = ExampleIterator.new(@root).to_a + Runner.new(examples).run + end + + private struct Runner + def initialize(@examples : Array(Example)) + end + + def run + @examples.each(&.run) end end end From e6d78345c4ff00c8658987f70ed6d48949679470 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Oct 2020 11:25:46 -0600 Subject: [PATCH 051/399] Rename debug macro --- src/spectator.cr | 2 +- src/spectator/example.cr | 2 +- src/spectator/spec_builder.cr | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/spectator.cr b/src/spectator.cr index 89c8306..407687e 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -10,7 +10,7 @@ module Spectator # Inserts code to produce a debug message. # The code will only be injected when `spectator_debug` is defined (`-Dspectator_debug`). - macro debug_out(message) + macro debug(message) {% if flag?(:spectator_debug) %} STDERR.puts {{message}} {% end %} diff --git a/src/spectator/example.cr b/src/spectator/example.cr index b395748..07d7415 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -28,7 +28,7 @@ module Spectator # Returns the result of the execution. # The result will also be stored in `#result`. def run : Result - Spectator.debug_out("Running example #{self}") + Spectator.debug("Running example #{self}") @delegate.call(self) raise NotImplementedError.new("#run") end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index fad4f22..3e3850e 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -36,7 +36,7 @@ module Spectator # The newly created group is returned. # It shouldn't be used outside of this class until a matching `#end_group` is called. def start_group(name, source = nil) : ExampleGroup - Spectator.debug_out("Start group: #{name.inspect} @ #{source}") + Spectator.debug("Start group: #{name.inspect} @ #{source}") ExampleGroup.new(name, source, current_group).tap do |group| @group_stack << group end @@ -49,7 +49,7 @@ module Spectator # At this point, it is safe to use the group. # All of its examples and sub-groups have been populated. def end_group : ExampleGroup - Spectator.debug_out("End group: #{current_group}") + Spectator.debug("End group: #{current_group}") raise "Can't pop root group" if root? @group_stack.pop @@ -74,7 +74,7 @@ module Spectator # # The newly created example is returned. def add_example(name, source, context, &block : Example, Context ->) : Example - Spectator.debug_out("Add example: #{name} @ #{source}") + Spectator.debug("Add example: #{name} @ #{source}") delegate = ExampleContextDelegate.new(context, block) Example.new(delegate, name, source, current_group) # The example is added to the current group by `Example` initializer. From 9c1fd6fb5a6887c813b81d0c32427c436df4b8ea Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Oct 2020 11:46:21 -0600 Subject: [PATCH 052/399] Use standard log utility --- src/spectator.cr | 25 ++++++------------------- src/spectator/example.cr | 2 +- src/spectator/spec_builder.cr | 8 +++++--- 3 files changed, 12 insertions(+), 23 deletions(-) diff --git a/src/spectator.cr b/src/spectator.cr index 407687e..a2df911 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -1,6 +1,9 @@ +require "log" require "./spectator/includes" require "./spectator_test_context" +Log.setup_from_env + # Module that contains all functionality related to Spectator. module Spectator extend self @@ -8,13 +11,8 @@ module Spectator # Current version of the Spectator library. VERSION = {{ `shards version #{__DIR__}`.stringify.chomp }} - # Inserts code to produce a debug message. - # The code will only be injected when `spectator_debug` is defined (`-Dspectator_debug`). - macro debug(message) - {% if flag?(:spectator_debug) %} - STDERR.puts {{message}} - {% end %} - end + # Logger for Spectator internals. + Log = ::Log.for(self) # Top-level describe method. # All specs in a file must be wrapped in this call. @@ -103,7 +101,7 @@ module Spectator # But if an exception occurs outside an example, # it's likely the fault of the test framework (Spectator). # So we display a helpful error that could be reported and return non-zero. - display_framework_error(ex) + Log.fatal(exception: ex) { "Spectator encountered an unexpected error" } false end @@ -143,15 +141,4 @@ module Spectator private def apply_command_line_args : Nil CommandLineArgumentsConfigSource.new.apply_to(@@config_builder) end - - # Displays a complete error stack. - # Prints an error and everything that caused it. - # Stacktrace is included. - private def display_framework_error(error) : Nil - STDERR.puts - STDERR.puts "!> Spectator encountered an unexpected error." - STDERR.puts "!> This is probably a bug and should be reported." - STDERR.puts - error.inspect_with_backtrace(STDERR) - end end diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 07d7415..04fbdb0 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -28,7 +28,7 @@ module Spectator # Returns the result of the execution. # The result will also be stored in `#result`. def run : Result - Spectator.debug("Running example #{self}") + Log.debug { "Running example #{self}" } @delegate.call(self) raise NotImplementedError.new("#run") end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 3e3850e..09c81a0 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -8,6 +8,8 @@ module Spectator # A stack is used to track the current example group. # Adding an example or group will nest it under the group at the top of the stack. class SpecBuilder + Log = ::Spectator::Log.for(self) + # Stack tracking the current group. # The bottom of the stack (first element) is the root group. # The root group should never be removed. @@ -36,7 +38,7 @@ module Spectator # The newly created group is returned. # It shouldn't be used outside of this class until a matching `#end_group` is called. def start_group(name, source = nil) : ExampleGroup - Spectator.debug("Start group: #{name.inspect} @ #{source}") + Log.trace { "Start group: #{name.inspect} @ #{source}" } ExampleGroup.new(name, source, current_group).tap do |group| @group_stack << group end @@ -49,7 +51,7 @@ module Spectator # At this point, it is safe to use the group. # All of its examples and sub-groups have been populated. def end_group : ExampleGroup - Spectator.debug("End group: #{current_group}") + Log.trace { "End group: #{current_group}" } raise "Can't pop root group" if root? @group_stack.pop @@ -74,7 +76,7 @@ module Spectator # # The newly created example is returned. def add_example(name, source, context, &block : Example, Context ->) : Example - Spectator.debug("Add example: #{name} @ #{source}") + Log.trace { "Add example: #{name} @ #{source}" } delegate = ExampleContextDelegate.new(context, block) Example.new(delegate, name, source, current_group) # The example is added to the current group by `Example` initializer. From 27875631d365553b129b54f30ea0c41bb3112192 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Oct 2020 11:51:16 -0600 Subject: [PATCH 053/399] Mostly implement inspect method --- src/spectator/example.cr | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 04fbdb0..eb6dda1 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -11,6 +11,7 @@ module Spectator getter? finished : Bool = false # Retrieves the result of the last time the example ran. + # TODO: Make result not nil and default to pending. getter! result : Result # Creates the example. @@ -35,7 +36,18 @@ module Spectator # Exposes information about the example useful for debugging. def inspect(io) - raise NotImplementedError.new("#inspect") + # Full example name. + io << '"' + to_s(io) + io << '"' + + # Add source if it's available. + if (s = source) + io << " @ " + io << s + end + + # TODO: Add result. end end end From 788b12a8bcccb9d52ee6ccacc1f980e4276b6eee Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Oct 2020 12:12:04 -0600 Subject: [PATCH 054/399] Fix example methods overriding previously defined methods --- src/spectator/dsl/examples.cr | 4 ++-- src/spectator/dsl/groups.cr | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 0082fc5..e245f79 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -7,7 +7,7 @@ module Spectator::DSL macro {{name.id}}(what = nil, &block) \{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %} - def %test + def \%test \{{block.body}} end @@ -15,7 +15,7 @@ module Spectator::DSL _spectator_example_name(\{{what}}), ::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}}), \{{@type.name}}.new - ) { |example, context| context.as(\{{@type.name}}).%test } + ) { |example, context| context.as(\{{@type.name}}).\%test } end end diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 2d82ecf..15deaac 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -13,7 +13,7 @@ module Spectator::DSL macro {{name.id}}(what, &block) \{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %} - class Group%group < \{{@type.id}} + class Group\%group < \{{@type.id}} _spectator_group_subject(\{{what}}) ::Spectator::DSL::Builder.start_group( From 4462f27316444a3acb8db611e2fe5896226a70d0 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Oct 2020 14:56:31 -0600 Subject: [PATCH 055/399] Rework result types --- src/spectator/error_result.cr | 18 +++++++++++ src/spectator/errored_result.cr | 50 ------------------------------ src/spectator/example.cr | 14 ++++++--- src/spectator/fail_result.cr | 28 +++++++++++++++++ src/spectator/failed_result.cr | 42 ------------------------- src/spectator/finished_result.cr | 29 ----------------- src/spectator/pass_result.cr | 16 ++++++++++ src/spectator/pending_result.cr | 17 +++++----- src/spectator/result.cr | 40 ++++++------------------ src/spectator/successful_result.cr | 23 -------------- 10 files changed, 89 insertions(+), 188 deletions(-) create mode 100644 src/spectator/error_result.cr delete mode 100644 src/spectator/errored_result.cr create mode 100644 src/spectator/fail_result.cr delete mode 100644 src/spectator/failed_result.cr delete mode 100644 src/spectator/finished_result.cr create mode 100644 src/spectator/pass_result.cr delete mode 100644 src/spectator/successful_result.cr diff --git a/src/spectator/error_result.cr b/src/spectator/error_result.cr new file mode 100644 index 0000000..cb47a4f --- /dev/null +++ b/src/spectator/error_result.cr @@ -0,0 +1,18 @@ +require "./fail_result" + +module Spectator + # Outcome that indicates running an example generated an error. + # This occurs when an unexpected exception was raised while running an example. + # This is different from a "failed" result in that the error was not from a failed assertion. + class ErrorResult < FailResult + # Calls the `error` method on *visitor*. + def accept(visitor) + visitor.error + end + + # One-word description of the result. + def to_s(io) + io << "error" + end + end +end diff --git a/src/spectator/errored_result.cr b/src/spectator/errored_result.cr deleted file mode 100644 index 00c73ff..0000000 --- a/src/spectator/errored_result.cr +++ /dev/null @@ -1,50 +0,0 @@ -require "./failed_result" - -module Spectator - # Outcome that indicates running an example generated an error. - # This type of result occurs when an exception was raised. - # This is different from a "failed" result - # in that the error was not from a failed expectation. - class ErroredResult < FailedResult - # Calls the `error` method on *interface*. - def call(interface) - interface.error - end - - # Calls the `error` method on *interface* - # and passes the yielded value. - def call(interface) - value = yield self - interface.error(value) - end - - # One-word descriptor of the result. - def to_s(io) - 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 - end - end -end diff --git a/src/spectator/example.cr b/src/spectator/example.cr index eb6dda1..bcfd96e 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -1,6 +1,8 @@ require "./example_context_delegate" require "./example_group" require "./example_node" +require "./pass_result" +require "./pending_result" require "./result" require "./source" @@ -11,8 +13,7 @@ module Spectator getter? finished : Bool = false # Retrieves the result of the last time the example ran. - # TODO: Make result not nil and default to pending. - getter! result : Result + getter result : Result = PendingResult.new # Creates the example. # The *delegate* contains the test context and method that runs the test case. @@ -30,8 +31,11 @@ module Spectator # The result will also be stored in `#result`. def run : Result Log.debug { "Running example #{self}" } - @delegate.call(self) - raise NotImplementedError.new("#run") + elapsed = Time.measure do + @delegate.call(self) + end + @finished = true + @result = PassResult.new(elapsed) end # Exposes information about the example useful for debugging. @@ -47,7 +51,7 @@ module Spectator io << s end - # TODO: Add result. + io << result end end end diff --git a/src/spectator/fail_result.cr b/src/spectator/fail_result.cr new file mode 100644 index 0000000..710698a --- /dev/null +++ b/src/spectator/fail_result.cr @@ -0,0 +1,28 @@ +require "./result" + +module Spectator + # Outcome that indicates an example failed. + # This typically means an assertion did not pass. + class FailResult < Result + # Error that occurred while running the example. + # This describes the primary reason for the failure. + getter error : Exception + + # Creates a failure result. + # The *elapsed* argument is the length of time it took to run the example. + # The *error* is the exception raised that caused the failure. + def initialize(elapsed, @error) + super(elapsed) + end + + # Calls the `failure` method on *visitor*. + def accept(visitor) + visitor.failure + end + + # One-word description of the result. + def to_s(io) + io << "fail" + end + end +end diff --git a/src/spectator/failed_result.cr b/src/spectator/failed_result.cr deleted file mode 100644 index 43b2ea6..0000000 --- a/src/spectator/failed_result.cr +++ /dev/null @@ -1,42 +0,0 @@ -require "./finished_result" - -module Spectator - # Outcome that indicates running an example was a failure. - class FailedResult < FinishedResult - # Error that occurred while running the example. - getter error : Exception - - # Creates a failed result. - # The *example* should refer to the example that was run - # and that this result is for. - # The *elapsed* argument is the length of time it took to run the example. - # The *expectations* references the expectations that were checked in the example. - # The *error* is the exception that was raised to cause the failure. - def initialize(example, elapsed, expectations, @error) - super(example, elapsed, expectations) - end - - # Calls the `failure` method on *interface*. - def call(interface) - interface.failure - end - - # Calls the `failure` method on *interface* - # and passes the yielded value. - def call(interface) - value = yield self - interface.failure(value) - end - - # One-word descriptor of the result. - def to_s(io) - io << "fail" - end - - # Adds all of the JSON fields for finished results and failed results. - private def add_json_fields(json : ::JSON::Builder) - super - json.field("error", error.message) - end - end -end diff --git a/src/spectator/finished_result.cr b/src/spectator/finished_result.cr deleted file mode 100644 index 1e3dae8..0000000 --- a/src/spectator/finished_result.cr +++ /dev/null @@ -1,29 +0,0 @@ -require "./result" - -module Spectator - # Abstract class for all results by examples - abstract class FinishedResult < Result - # Length of time it took to run the example. - getter elapsed : Time::Span - - # The expectations that were run in the example. - getter expectations : Expectations::ExampleExpectations - - # Creates a successful result. - # The *example* should refer to the example that was run - # and that this result is for. - # The *elapsed* argument is the length of time it took to run the example. - # The *expectations* references the expectations that were checked in the example. - def initialize(example, @elapsed, @expectations) - super(example) - end - - # Adds the common JSON fields for all result types - # and fields specific to finished results. - private def add_json_fields(json : ::JSON::Builder) - super - json.field("time", elapsed.total_seconds) - json.field("expectations", expectations) - end - end -end diff --git a/src/spectator/pass_result.cr b/src/spectator/pass_result.cr new file mode 100644 index 0000000..193e3c2 --- /dev/null +++ b/src/spectator/pass_result.cr @@ -0,0 +1,16 @@ +require "./result" + +module Spectator + # Outcome that indicates running an example was successful. + class PassResult < Result + # Calls the `pass` method on *visitor*. + def accept(visitor) + visitor.pass + end + + # One-word description of the result. + def to_s(io) + io << "pass" + end + end +end diff --git a/src/spectator/pending_result.cr b/src/spectator/pending_result.cr index 65512e9..5781edc 100644 --- a/src/spectator/pending_result.cr +++ b/src/spectator/pending_result.cr @@ -5,19 +5,18 @@ module Spectator # A pending result means the example is not ready to run yet. # This can happen when the functionality to be tested is not implemented yet. class PendingResult < Result - # Calls the `pending` method on *interface*. - def call(interface) - interface.pending + # Creates the result. + # *elapsed* is the length of time it took to run the example. + def initialize(elapsed = Time::Span::ZERO) + super end - # Calls the `pending` method on *interface* - # and passes the yielded value. - def call(interface) - value = yield self - interface.pending(value) + # Calls the `pending` method on the *visitor*. + def accept(visitor) + visitor.pending end - # One-word descriptor of the result. + # One-word description of the result. def to_s(io) io << "pending" end diff --git a/src/spectator/result.cr b/src/spectator/result.cr index c2f0b45..056c979 100644 --- a/src/spectator/result.cr +++ b/src/spectator/result.cr @@ -2,39 +2,19 @@ module Spectator # Base class that represents the outcome of running an example. # Sub-classes contain additional information specific to the type of result. abstract class Result - # Example that was run that this result is for. - getter example : Example + # Length of time it took to run the example. + getter elapsed : Time::Span - # Constructs the base of the result. - # The *example* should refer to the example that was run - # and that this result is for. - def initialize(@example) + # The assertions checked in the example. + # getter assertions : Enumerable(Assertion) # TODO: Implement Assertion type. + + # Creates the result. + # *elapsed* is the length of time it took to run the example. + def initialize(@elapsed) end # Calls the corresponding method for the type of result. - # This is used to avoid placing if or case-statements everywhere based on type. - # Each sub-class implements this method by calling the correct method on *interface*. - abstract def call(interface) - - # Calls the corresponding method for the type of result. - # This is used to avoid placing if or case-statements everywhere based on type. - # Each sub-class implements this method by calling the correct method on *interface*. - # This variation takes a block, which is passed the result. - # The value returned from the block will be returned by this method. - abstract def call(interface, &block : Result -> _) - - # Creates a JSON object from the result information. - def to_json(json : ::JSON::Builder) - json.object do - add_json_fields(json) - end - end - - # Adds the common fields for a result to a JSON builder. - private def add_json_fields(json : ::JSON::Builder) - json.field("name", example) - json.field("location", example.source) - json.field("result", to_s) - end + # This is the visitor design pattern. + abstract def accept(visitor) end end diff --git a/src/spectator/successful_result.cr b/src/spectator/successful_result.cr deleted file mode 100644 index bd3f0ed..0000000 --- a/src/spectator/successful_result.cr +++ /dev/null @@ -1,23 +0,0 @@ -require "./finished_result" - -module Spectator - # Outcome that indicates running an example was successful. - class SuccessfulResult < FinishedResult - # Calls the `success` method on *interface*. - def call(interface) - interface.success - end - - # Calls the `success` method on *interface* - # and passes the yielded value. - def call(interface) - value = yield self - interface.success(value) - end - - # One-word descriptor of the result. - def to_s(io) - io << "success" - end - end -end From 79499c5d2e66e9839ee50eda777194fa6b5faf45 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Oct 2020 17:40:38 -0600 Subject: [PATCH 056/399] Add config to spec builder --- src/spectator.cr | 5 +++-- src/spectator/config_builder.cr | 4 ++++ src/spectator/dsl/builder.cr | 7 +++++++ src/spectator/spec.cr | 19 +++++++++++++++++-- src/spectator/spec_builder.cr | 19 ++++++++++++++++++- 5 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/spectator.cr b/src/spectator.cr index a2df911..5391a78 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -91,8 +91,9 @@ module Spectator # Builds the tests and runs the framework. private def run - # Build the test spec and run it. - spec = ::Spectator::DSL::Builder.build + # Build the spec and run it. + # DSL::Builder.config = config # TODO: Set config. + spec = DSL::Builder.build spec.run true rescue ex diff --git a/src/spectator/config_builder.cr b/src/spectator/config_builder.cr index d65ffc1..582edaf 100644 --- a/src/spectator/config_builder.cr +++ b/src/spectator/config_builder.cr @@ -1,3 +1,7 @@ +require "./composite_example_filter" +require "./example_filter" +require "./null_example_filter" + module Spectator # Mutable configuration used to produce a final configuration. # Use the setters in this class to incrementally build a configuration. diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr index 638b39d..dc4647c 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -35,6 +35,13 @@ module Spectator::DSL @@builder.add_example(*args, &block) end + # Sets the configuration of the spec. + # + # See `Spec::Builder#config=` for usage details. + def config=(config) + @@builder.config = config + end + # Constructs the test spec. # Returns the spec instance. # diff --git a/src/spectator/spec.cr b/src/spectator/spec.cr index 17218f9..a1dfa5c 100644 --- a/src/spectator/spec.cr +++ b/src/spectator/spec.cr @@ -1,3 +1,4 @@ +require "./config" require "./example" require "./example_group" require "./example_iterator" @@ -5,14 +6,28 @@ require "./example_iterator" module Spectator # Contains examples to be tested. class Spec - def initialize(@root : ExampleGroup) + def initialize(@root : ExampleGroup, @config : Config) end def run - examples = ExampleIterator.new(@root).to_a Runner.new(examples).run end + # Generates a list of examples to run. + # The order of the examples are also sorted based on the configuration. + private def examples + ExampleIterator.new(@root).to_a.tap do |examples| + if @config.randomize? + random = if (seed = @config.random_seed) + Random.new(seed) + else + Random.new + end + examples.shuffle!(random) + end + end + end + private struct Runner def initialize(@examples : Array(Example)) end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 09c81a0..54f8ed6 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -1,3 +1,5 @@ +require "./config" +require "./config_builder" require "./example" require "./example_context_method" require "./example_group" @@ -17,6 +19,9 @@ module Spectator # New examples should be added to the current group. @group_stack : Deque(ExampleGroup) + # Configuration for the spec. + @config : Config? + # Creates a new spec builder. # A root group is pushed onto the group stack. def initialize @@ -82,6 +87,12 @@ module Spectator # The example is added to the current group by `Example` initializer. end + # Sets the configuration of the spec. + # This configuration controls how examples run. + def config=(config) + @config = config + end + # Constructs the test spec. # Returns the spec instance. # @@ -90,7 +101,7 @@ module Spectator def build : Spec raise "Mismatched start and end groups" unless root? - Spec.new(root_group) + Spec.new(root_group, config) end # Checks if the current group is the root group. @@ -108,5 +119,11 @@ module Spectator private def current_group @group_stack.last end + + # Retrieves the configuration. + # If one wasn't previously set, a default configuration is used. + private def config + @config || ConfigBuilder.new.build + end end end From b2bf98068539f305b22bf7461757b889e94eb03b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Oct 2020 21:39:41 -0600 Subject: [PATCH 057/399] Some config cleanup --- src/spectator/config.cr | 12 ++++------- src/spectator/config_builder.cr | 20 ++++++------------- src/spectator/spec_builder.cr | 35 +++++++++++++++++++++------------ 3 files changed, 32 insertions(+), 35 deletions(-) diff --git a/src/spectator/config.cr b/src/spectator/config.cr index cde982d..bc71d41 100644 --- a/src/spectator/config.cr +++ b/src/spectator/config.cr @@ -16,19 +16,16 @@ module Spectator # Examples won't run, but the output will show that they did. getter? dry_run : Bool - # Random number generator to use for everything. - getter random : Random - - # Indicates whether tests are run in a random order. + # Indicates whether examples run in a random order. getter? randomize : Bool - # Random seed used for number generation. + # Seed used for random number generation. getter! random_seed : UInt64? - # Indicates whether profiling information should be displayed. + # Indicates whether timing information should be displayed. getter? profile : Bool - # Filter that determines which examples to run. + # Filter determining examples to run. getter example_filter : ExampleFilter # Creates a new configuration. @@ -37,7 +34,6 @@ module Spectator @fail_fast = builder.fail_fast? @fail_blank = builder.fail_blank? @dry_run = builder.dry_run? - @random = builder.random @randomize = builder.randomize? @random_seed = builder.seed? @profile = builder.profile? diff --git a/src/spectator/config_builder.cr b/src/spectator/config_builder.cr index 582edaf..1db01dc 100644 --- a/src/spectator/config_builder.cr +++ b/src/spectator/config_builder.cr @@ -1,4 +1,5 @@ require "./composite_example_filter" +require "./config" require "./example_filter" require "./null_example_filter" @@ -12,14 +13,6 @@ module Spectator new.build end - # Random number generator to use. - protected getter random = Random::DEFAULT - - def initialize - @seed = seed = @random.rand(UInt16).to_u64 - @random.new_seed(seed) - end - @primary_formatter : Formatting::Formatter? @additional_formatters = [] of Formatting::Formatter @fail_fast = false @@ -29,6 +22,11 @@ module Spectator @profile = false @filters = [] of ExampleFilter + # Creates a configuration. + def build : Config + Config.new(self) + end + # Sets the primary formatter to use for reporting test progress and results. def formatter=(formatter : Formatting::Formatter) @primary_formatter = formatter @@ -105,7 +103,6 @@ module Spectator # Sets the seed for the random number generator. def seed=(seed) @seed = seed - @random = Random.new(seed) end # Randomizes test execution order. @@ -153,10 +150,5 @@ module Spectator CompositeExampleFilter.new(@filters) end end - - # Creates a configuration. - def build : Config - Config.new(self) - end end end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 54f8ed6..4c54b17 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -30,6 +30,17 @@ module Spectator @group_stack.push(root_group) end + # Constructs the test spec. + # Returns the spec instance. + # + # Raises an error if there were not symmetrical calls to `#start_group` and `#end_group`. + # This would indicate a logical error somewhere in Spectator or an extension of it. + def build : Spec + raise "Mismatched start and end groups" unless root? + + Spec.new(root_group, config) + end + # Defines a new example group and pushes it onto the group stack. # Examples and groups defined after calling this method will be nested under the new group. # The group will be finished and popped off the stack when `#end_example` is called. @@ -87,23 +98,21 @@ module Spectator # The example is added to the current group by `Example` initializer. end + # Builds the configuration to use for the spec. + # A `ConfigBuilder` is yielded to the block provided to this method. + # That builder will be used to create the configuration. + def config + builder = ConfigBuilder.new + yield builder + @config = builder.build + end + # Sets the configuration of the spec. # This configuration controls how examples run. def config=(config) @config = config end - # Constructs the test spec. - # Returns the spec instance. - # - # Raises an error if there were not symmetrical calls to `#start_group` and `#end_group`. - # This would indicate a logical error somewhere in Spectator or an extension of it. - def build : Spec - raise "Mismatched start and end groups" unless root? - - Spec.new(root_group, config) - end - # Checks if the current group is the root group. private def root? @group_stack.size == 1 @@ -122,8 +131,8 @@ module Spectator # Retrieves the configuration. # If one wasn't previously set, a default configuration is used. - private def config - @config || ConfigBuilder.new.build + private def config : Config + @config || ConfigBuilder.default end end end From 225e1a52badc212b241350825a919cd3d250a652 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Oct 2020 22:11:04 -0600 Subject: [PATCH 058/399] Clean up example randomization --- src/spectator/config.cr | 32 ++++++++++++++++++++++++++++++-- src/spectator/config_builder.cr | 11 +++-------- src/spectator/spec.cr | 12 ++---------- 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/spectator/config.cr b/src/spectator/config.cr index bc71d41..1d1a535 100644 --- a/src/spectator/config.cr +++ b/src/spectator/config.cr @@ -20,7 +20,7 @@ module Spectator getter? randomize : Bool # Seed used for random number generation. - getter! random_seed : UInt64? + getter random_seed : UInt64 # Indicates whether timing information should be displayed. getter? profile : Bool @@ -29,22 +29,50 @@ module Spectator getter example_filter : ExampleFilter # Creates a new configuration. + # Properties are pulled from *source*. + # Typically, *source* is a `ConfigBuilder`. def initialize(builder) @formatters = builder.formatters @fail_fast = builder.fail_fast? @fail_blank = builder.fail_blank? @dry_run = builder.dry_run? @randomize = builder.randomize? - @random_seed = builder.seed? + @random_seed = builder.seed @profile = builder.profile? @example_filter = builder.example_filter end + # Shuffles the items in an array using the configured random settings. + # If `#randomize?` is true, the *items* are shuffled and returned as a new array. + # Otherwise, the items are left alone and returned as-is. + # The array of *items* is never modified. + def shuffle(items) + return items unless randomize? + + items.shuffle(random) + end + + # Shuffles the items in an array using the configured random settings. + # If `#randomize?` is true, the *items* are shuffled and returned. + # Otherwise, the items are left alone and returned as-is. + # The array of *items* is modified, the items are shuffled in-place. + def shuffle!(items) + return items unless randomize? + + items.shuffle!(random) + end + # Yields each formatter that should be reported to. def each_formatter @formatters.each do |formatter| yield formatter end end + + # Retrieves the configured random number generator. + # This will produce the same generator with the same seed every time. + private def random + Random.new(random_seed) + end end end diff --git a/src/spectator/config_builder.cr b/src/spectator/config_builder.cr index 1db01dc..8a4b044 100644 --- a/src/spectator/config_builder.cr +++ b/src/spectator/config_builder.cr @@ -13,6 +13,9 @@ module Spectator new.build end + # Seed used for random number generation. + property seed : UInt64 = Random.rand(UInt64) + @primary_formatter : Formatting::Formatter? @additional_formatters = [] of Formatting::Formatter @fail_fast = false @@ -97,14 +100,6 @@ module Spectator @dry_run end - # Seed used for random number generation. - getter! seed : UInt64? - - # Sets the seed for the random number generator. - def seed=(seed) - @seed = seed - end - # Randomizes test execution order. def randomize self.randomize = true diff --git a/src/spectator/spec.cr b/src/spectator/spec.cr index a1dfa5c..8cfb235 100644 --- a/src/spectator/spec.cr +++ b/src/spectator/spec.cr @@ -16,16 +16,8 @@ module Spectator # Generates a list of examples to run. # The order of the examples are also sorted based on the configuration. private def examples - ExampleIterator.new(@root).to_a.tap do |examples| - if @config.randomize? - random = if (seed = @config.random_seed) - Random.new(seed) - else - Random.new - end - examples.shuffle!(random) - end - end + examples = ExampleIterator.new(@root).to_a + @config.shuffle!(examples) end private struct Runner From e4f3d334ed35888595d3d13dd820744cf4b11788 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Oct 2020 22:12:02 -0600 Subject: [PATCH 059/399] Consistent naming of random seed --- src/spectator/config.cr | 2 +- src/spectator/config_builder.cr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/config.cr b/src/spectator/config.cr index 1d1a535..a877108 100644 --- a/src/spectator/config.cr +++ b/src/spectator/config.cr @@ -37,7 +37,7 @@ module Spectator @fail_blank = builder.fail_blank? @dry_run = builder.dry_run? @randomize = builder.randomize? - @random_seed = builder.seed + @random_seed = builder.random_seed @profile = builder.profile? @example_filter = builder.example_filter end diff --git a/src/spectator/config_builder.cr b/src/spectator/config_builder.cr index 8a4b044..3fb10cb 100644 --- a/src/spectator/config_builder.cr +++ b/src/spectator/config_builder.cr @@ -14,7 +14,7 @@ module Spectator end # Seed used for random number generation. - property seed : UInt64 = Random.rand(UInt64) + property random_seed : UInt64 = Random.rand(UInt64) @primary_formatter : Formatting::Formatter? @additional_formatters = [] of Formatting::Formatter From 87c8914187eb6b2e360fd2b52ed812790059aada Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Oct 2020 22:12:47 -0600 Subject: [PATCH 060/399] Typo --- src/spectator/config.cr | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/spectator/config.cr b/src/spectator/config.cr index a877108..025697d 100644 --- a/src/spectator/config.cr +++ b/src/spectator/config.cr @@ -31,15 +31,15 @@ module Spectator # Creates a new configuration. # Properties are pulled from *source*. # Typically, *source* is a `ConfigBuilder`. - def initialize(builder) - @formatters = builder.formatters - @fail_fast = builder.fail_fast? - @fail_blank = builder.fail_blank? - @dry_run = builder.dry_run? - @randomize = builder.randomize? - @random_seed = builder.random_seed - @profile = builder.profile? - @example_filter = builder.example_filter + def initialize(source) + @formatters = source.formatters + @fail_fast = source.fail_fast? + @fail_blank = source.fail_blank? + @dry_run = source.dry_run? + @randomize = source.randomize? + @random_seed = source.random_seed + @profile = source.profile? + @example_filter = source.example_filter end # Shuffles the items in an array using the configured random settings. From c36e006c85875ad8d73a8d0c9eb9679b41eaa9cc Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Oct 2020 22:57:27 -0600 Subject: [PATCH 061/399] Move top-level DSL to its own module --- src/spectator.cr | 28 +--------------------------- src/spectator/dsl.cr | 1 + src/spectator/dsl/top.cr | 29 +++++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 27 deletions(-) create mode 100644 src/spectator/dsl/top.cr diff --git a/src/spectator.cr b/src/spectator.cr index 5391a78..d45fd13 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -7,6 +7,7 @@ Log.setup_from_env # Module that contains all functionality related to Spectator. module Spectator extend self + include DSL::Top # Current version of the Spectator library. VERSION = {{ `shards version #{__DIR__}`.stringify.chomp }} @@ -14,33 +15,6 @@ module Spectator # Logger for Spectator internals. Log = ::Log.for(self) - # Top-level describe method. - # All specs in a file must be wrapped in this call. - # This takes an argument and a block. - # The argument is what your spec is describing. - # It can be any Crystal expression, - # but is typically a class name or feature string. - # The block should contain all of the specs for what is being described. - # Example: - # ``` - # Spectator.describe Foo do - # # Your specs for `Foo` go here. - # end - # ``` - # NOTE: Inside the block, the `Spectator` prefix is no longer needed. - # Actually, prefixing methods and macros with `Spectator` - # most likely won't work and can cause compiler errors. - macro describe(description, &block) - class ::SpectatorTestContext - example_group({{description}}) {{block}} - end - end - - # :ditto: - macro context(description, &block) - describe({{description}}) {{block}} - end - # Flag indicating whether Spectator should automatically run tests. # This should be left alone (set to true) in typical usage. # There are times when Spectator shouldn't run tests. diff --git a/src/spectator/dsl.cr b/src/spectator/dsl.cr index f4cd763..1e46330 100644 --- a/src/spectator/dsl.cr +++ b/src/spectator/dsl.cr @@ -2,6 +2,7 @@ require "./dsl/builder" require "./dsl/examples" require "./dsl/groups" +require "./dsl/top" module Spectator # Namespace containing methods representing the spec domain specific language. diff --git a/src/spectator/dsl/top.cr b/src/spectator/dsl/top.cr new file mode 100644 index 0000000..d6682b6 --- /dev/null +++ b/src/spectator/dsl/top.cr @@ -0,0 +1,29 @@ +require "./groups" + +module Spectator::DSL + module Top + {% for method in %i[example_group describe context] %} + # Top-level describe method. + # All specs in a file must be wrapped in this call. + # This takes an argument and a block. + # The argument is what your spec is describing. + # It can be any Crystal expression, + # but is typically a class name or feature string. + # The block should contain all of the examples for what is being described. + # Example: + # ``` + # Spectator.describe Foo do + # # Your examples for `Foo` go here. + # end + # ``` + # NOTE: Inside the block, the `Spectator` prefix is no longer needed. + # Actually, prefixing methods and macros with `Spectator` + # most likely won't work and can cause compiler errors. + macro {{method.id}}(description, &block) + class ::SpectatorTestContext + {{method.id}}(\{{description}}) \{{block}} + end + end + {% end %} + end +end From a0e6d5c4e8a30b5b72ad3dc16b71ba0891acc03f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 19 Oct 2020 20:19:25 -0600 Subject: [PATCH 062/399] Re-introduce config options --- src/spectator.cr | 6 ++-- .../command_line_arguments_config_source.cr | 34 +++++++++++++++++-- src/spectator/config_source.cr | 4 ++- src/spectator/includes.cr | 3 +- 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/spectator.cr b/src/spectator.cr index d45fd13..4f29bc8 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -66,7 +66,7 @@ module Spectator # Builds the tests and runs the framework. private def run # Build the spec and run it. - # DSL::Builder.config = config # TODO: Set config. + DSL::Builder.config = config spec = DSL::Builder.build spec.run true @@ -109,11 +109,11 @@ module Spectator private def apply_config_file(file_path = CONFIG_FILE_PATH) : Nil return unless File.exists?(file_path) args = File.read(file_path).lines - CommandLineArgumentsConfigSource.new(args).apply_to(@@config_builder) + CommandLineArgumentsConfigSource.new(args).apply(@@config_builder) end # Applies configuration options from the command-line arguments private def apply_command_line_args : Nil - CommandLineArgumentsConfigSource.new.apply_to(@@config_builder) + CommandLineArgumentsConfigSource.new.apply(@@config_builder) end end diff --git a/src/spectator/command_line_arguments_config_source.cr b/src/spectator/command_line_arguments_config_source.cr index 1631c49..07ea144 100644 --- a/src/spectator/command_line_arguments_config_source.cr +++ b/src/spectator/command_line_arguments_config_source.cr @@ -1,9 +1,17 @@ require "option_parser" require "./config_source" +require "./formatting" +require "./line_example_filter" +require "./name_example_filter" +require "./source" +require "./source_example_filter" module Spectator # Generates configuration from the command-line arguments. class CommandLineArgumentsConfigSource < ConfigSource + # Logger for this class. + Log = Spectator::Log.for("config") + # Creates the configuration source. # By default, the command-line arguments (ARGV) are used. # But custom arguments can be passed in. @@ -12,7 +20,7 @@ module Spectator # Applies the specified configuration to a builder. # Calling this method from multiple sources builds up the final configuration. - def apply_to(builder : ConfigBuilder) : Nil + def apply(builder : ConfigBuilder) : Nil OptionParser.parse(@args) do |parser| control_parser_options(parser, builder) filter_parser_options(parser, builder) @@ -33,6 +41,7 @@ module Spectator # Adds the fail-fast option to the parser. private def fail_fast_option(parser, builder) parser.on("-f", "--fail-fast", "Stop testing on first failure") do + Log.debug { "Enabling fail-fast (-f)" } builder.fail_fast end end @@ -40,6 +49,7 @@ module Spectator # Adds the fail-blank option to the parser. private def fail_blank_option(parser, builder) parser.on("-b", "--fail-blank", "Fail if there are no examples") do + Log.debug { "Enabling fail-blank (-b)" } builder.fail_blank end end @@ -47,6 +57,7 @@ module Spectator # Adds the dry-run option to the parser. private def dry_run_option(parser, builder) parser.on("-d", "--dry-run", "Don't run any tests, output what would have run") do + Log.debug { "Enabling dry-run (-d)" } builder.dry_run end end @@ -54,6 +65,7 @@ module Spectator # Adds the randomize examples option to the parser. private def random_option(parser, builder) parser.on("-r", "--rand", "Randomize the execution order of tests") do + Log.debug { "Randomizing test order (-r)" } builder.randomize end end @@ -61,8 +73,9 @@ module Spectator # Adds the random seed option to the parser. private def seed_option(parser, builder) parser.on("--seed INTEGER", "Set the seed for the random number generator (implies -r)") do |seed| + Log.debug { "Randomizing test order and setting RNG seed to #{seed}" } builder.randomize - builder.seed = seed.to_u64 + builder.random_seed = seed.to_u64 end end @@ -71,11 +84,17 @@ module Spectator parser.on("--order ORDER", "Set the test execution order. ORDER should be one of: defined, rand, or rand:SEED") do |method| case method.downcase when "defined" + Log.debug { "Disabling randomized tests (--order defined)" } builder.randomize = false when /^rand/ builder.randomize parts = method.split(':', 2) - builder.seed = parts[1].to_u64 if parts.size > 1 + if (seed = parts[1]?) + Log.debug { "Randomizing test order and setting RNG seed to #{seed} (--order rand:#{seed})" } + builder.random_seed = seed.to_u64 + else + Log.debug { "Randomizing test order (--order rand)" } + end else nil end @@ -92,6 +111,7 @@ module Spectator # Adds the example filter option to the parser. private def example_option(parser, builder) parser.on("-e", "--example STRING", "Run examples whose full nested names include STRING") do |pattern| + Log.debug { "Filtering for examples named '#{pattern}' (-e '#{pattern}')" } filter = NameExampleFilter.new(pattern) builder.add_example_filter(filter) end @@ -100,6 +120,7 @@ module Spectator # Adds the line filter option to the parser. private def line_option(parser, builder) parser.on("-l", "--line LINE", "Run examples whose line matches LINE") do |line| + Log.debug { "Filtering for examples on line #{line} (-l #{line})" } filter = LineExampleFilter.new(line.to_i) builder.add_example_filter(filter) end @@ -108,6 +129,7 @@ module Spectator # Adds the location filter option to the parser. private def location_option(parser, builder) parser.on("--location FILE:LINE", "Run the example at line 'LINE' in the file 'FILE', multiple allowed") do |location| + Log.debug { "Filtering for examples at #{location} (--location '#{location}')" } source = Source.parse(location) filter = SourceExampleFilter.new(source) builder.add_example_filter(filter) @@ -128,6 +150,7 @@ module Spectator # Adds the verbose output option to the parser. private def verbose_option(parser, builder) parser.on("-v", "--verbose", "Verbose output using document formatter") do + Log.debug { "Setting output format to document (-v)" } builder.formatter = Formatting::DocumentFormatter.new end end @@ -143,6 +166,7 @@ module Spectator # Adds the profile output option to the parser. private def profile_option(parser, builder) parser.on("-p", "--profile", "Display the 10 slowest specs") do + Log.debug { "Enabling timing information (-p)" } builder.profile end end @@ -150,6 +174,7 @@ module Spectator # Adds the JSON output option to the parser. private def json_option(parser, builder) parser.on("--json", "Generate JSON output") do + Log.debug { "Setting output format to JSON (--json)" } builder.formatter = Formatting::JsonFormatter.new end end @@ -157,6 +182,7 @@ module Spectator # Adds the TAP output option to the parser. private def tap_option(parser, builder) parser.on("--tap", "Generate TAP output (Test Anything Protocol)") do + Log.debug { "Setting output format to TAP (--tap)" } builder.formatter = Formatting::TAPFormatter.new end end @@ -164,6 +190,7 @@ module Spectator # Adds the JUnit output option to the parser. private def junit_option(parser, builder) parser.on("--junit_output OUTPUT_DIR", "Generate JUnit XML output") do |output_dir| + Log.debug { "Setting output format to JUnit XML (--junit_output '#{output_dir}')" } formatter = Formatting::JUnitFormatter.new(output_dir) builder.add_formatter(formatter) end @@ -172,6 +199,7 @@ module Spectator # Adds the "no color" output option to the parser. private def no_color_option(parser, builder) parser.on("--no-color", "Disable colored output") do + Log.debug { "Disabling color output (--no-color)" } Colorize.enabled = false end end diff --git a/src/spectator/config_source.cr b/src/spectator/config_source.cr index 40a243f..472ea8a 100644 --- a/src/spectator/config_source.cr +++ b/src/spectator/config_source.cr @@ -1,8 +1,10 @@ +require "./config_builder" + module Spectator # Interface for all places that configuration can originate. abstract class ConfigSource # Applies the specified configuration to a builder. # Calling this method from multiple sources builds up the final configuration. - abstract def apply_to(builder : ConfigBuilder) : Nil + abstract def apply(builder : ConfigBuilder) : Nil end end diff --git a/src/spectator/includes.cr b/src/spectator/includes.cr index 200b7a0..9b270a8 100644 --- a/src/spectator/includes.cr +++ b/src/spectator/includes.cr @@ -4,6 +4,7 @@ # Including all files with a wildcard would accidentally enable should-syntax. # Unfortunately, that leads to the existence of this file to include everything but that file. -require "./config" +require "./command_line_arguments_config_source" require "./config_builder" +require "./config" require "./dsl" From e8afe5070bbc0da062ea93c5f8cb05d8363c3b15 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 19 Oct 2020 20:34:18 -0600 Subject: [PATCH 063/399] Address Ameba issues --- src/spectator/dsl/groups.cr | 2 +- src/spectator/example_node.cr | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 15deaac..4a4c0f7 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -38,7 +38,7 @@ module Spectator::DSL what.is_a?(Path) || what.is_a?(TypeNode) || what.is_a?(Union)) && - (described_type = what.resolve?).is_a?(TypeNode) %} + what.resolve?.is_a?(TypeNode) %} {{what.symbolize}} {% elsif what.is_a?(StringLiteral) || what.is_a?(StringInterpolation) || diff --git a/src/spectator/example_node.cr b/src/spectator/example_node.cr index 42f970a..4444ddc 100644 --- a/src/spectator/example_node.cr +++ b/src/spectator/example_node.cr @@ -54,7 +54,8 @@ module Spectator # Add padding between the node names # only if the names don't appear to be symbolic. - io << ' ' unless !group.name? || # Skip blank group names (like the root group). + # Skip blank group names (like the root group). + io << ' ' unless !group.name? || # ameba:disable Style/NegatedConditionsInUnless (group.name?.is_a?(Symbol) && name.is_a?(String) && (name.starts_with?('#') || name.starts_with?('.'))) end From 347e1a84e5ecc8d25133c40e28d5e14873bfc52e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 7 Nov 2020 13:47:31 -0700 Subject: [PATCH 064/399] Dedicated example runner type --- src/spectator/example.cr | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index bcfd96e..4b8c7e7 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -30,12 +30,10 @@ module Spectator # Returns the result of the execution. # The result will also be stored in `#result`. def run : Result - Log.debug { "Running example #{self}" } - elapsed = Time.measure do - @delegate.call(self) - end + runner = Runner.new(self, @delegate) + @result = runner.run + ensure @finished = true - @result = PassResult.new(elapsed) end # Exposes information about the example useful for debugging. @@ -53,5 +51,23 @@ module Spectator io << result end + + # Responsible for executing example code and reporting results. + private struct Runner + # Creates the runner. + # *example* is the example being tested. + # The *delegate* is the entrypoint of the example's test code. + def initialize(@example : Example, @delegate : ExampleContextDelegate) + end + + # Executes the example's test code and produces a result. + def run : Result + Log.debug { "Running example #{@example}" } + elapsed = Time.measure do + @delegate.call(@example) + end + PassResult.new(elapsed) + end + end end end From 40dd85eb384f43f8be537df8ff4204df1bc491a5 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 7 Nov 2020 13:47:39 -0700 Subject: [PATCH 065/399] Bit of naming cleanup --- src/spectator/example.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 4b8c7e7..6cc630c 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -44,9 +44,9 @@ module Spectator io << '"' # Add source if it's available. - if (s = source) + if (sourse = self.source) io << " @ " - io << s + io << source end io << result From 8ae6ef478b2883ec632a46fb3053e5d3a731d97c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 7 Nov 2020 14:43:59 -0700 Subject: [PATCH 066/399] Dynamic examples with null context --- src/spectator/example.cr | 11 +++++++++++ src/spectator/example_context_delegate.cr | 10 ++++++++++ src/spectator/example_context_method.cr | 3 +-- src/spectator/null_context.cr | 6 ++++++ 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 src/spectator/null_context.cr diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 6cc630c..2f435e6 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -26,6 +26,17 @@ module Spectator super(name, source, group) end + # Creates a dynamic example. + # A block provided to this method will be called as-if it were the test code for the example. + # The block will be given this example instance as an argument. + # The *name* describes the purpose of the example. + # It can be a `Symbol` to describe a type. + # The *source* tracks where the example exists in source code. + # The example will be assigned to *group* if it is provided. + def initialize(name : String? = nil, source : Source? = nil, group : ExampleGroup? = nil, &block : Example -> _) + @delegate = ExampleContextDelegate.null(&block) + end + # Executes the test case. # Returns the result of the execution. # The result will also be stored in `#result`. diff --git a/src/spectator/example_context_delegate.cr b/src/spectator/example_context_delegate.cr index bd5aa9c..cd8f6bb 100644 --- a/src/spectator/example_context_delegate.cr +++ b/src/spectator/example_context_delegate.cr @@ -1,5 +1,6 @@ require "./context" require "./example_context_method" +require "./null_context" module Spectator # Stores a test context and a method to call within it. @@ -11,6 +12,15 @@ module Spectator def initialize(@context : Context, @method : ExampleContextMethod) end + # Creates a delegate with a null context. + # The context will be ignored and the block will be executed in its original scope. + # The example instance will be provided as an argument to the block. + def self.null(&block : Example -> _) + context = NullContext.new + method = ExampleContextMethod.new { |example| block.call(example) } + new(context, method) + end + # Invokes a method in the test context. # The *example* is the current running example. def call(example : Example) diff --git a/src/spectator/example_context_method.cr b/src/spectator/example_context_method.cr index a3e3dfc..c14d254 100644 --- a/src/spectator/example_context_method.cr +++ b/src/spectator/example_context_method.cr @@ -4,8 +4,7 @@ module Spectator # Encapsulates a method in a test context. # This could be used to invoke a test case or hook method. # The context is passed as an argument. - # The proc should downcast the context instance to the desired type - # and call a method on that context. + # The proc should downcast the context instance to the desired type and call a method on that context. # The current example is also passed as an argument. alias ExampleContextMethod = Example, Context -> end diff --git a/src/spectator/null_context.cr b/src/spectator/null_context.cr new file mode 100644 index 0000000..3597203 --- /dev/null +++ b/src/spectator/null_context.cr @@ -0,0 +1,6 @@ +module Spectator + # Empty context used to construct examples that don't have contexts. + # This is useful for dynamically creating examples outside of a spec. + class NullContext < Context + end +end From f7fe17768550db8394dd73e2209d860623a66d8e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 7 Nov 2020 15:01:08 -0700 Subject: [PATCH 067/399] Minor adjustments --- src/spectator/example.cr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 2f435e6..56238e6 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -42,9 +42,8 @@ module Spectator # The result will also be stored in `#result`. def run : Result runner = Runner.new(self, @delegate) - @result = runner.run - ensure @finished = true + @result = runner.run end # Exposes information about the example useful for debugging. @@ -63,7 +62,8 @@ module Spectator io << result end - # Responsible for executing example code and reporting results. + # Logic dedicated to running an example and necessary hooks. + # This type does not directly modify or mutate state in the `Example` class. private struct Runner # Creates the runner. # *example* is the example being tested. From dba2e23750ae501b3326cfe9422bacbd76b591ac Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 7 Nov 2020 15:24:22 -0700 Subject: [PATCH 068/399] Typo --- src/spectator/example.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 56238e6..67fdfff 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -54,7 +54,7 @@ module Spectator io << '"' # Add source if it's available. - if (sourse = self.source) + if (source = self.source) io << " @ " io << source end From 4230ec70a023d372ccd1435242c1fd4a13c7d7fc Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 7 Nov 2020 20:56:30 -0700 Subject: [PATCH 069/399] Move test handling code to Harness --- src/spectator/example.cr | 32 ++++------- src/spectator/harness.cr | 116 ++++++++++++++++++++++----------------- 2 files changed, 76 insertions(+), 72 deletions(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 67fdfff..b8c1207 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -1,7 +1,7 @@ require "./example_context_delegate" require "./example_group" require "./example_node" -require "./pass_result" +require "./harness" require "./pending_result" require "./result" require "./source" @@ -9,6 +9,9 @@ require "./source" module Spectator # Standard example that runs a test case. class Example < ExampleNode + # Currently running example. + class_getter! current : Example + # Indicates whether the example already ran. getter? finished : Bool = false @@ -41,9 +44,13 @@ module Spectator # Returns the result of the execution. # The result will also be stored in `#result`. def run : Result - runner = Runner.new(self, @delegate) + @@current = self + Log.debug { "Running example #{self}" } + Log.warn { "Example #{self} running more than once" } if @finished + @result = Harness.run { @delegate.call(self) } + ensure + @@current = nil @finished = true - @result = runner.run end # Exposes information about the example useful for debugging. @@ -61,24 +68,5 @@ module Spectator io << result end - - # Logic dedicated to running an example and necessary hooks. - # This type does not directly modify or mutate state in the `Example` class. - private struct Runner - # Creates the runner. - # *example* is the example being tested. - # The *delegate* is the entrypoint of the example's test code. - def initialize(@example : Example, @delegate : ExampleContextDelegate) - end - - # Executes the example's test code and produces a result. - def run : Result - Log.debug { "Running example #{@example}" } - elapsed = Time.measure do - @delegate.call(@example) - end - PassResult.new(elapsed) - end - end end end diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 5268edc..37bf1eb 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -1,78 +1,94 @@ -require "./mocks/registry" +require "./error_result" +require "./pass_result" +require "./result" module Spectator - # Helper class that acts as a gateway between example code and the test framework. - # Every example must be invoked by passing it to `#run`. - # This sets up the harness so that the example code can use it. - # The test framework does the following: + # Helper class that acts as a gateway between test code and the framework. + # This is essentially an "example runner." + # + # Test code should be wrapped with a call to `.run`. + # This class will catch all errors raised by the test code. + # Errors caused by failed assertions (`AssertionFailed`) are translated to failed results (`FailResult`). + # Errors not caused by assertions are translated to error results (`ErrorResult`). + # + # Every runnable example should invoke the test code by calling `.run`. + # This sets up the harness so that the test code can use it. + # The framework does the following: # ``` - # result = Harness.run(example) + # result = Harness.run { delegate.call(example) } # # Do something with the result. # ``` - # Then from the example code, the harness can be accessed via `#current` like so: + # Then from the test code, the harness can be accessed via `.current` like so: # ``` # harness = ::Spectator::Harness.current # # Do something with the harness. # ``` # Of course, the end-user shouldn't see this or work directly with the harness. # Instead, methods the user calls can access it. - # For instance, an expectation reporting a result. - class Harness + # For instance, an assertion reporting a result. + private class Harness # Retrieves the harness for the current running example. class_getter! current : self - # Wraps an example with a harness and runs the example. - # The `#current` harness will be set - # prior to running the example, and reset after. - # The *example* argument will be the example to run. - # The result returned from `Example#run` will be returned. - def self.run(example : Example) : Result - @@current = new(example) - example.run - ensure - @@current = nil + # Wraps an example with a harness and runs test code. + # A block provided to this method is considered to be the test code. + # The value of `.current` is set to the harness for the duration of the test. + # It will be reset after the test regardless of the outcome. + # The result of running the test code will be returned. + def self.run : Result + harness = new + previous = @@current + @@current = harness + result = harness.run { yield } + @@current = previous + result end - # Retrieves the current running example. - getter example : Example + @deferred = Deque(->).new - getter mocks : Mocks::Registry - - # Retrieves the group for the current running example. - def group - example.group + # Runs test code and produces a result based on the outcome. + # The test code should be called from within the block given to this method. + def run : Result + outcome = capture { yield } + run_deferred # TODO: Handle errors in deferred blocks. + translate(*outcome) end - # Reports the outcome of an expectation. - # An exception will be raised when a failing result is given. - def report_expectation(expectation : Expectations::Expectation) : Nil - @example.description = expectation.description unless @example.test_wrapper.description? - @reporter.report(expectation) - end - - # Generates the reported expectations from the example. - # This should be run after the example has finished. - def expectations : Expectations::ExampleExpectations - @reporter.expectations - end - - # Marks a block of code to run later. - def defer(&block : ->) : Nil + # Stores a block of code to be executed later. + # All deferred blocks run just before the `#run` method completes. + def defer(&block) : Nil @deferred << block end + # Yields to run the test code and returns information about the outcome. + # Returns a tuple with the elapsed time and an error if one occurred (otherwise nil). + private def capture + error = nil.as(Exception?) + elapsed = Time.measure do + begin + yield + rescue e + error = e + end + end + {elapsed, error} + end + + # Translates the outcome of running a test to a result. + # Takes the *elapsed* time and a possible *error* from the test. + # Returns a type of `Result`. + private def translate(elapsed, error) : Result + if error + ErrorResult.new(elapsed, error) + else + PassResult.new(elapsed) + end + end + # Runs all deferred blocks. - def run_deferred : Nil + private def run_deferred : Nil @deferred.each(&.call) @deferred.clear end - - # Creates a new harness. - # The example the harness is for should be passed in. - private def initialize(@example) - @reporter = Expectations::ExpectationReporter.new - @mocks = Mocks::Registry.new(@example.group.context) - @deferred = Deque(->).new - end end end From 1f319a70ce82a15e656b004e914d2e297ee976b2 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 7 Nov 2020 22:04:21 -0700 Subject: [PATCH 070/399] Add docs --- src/spectator/dsl/examples.cr | 43 ++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index e245f79..7b246bc 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -2,12 +2,17 @@ require "../source" require "./builder" module Spectator::DSL + # DSL methods for defining examples and test code. module Examples + # Defines a macro to generate code for an example. + # The *name* is the name given to the macro. + # TODO: Mark example as pending if block is omitted. macro define_example(name) macro {{name.id}}(what = nil, &block) \{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %} + \{% raise "A description or block must be provided. Cannot use '{{name.id}}' alone." unless what || block %} - def \%test + def \%test # TODO: Pass example instance. \{{block.body}} end @@ -33,10 +38,46 @@ module Spectator::DSL {% end %} end + # Defines an example. + # + # If a block is given, it is treated as the code to test. + # The block is provided the current example instance as an argument. + # + # The first argument names the example (test). + # Typically, this specifies what is being tested. + # It has no effect on the test and is purely used for output. + # If omitted, a name is generated from the first assertion in the test. + # + # The example will be marked as pending if the block is omitted. + # A block or name must be provided. define_example :example + # Defines an example. + # + # If a block is given, it is treated as the code to test. + # The block is provided the current example instance as an argument. + # + # The first argument names the example (test). + # Typically, this specifies what is being tested. + # It has no effect on the test and is purely used for output. + # If omitted, a name is generated from the first assertion in the test. + # + # The example will be marked as pending if the block is omitted. + # A block or name must be provided. define_example :it + # Defines an example. + # + # If a block is given, it is treated as the code to test. + # The block is provided the current example instance as an argument. + # + # The first argument names the example (test). + # Typically, this specifies what is being tested. + # It has no effect on the test and is purely used for output. + # If omitted, a name is generated from the first assertion in the test. + # + # The example will be marked as pending if the block is omitted. + # A block or name must be provided. define_example :specify end From 688c08b087098b7861959bf03128a85cb51ee280 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 7 Nov 2020 22:04:37 -0700 Subject: [PATCH 071/399] Initial work on hooks rework --- src/spectator/dsl/hooks.cr | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/spectator/dsl/hooks.cr b/src/spectator/dsl/hooks.cr index bd65db3..c6c6154 100644 --- a/src/spectator/dsl/hooks.cr +++ b/src/spectator/dsl/hooks.cr @@ -1,5 +1,39 @@ -module Spectator - module DSL +module Spectator::DSL + # DSL methods for adding custom logic to key times of the spec execution. + module Hooks + # Defines code to run before any and all examples in an example group. + macro before_all(&block) + {% raise "Cannot use 'before_all' inside of a test block" if @def %} + + def self.%hook : Nil + {{block.body}} + end + + ::Spectator::DSL::Builder.add_hook( + :before + ) { {{@type.name}.%hook } + end + + macro before_each(&block) + {% raise "Cannot use 'before_each' inside of a test block" if @def %} + + def %hook : Nil + {{block.body}} + end + + ::Spectator::DSL::Builder.add_context_hook( + :before, + {{@type.name}} + ) { |context| context.as({{@type.name}).%hook } + end + + macro after_all(&block) + end + + macro after_each(&block) + end + end + macro before_each(&block) def %hook({{block.args.splat}}) : Nil {{block.body}} From f433405ece3c97c5960a6a33fde431238b151b72 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Nov 2020 15:06:49 -0700 Subject: [PATCH 072/399] Scratch work Trying to implement hooks. Ran into a problem with contexts. --- src/spectator/dsl/builder.cr | 28 ++++++++++ src/spectator/dsl/hooks.cr | 9 +--- src/spectator/events.cr | 62 +++++++++++++++++++++++ src/spectator/example.cr | 26 ++++++++-- src/spectator/example_context_delegate.cr | 3 ++ src/spectator/example_group.cr | 7 +++ src/spectator/spec_builder.cr | 40 +++++++++++++++ 7 files changed, 163 insertions(+), 12 deletions(-) create mode 100644 src/spectator/events.cr diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr index dc4647c..2b791da 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -35,6 +35,34 @@ module Spectator::DSL @@builder.add_example(*args, &block) end + # Defines a block of code to execute before any and all examples in the current group. + # + # See `Spec::Builder#before_all` for usage details. + def before_all(&block) + @@builder.before_all(&block) + end + + # Defines a block of code to execute before every example in the current group + # + # See `Spec::Builder#before_each` for usage details. + def before_each(&block : Example, Context -> _) + @@builder.before_each(&block) + end + + # Defines a block of code to execute after any and all examples in the current group. + # + # See `Spec::Builder#after_all` for usage details. + def after_all(&block) + @@builder.after_all(&block) + end + + # Defines a block of code to execute after every example in the current group. + # + # See `Spec::Builder#after_each` for usage details. + def after_each(&block : Example, Context ->) + @@builder.after_each(&block) + end + # Sets the configuration of the spec. # # See `Spec::Builder#config=` for usage details. diff --git a/src/spectator/dsl/hooks.cr b/src/spectator/dsl/hooks.cr index c6c6154..00eb251 100644 --- a/src/spectator/dsl/hooks.cr +++ b/src/spectator/dsl/hooks.cr @@ -9,9 +9,7 @@ module Spectator::DSL {{block.body}} end - ::Spectator::DSL::Builder.add_hook( - :before - ) { {{@type.name}.%hook } + ::Spectator::DSL::Builder.before_all { {{@type.name}.%hook } end macro before_each(&block) @@ -21,10 +19,7 @@ module Spectator::DSL {{block.body}} end - ::Spectator::DSL::Builder.add_context_hook( - :before, - {{@type.name}} - ) { |context| context.as({{@type.name}).%hook } + ::Spectator::DSL::Builder.before_each { |context| context.as({{@type.name}).%hook } end macro after_all(&block) diff --git a/src/spectator/events.cr b/src/spectator/events.cr new file mode 100644 index 0000000..ae8bd60 --- /dev/null +++ b/src/spectator/events.cr @@ -0,0 +1,62 @@ +require "./example_context_delegate" + +module Spectator + # Mix-in for managing events and hooks. + # This module is intended to be included by `ExampleGroup`. + module Events + # Defines an event for an example group. + # This event typically runs before or after an example group finishes. + # No contextual information (or example) is provided to the hooks. + # The *name* defines the name of the event. + # This must be unique across all events. + # Two methods are defined - one to add a hook and the other to trigger the event which calls every hook. + private macro group_event(name) + @{{name.id}}_hooks = Deque(->).new + + # Defines a hook for the *{{name.id}}* event. + # The block of code given to this method is invoked when the event occurs. + def {{name.id}}(&block) : Nil + @{{name.id}}_hooks << block + end + + # Signals that the *{{name.id}}* event has occurred. + # All hooks associated with the event will be called. + def call_{{name.id}} : Nil + @{{name.id}}_hooks.each(&.call) + end + end + + # Defines an event for an example. + # This event typically runs before or after an example finishes. + # The current example is provided to the hooks. + # The *name* defines the name of the event. + # This must be unique across all events. + # Three methods are defined - two to add a hook and the other to trigger the event which calls every hook. + # A hook can be added with an `ExampleContextDelegate` or a block that accepts an example (no context). + private macro example_event(name) + @{{name.id}}_hooks = Deque(ExampleContextMethod).new + + # Defines a hook for the *{{name.id}}* event. + # The *delegate* will be called when the event occurs. + # The current example is provided to the delegate. + def {{name.id}}(delegate : ExampleContextDelegate) : Nil + @{{name.id}}_hooks << delegate + end + + # Defines a hook for the *{{name.id}}* event. + # The block of code given to this method is invoked when the event occurs. + # The current example is provided as a block argument. + def {{name.id}}(&block : Example -> _) : Nil + delegate = ExampleContextDelegate.null(&block) + @{{name.id}}_hooks << delegate + end + + # Signals that the *{{name.id}}* event has occurred. + # All hooks associated with the event will be called. + # The *example* should be the current example. + def call_{{name.id}}(example) : Nil + @{{name.id}}_hooks.each(&.call(example)) + end + end + end +end diff --git a/src/spectator/example.cr b/src/spectator/example.cr index b8c1207..27e67d6 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -19,12 +19,13 @@ module Spectator getter result : Result = PendingResult.new # Creates the example. - # The *delegate* contains the test context and method that runs the test case. + # An instance to run the test code in is given by *context*. + # The *entrypoint* defines the test code (typically inside *context*). # The *name* describes the purpose of the example. # It can be a `Symbol` to describe a type. # The *source* tracks where the example exists in source code. # The example will be assigned to *group* if it is provided. - def initialize(@delegate : ExampleContextDelegate, + def initialize(@context : Context, @entrypoint : ExampleContextMethod, name : String? = nil, source : Source? = nil, group : ExampleGroup? = nil) super(name, source, group) end @@ -37,7 +38,8 @@ module Spectator # The *source* tracks where the example exists in source code. # The example will be assigned to *group* if it is provided. def initialize(name : String? = nil, source : Source? = nil, group : ExampleGroup? = nil, &block : Example -> _) - @delegate = ExampleContextDelegate.null(&block) + @context = NullContext.new + @entrypoint = block end # Executes the test case. @@ -46,13 +48,27 @@ module Spectator def run : Result @@current = self Log.debug { "Running example #{self}" } - Log.warn { "Example #{self} running more than once" } if @finished - @result = Harness.run { @delegate.call(self) } + Log.warn { "Example #{self} already ran" } if @finished + @result = Harness.run { @entrypoint.call(self, @context) } ensure @@current = nil @finished = true end + # Executes code within the example's test context. + # This is an advanced method intended for internal usage only. + # + # The *klass* defines the type of the test context. + # This is typically only known by the code constructing the example. + # An error will be raised if *klass* doesn't match the test context's type. + # The block given to this method will be executed within the test context. + # + # TODO: Benchmark compiler performance using this method versus client-side casting in a proc. + def with_context(klass) + context = klass.cast(@delegate.context) + with context yield + end + # Exposes information about the example useful for debugging. def inspect(io) # Full example name. diff --git a/src/spectator/example_context_delegate.cr b/src/spectator/example_context_delegate.cr index cd8f6bb..0b61f57 100644 --- a/src/spectator/example_context_delegate.cr +++ b/src/spectator/example_context_delegate.cr @@ -6,6 +6,9 @@ module Spectator # Stores a test context and a method to call within it. # This is a variant of `ContextDelegate` that accepts the current running example. struct ExampleContextDelegate + # Retrieves the underlying context. + protected getter context : Context + # Creates the delegate. # The *context* is the instance of the test context. # The *method* is proc that downcasts *context* and calls a method on it. diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index d6d5f84..cd5bb71 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -1,11 +1,18 @@ +require "./events" require "./example_node" module Spectator # Collection of examples and sub-groups. class ExampleGroup < ExampleNode include Enumerable(ExampleNode) + include Events include Iterable(ExampleNode) + group_event before_all + group_event after_all + example_event before_each + example_event after_each + @nodes = [] of ExampleNode # Removes the specified *node* from the group. diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 4c54b17..d6be9f2 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -98,6 +98,46 @@ module Spectator # The example is added to the current group by `Example` initializer. end + # Defines a block of code to execute before any and all examples in the current group. + def before_all(&block) + Log.trace { "Add before_all hook" } + current_group.before_all(&block) + end + + # Defines a delegate to call before every example in the current group. + # The current example is provided to the delegate. + def before_each(delegate : ExampleContextDelegate) + Log.trace { "Add before_each hook delegate" } + current_group.before_each(delegate) + end + + # Defines a block of code to execute before every example in the current group. + # The current example is provided as a block argument. + def before_each(&block : Example -> _) + Log.trace { "Add before_each hook block" } + current_group.before_each(&block) + end + + # Defines a block of code to execute after any and all examples in the current group. + def after_all(&block) + Log.trace { "Add after_all hook" } + current_group.after_all(&block) + end + + # Defines a delegate to call after every example in the current group. + # The current example is provided to the delegate. + def after_each(delegate : ExampleContextDelegate) + Log.trace { "Add after_each hook delegate" } + current_group.after_each(delegate) + end + + # Defines a block of code to execute after every example in the current group. + # The current example is provided as a block argument. + def after_each(&block : Example -> _) + Log.trace { "Add after_each hook" } + current_group.after_each(&block) + end + # Builds the configuration to use for the spec. # A `ConfigBuilder` is yielded to the block provided to this method. # That builder will be used to create the configuration. From b8dc83286c3e8eaa0875cd0d45e6f083eee3a2f2 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Nov 2020 16:52:08 -0700 Subject: [PATCH 073/399] Upcast context to reduce method instances generated by compiler --- src/spectator/dsl/examples.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 7b246bc..85c63b1 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -19,8 +19,8 @@ module Spectator::DSL ::Spectator::DSL::Builder.add_example( _spectator_example_name(\{{what}}), ::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}}), - \{{@type.name}}.new ) { |example, context| context.as(\{{@type.name}}).\%test } + \{{@type.name}}.new.as(::Spectator::Context) end end From 7d54884196693b804d2ba38fe6ce6cb2f2471948 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Nov 2020 16:53:54 -0700 Subject: [PATCH 074/399] Don't pass context, get/cast from example instance --- src/spectator/dsl/builder.cr | 6 +++--- src/spectator/dsl/examples.cr | 2 +- src/spectator/dsl/hooks.cr | 2 +- src/spectator/events.cr | 14 +++----------- src/spectator/example.cr | 8 ++++---- src/spectator/spec_builder.cr | 19 ++----------------- 6 files changed, 14 insertions(+), 37 deletions(-) diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr index 2b791da..722f2b7 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -31,7 +31,7 @@ module Spectator::DSL # The example is added to the group currently on the top of the stack. # # See `Spec::Builder#add_example` for usage details. - def add_example(*args, &block : Example, Context ->) + def add_example(*args, &block : Example ->) @@builder.add_example(*args, &block) end @@ -45,7 +45,7 @@ module Spectator::DSL # Defines a block of code to execute before every example in the current group # # See `Spec::Builder#before_each` for usage details. - def before_each(&block : Example, Context -> _) + def before_each(&block : Example -> _) @@builder.before_each(&block) end @@ -59,7 +59,7 @@ module Spectator::DSL # Defines a block of code to execute after every example in the current group. # # See `Spec::Builder#after_each` for usage details. - def after_each(&block : Example, Context ->) + def after_each(&block : Example ->) @@builder.after_each(&block) end diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 85c63b1..628f0fc 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -19,8 +19,8 @@ module Spectator::DSL ::Spectator::DSL::Builder.add_example( _spectator_example_name(\{{what}}), ::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}}), - ) { |example, context| context.as(\{{@type.name}}).\%test } \{{@type.name}}.new.as(::Spectator::Context) + ) { |example| example.with_context(\{{@type.name}}) { \%test } } end end diff --git a/src/spectator/dsl/hooks.cr b/src/spectator/dsl/hooks.cr index 00eb251..e495004 100644 --- a/src/spectator/dsl/hooks.cr +++ b/src/spectator/dsl/hooks.cr @@ -19,7 +19,7 @@ module Spectator::DSL {{block.body}} end - ::Spectator::DSL::Builder.before_each { |context| context.as({{@type.name}).%hook } + ::Spectator::DSL::Builder.before_each { |example| example.with_context({{@type.name}) { %hook } } end macro after_all(&block) diff --git a/src/spectator/events.cr b/src/spectator/events.cr index ae8bd60..7c1c760 100644 --- a/src/spectator/events.cr +++ b/src/spectator/events.cr @@ -34,21 +34,13 @@ module Spectator # Three methods are defined - two to add a hook and the other to trigger the event which calls every hook. # A hook can be added with an `ExampleContextDelegate` or a block that accepts an example (no context). private macro example_event(name) - @{{name.id}}_hooks = Deque(ExampleContextMethod).new - - # Defines a hook for the *{{name.id}}* event. - # The *delegate* will be called when the event occurs. - # The current example is provided to the delegate. - def {{name.id}}(delegate : ExampleContextDelegate) : Nil - @{{name.id}}_hooks << delegate - end + @{{name.id}}_hooks = Deque(Example ->).new # Defines a hook for the *{{name.id}}* event. # The block of code given to this method is invoked when the event occurs. # The current example is provided as a block argument. - def {{name.id}}(&block : Example -> _) : Nil - delegate = ExampleContextDelegate.null(&block) - @{{name.id}}_hooks << delegate + def {{name.id}}(&block : Example ->) : Nil + @{{name.id}}_hooks << block end # Signals that the *{{name.id}}* event has occurred. diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 27e67d6..614fbbf 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -25,7 +25,7 @@ module Spectator # It can be a `Symbol` to describe a type. # The *source* tracks where the example exists in source code. # The example will be assigned to *group* if it is provided. - def initialize(@context : Context, @entrypoint : ExampleContextMethod, + def initialize(@context : Context, @entrypoint : self ->, name : String? = nil, source : Source? = nil, group : ExampleGroup? = nil) super(name, source, group) end @@ -37,7 +37,7 @@ module Spectator # It can be a `Symbol` to describe a type. # The *source* tracks where the example exists in source code. # The example will be assigned to *group* if it is provided. - def initialize(name : String? = nil, source : Source? = nil, group : ExampleGroup? = nil, &block : Example -> _) + def initialize(name : String? = nil, source : Source? = nil, group : ExampleGroup? = nil, &block : self ->) @context = NullContext.new @entrypoint = block end @@ -49,7 +49,7 @@ module Spectator @@current = self Log.debug { "Running example #{self}" } Log.warn { "Example #{self} already ran" } if @finished - @result = Harness.run { @entrypoint.call(self, @context) } + @result = Harness.run { @entrypoint.call(self) } ensure @@current = nil @finished = true @@ -65,7 +65,7 @@ module Spectator # # TODO: Benchmark compiler performance using this method versus client-side casting in a proc. def with_context(klass) - context = klass.cast(@delegate.context) + context = klass.cast(@context) with context yield end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index d6be9f2..387fbfd 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -91,10 +91,9 @@ module Spectator # It is expected that the test code runs when the block is called. # # The newly created example is returned. - def add_example(name, source, context, &block : Example, Context ->) : Example + def add_example(name, source, context, &block : Example -> _) : Example Log.trace { "Add example: #{name} @ #{source}" } - delegate = ExampleContextDelegate.new(context, block) - Example.new(delegate, name, source, current_group) + Example.new(context, block, name, source, current_group) # The example is added to the current group by `Example` initializer. end @@ -104,13 +103,6 @@ module Spectator current_group.before_all(&block) end - # Defines a delegate to call before every example in the current group. - # The current example is provided to the delegate. - def before_each(delegate : ExampleContextDelegate) - Log.trace { "Add before_each hook delegate" } - current_group.before_each(delegate) - end - # Defines a block of code to execute before every example in the current group. # The current example is provided as a block argument. def before_each(&block : Example -> _) @@ -124,13 +116,6 @@ module Spectator current_group.after_all(&block) end - # Defines a delegate to call after every example in the current group. - # The current example is provided to the delegate. - def after_each(delegate : ExampleContextDelegate) - Log.trace { "Add after_each hook delegate" } - current_group.after_each(delegate) - end - # Defines a block of code to execute after every example in the current group. # The current example is provided as a block argument. def after_each(&block : Example -> _) From b4e74444d173c516eaa195d98abf57272c3c26b0 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Nov 2020 22:21:52 -0700 Subject: [PATCH 075/399] Some work on hooks in DSL --- src/spectator/dsl.cr | 1 + src/spectator/dsl/hooks.cr | 85 +++-------------------------------- src/spectator_test_context.cr | 1 + 3 files changed, 8 insertions(+), 79 deletions(-) diff --git a/src/spectator/dsl.cr b/src/spectator/dsl.cr index 1e46330..e76bd7f 100644 --- a/src/spectator/dsl.cr +++ b/src/spectator/dsl.cr @@ -2,6 +2,7 @@ require "./dsl/builder" require "./dsl/examples" require "./dsl/groups" +require "./dsl/hooks" require "./dsl/top" module Spectator diff --git a/src/spectator/dsl/hooks.cr b/src/spectator/dsl/hooks.cr index e495004..88e513f 100644 --- a/src/spectator/dsl/hooks.cr +++ b/src/spectator/dsl/hooks.cr @@ -1,3 +1,5 @@ +require "./builder" + module Spectator::DSL # DSL methods for adding custom logic to key times of the spec execution. module Hooks @@ -9,7 +11,7 @@ module Spectator::DSL {{block.body}} end - ::Spectator::DSL::Builder.before_all { {{@type.name}.%hook } + ::Spectator::DSL::Builder.before_all { {{@type.name}}.%hook } end macro before_each(&block) @@ -19,7 +21,9 @@ module Spectator::DSL {{block.body}} end - ::Spectator::DSL::Builder.before_each { |example| example.with_context({{@type.name}) { %hook } } + ::Spectator::DSL::Builder.before_each do |example| + example.with_context({{@type.name}}) { %hook } + end end macro after_all(&block) @@ -28,81 +32,4 @@ module Spectator::DSL macro after_each(&block) end end - - macro before_each(&block) - def %hook({{block.args.splat}}) : Nil - {{block.body}} - end - - ::Spectator::SpecBuilder.add_before_each_hook do |test, example| - cast_test = test.as({{@type.id}}) - {% if block.args.empty? %} - cast_test.%hook - {% else %} - cast_test.%hook(example) - {% end %} - end - end - - macro after_each(&block) - def %hook({{block.args.splat}}) : Nil - {{block.body}} - end - - ::Spectator::SpecBuilder.add_after_each_hook do |test, example| - cast_test = test.as({{@type.id}}) - {% if block.args.empty? %} - cast_test.%hook - {% else %} - cast_test.%hook(example) - {% end %} - end - end - - macro before_all(&block) - ::Spectator::SpecBuilder.add_before_all_hook {{block}} - end - - macro after_all(&block) - ::Spectator::SpecBuilder.add_after_all_hook {{block}} - end - - macro around_each(&block) - def %hook({{block.args.first || :example.id}}) : Nil - {{block.body}} - end - - ::Spectator::SpecBuilder.add_around_each_hook { |test, proc| test.as({{@type.id}}).%hook(proc) } - end - - macro pre_condition(&block) - def %hook({{block.args.splat}}) : Nil - {{block.body}} - end - - ::Spectator::SpecBuilder.add_pre_condition do |test, example| - cast_test = test.as({{@type.id}}) - {% if block.args.empty? %} - cast_test.%hook - {% else %} - cast_test.%hook(example) - {% end %} - end - end - - macro post_condition(&block) - def %hook({{block.args.splat}}) : Nil - {{block.body}} - end - - ::Spectator::SpecBuilder.add_post_condition do |test, example| - cast_test = test.as({{@type.id}}) - {% if block.args.empty? %} - cast_test.%hook - {% else %} - cast_test.%hook(example) - {% end %} - end - end - end end diff --git a/src/spectator_test_context.cr b/src/spectator_test_context.cr index 2031005..0e8fb2b 100644 --- a/src/spectator_test_context.cr +++ b/src/spectator_test_context.cr @@ -4,6 +4,7 @@ require "./spectator/dsl" class SpectatorTestContext < SpectatorContext include ::Spectator::DSL::Examples include ::Spectator::DSL::Groups + include ::Spectator::DSL::Hooks # Initial implicit subject for tests. # This method should be overridden by example groups when an object is described. From 19d57dd82861e214831e95df76424e8619da7712 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 14 Nov 2020 17:02:24 -0700 Subject: [PATCH 076/399] Add call_once variant of events --- src/spectator/events.cr | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/spectator/events.cr b/src/spectator/events.cr index 7c1c760..847cc5c 100644 --- a/src/spectator/events.cr +++ b/src/spectator/events.cr @@ -9,9 +9,12 @@ module Spectator # No contextual information (or example) is provided to the hooks. # The *name* defines the name of the event. # This must be unique across all events. - # Two methods are defined - one to add a hook and the other to trigger the event which calls every hook. + # Three methods are defined - one to add a hook and the others to trigger the event which calls every hook. + # One trigger method, prefixed with *call_* will always call the event hooks. + # The other trigger method, prefixed with *call_once_* will only call the event hooks on the first invocation. private macro group_event(name) @{{name.id}}_hooks = Deque(->).new + @{{name.id}}_called = Atomic::Flag.new # Defines a hook for the *{{name.id}}* event. # The block of code given to this method is invoked when the event occurs. @@ -24,6 +27,15 @@ module Spectator def call_{{name.id}} : Nil @{{name.id}}_hooks.each(&.call) end + + # Signals that the *{{name.id}}* event has occurred. + # Only calls the hooks if the event hasn't been triggered before by this method. + # Returns true if the hooks were called and false if they weren't (called previously). + def call_once_{{name.id}} : Bool + first = @{{name.id}}_called.test_and_set + call_{{name.id}} if first + first + end end # Defines an event for an example. From 0279606a1cad15516ea2eabeb7bcca61d31a4564 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 15 Nov 2020 11:22:06 -0700 Subject: [PATCH 077/399] Placeholder code for after hooks --- src/spectator/dsl/hooks.cr | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/spectator/dsl/hooks.cr b/src/spectator/dsl/hooks.cr index 88e513f..fac734f 100644 --- a/src/spectator/dsl/hooks.cr +++ b/src/spectator/dsl/hooks.cr @@ -27,9 +27,25 @@ module Spectator::DSL end macro after_all(&block) + {% raise "Cannot use 'after_all' inside of a test block" if @def %} + + def self.%hook : Nil + {{block.body}} + end + + ::Spectator::DSL::Builder.after_all { {{@type.name}}.%hook } end macro after_each(&block) + {% raise "Cannot use 'after_each' inside of a test block" if @def %} + + def %hook : Nil + {{block.body}} + end + + ::Spectator::DSL::Builder.after_each do |example| + example.with_context({{@type.name}}) { %hook } + end end end end From 2f4cbd9c33ad68e99141e92413ef7a7162f3a6c9 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 15 Nov 2020 11:22:52 -0700 Subject: [PATCH 078/399] Specify contents of event trigger method with macro block Implement calling parent group hooks. --- src/spectator/events.cr | 12 ++++++---- src/spectator/example_group.cr | 43 ++++++++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/src/spectator/events.cr b/src/spectator/events.cr index 847cc5c..86a6132 100644 --- a/src/spectator/events.cr +++ b/src/spectator/events.cr @@ -12,7 +12,7 @@ module Spectator # Three methods are defined - one to add a hook and the others to trigger the event which calls every hook. # One trigger method, prefixed with *call_* will always call the event hooks. # The other trigger method, prefixed with *call_once_* will only call the event hooks on the first invocation. - private macro group_event(name) + private macro group_event(name, &block) @{{name.id}}_hooks = Deque(->).new @{{name.id}}_called = Atomic::Flag.new @@ -25,7 +25,8 @@ module Spectator # Signals that the *{{name.id}}* event has occurred. # All hooks associated with the event will be called. def call_{{name.id}} : Nil - @{{name.id}}_hooks.each(&.call) + {{block.args.first}} = @{{name.id}}_hooks + {{yield}} end # Signals that the *{{name.id}}* event has occurred. @@ -45,7 +46,7 @@ module Spectator # This must be unique across all events. # Three methods are defined - two to add a hook and the other to trigger the event which calls every hook. # A hook can be added with an `ExampleContextDelegate` or a block that accepts an example (no context). - private macro example_event(name) + private macro example_event(name, &block) @{{name.id}}_hooks = Deque(Example ->).new # Defines a hook for the *{{name.id}}* event. @@ -58,8 +59,9 @@ module Spectator # Signals that the *{{name.id}}* event has occurred. # All hooks associated with the event will be called. # The *example* should be the current example. - def call_{{name.id}}(example) : Nil - @{{name.id}}_hooks.each(&.call(example)) + def call_{{name.id}}({{block.args[1]}}) : Nil + {{block.args.first}} = @{{name.id}}_hooks + {{yield}} end end end diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index cd5bb71..0f64801 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -8,10 +8,45 @@ module Spectator include Events include Iterable(ExampleNode) - group_event before_all - group_event after_all - example_event before_each - example_event after_each + group_event before_all do |hooks| + Log.trace { "Processing before_all hooks" } + + if (parent = group?) + parent.call_once_before_all + end + + hooks.each(&.call) + end + + group_event after_all do |hooks| + Log.trace { "Processing after_all hooks" } + + hooks.each(&.call) + + if (parent = group?) + parent.call_once_after_all + end + end + + example_event before_each do |hooks, example| + Log.trace { "Processing before_each hooks" } + + if (parent = group?) + parent.call_before_each(example) + end + + hooks.each(&.call(example)) + end + + example_event after_each do |hooks, example| + Log.trace { "Processing after_each hooks" } + + hooks.each(&.call(example)) + + if (parent = group?) + parent.call_after_each(example) + end + end @nodes = [] of ExampleNode From 4533cffce7c7ed888fa1d1f4634f3fbc10a8028d Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 15 Nov 2020 11:25:07 -0700 Subject: [PATCH 079/399] Initial code to run hooks --- src/spectator/example.cr | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 614fbbf..ba100bf 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -49,7 +49,20 @@ module Spectator @@current = self Log.debug { "Running example #{self}" } Log.warn { "Example #{self} already ran" } if @finished - @result = Harness.run { @entrypoint.call(self) } + @result = Harness.run do + if (parent = group?) + parent.call_once_before_all + parent.call_before_each(self) + end + + @entrypoint.call(self) + @finished = true + + if (parent = group?) + parent.call_after_each(self) + parent.call_once_after_all if parent.finished? + end + end ensure @@current = nil @finished = true From b69751176159d70114681f27c6c88696b29b1dc0 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 31 Dec 2020 18:45:56 -0700 Subject: [PATCH 080/399] Slight doc improvement --- src/spectator/source.cr | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/spectator/source.cr b/src/spectator/source.cr index 628db29..b09d5df 100644 --- a/src/spectator/source.cr +++ b/src/spectator/source.cr @@ -14,6 +14,11 @@ module Spectator end # Parses a source from a string. + # The *string* should be in the form: + # ```text + # FILE:LINE + # ``` + # This matches the output of the `#to_s` method. def self.parse(string) # Make sure to handle multiple colons. # If this ran on Windows, there's a possibility of a colon in the path. From fbe9f22e0202f0448816289e3e2eabbe88d9e216 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Jan 2021 00:12:28 -0700 Subject: [PATCH 081/399] Start rework on capturing expressions --- src/spectator/abstract_expression.cr | 52 ++++++++++++++++++ src/spectator/expression.cr | 22 ++++++++ src/spectator/label.cr | 7 +++ src/spectator/test_block.cr | 48 ---------------- src/spectator/test_context.cr | 82 ---------------------------- src/spectator/test_expression.cr | 25 --------- src/spectator/test_value.cr | 29 ---------- src/spectator/test_wrapper.cr | 42 -------------- src/spectator/typed_value_wrapper.cr | 17 ------ src/spectator/value_wrapper.cr | 7 --- 10 files changed, 81 insertions(+), 250 deletions(-) create mode 100644 src/spectator/abstract_expression.cr create mode 100644 src/spectator/expression.cr create mode 100644 src/spectator/label.cr delete mode 100644 src/spectator/test_block.cr delete mode 100644 src/spectator/test_context.cr delete mode 100644 src/spectator/test_expression.cr delete mode 100644 src/spectator/test_value.cr delete mode 100644 src/spectator/test_wrapper.cr delete mode 100644 src/spectator/typed_value_wrapper.cr delete mode 100644 src/spectator/value_wrapper.cr diff --git a/src/spectator/abstract_expression.cr b/src/spectator/abstract_expression.cr new file mode 100644 index 0000000..2a58e06 --- /dev/null +++ b/src/spectator/abstract_expression.cr @@ -0,0 +1,52 @@ +require "./label" + +module Spectator + # Represents an expression from a test. + # This is typically captured by an `expect` macro. + # It consists of a label and the value of the expression. + # The label should be a string recognizable by the user, + # or nil if one isn't available. + # This base class is provided so that all generic sub-classes can be stored as this one type. + # The value of the expression can be retrieved by downcasting to the expected type with `#cast`. + abstract class AbstractExpression + # User recognizable string for the expression. + # This can be something like a variable name or a snippet of Crystal code. + getter label : Label + + # Creates the expression. + # The *label* is usually the Crystal code evaluating to the `#value`. + # It can be nil if it isn't available. + def initialize(@label : Label) + end + + # Retrieves the real value of the expression. + abstract def value + + # Attempts to cast `#value` to the type *T* and return it. + def cast(type : T.class) : T forall T + value.as(T) + end + + # Produces a string representation of the expression. + # This consists of the label (if one is available) and the value. + def to_s(io) + if (label = @label) + io << label + io << ':' + io << ' ' + end + io << value + end + + # Produces a detailed string representation of the expression. + # This consists of the label (if one is available) and the value. + def inspect(io) + if (label = @label) + io << @label + io << ':' + io << ' ' + end + value.inspect(io) + end + end +end diff --git a/src/spectator/expression.cr b/src/spectator/expression.cr new file mode 100644 index 0000000..b12efce --- /dev/null +++ b/src/spectator/expression.cr @@ -0,0 +1,22 @@ +require "./abstract_expression" +require "./label" + +module Spectator + # Represents an expression from a test. + # This is typically captured by an `expect` macro. + # It consists of a label and the value of the expression. + # The label should be a string recognizable by the user, + # or nil if one isn't available. + class Expression(T) < AbstractExpression + # Raw value of the expression. + getter value + + # Creates the expression. + # Expects the *value* of the expression and a *label* describing it. + # The *label* is usually the Crystal code evaluating to the *value*. + # It can be nil if it isn't available. + def initialize(@value : T, label : Label) + super(label) + end + end +end diff --git a/src/spectator/label.cr b/src/spectator/label.cr new file mode 100644 index 0000000..2a86b34 --- /dev/null +++ b/src/spectator/label.cr @@ -0,0 +1,7 @@ +module Spectator + # Identifier used in the spec. + # Signficant to the user. + # When a label is a symbol, then it is referencing a type or method. + # A label is nil when one can't be provided or captured. + alias Label = String | Symbol | Nil +end diff --git a/src/spectator/test_block.cr b/src/spectator/test_block.cr deleted file mode 100644 index a6af675..0000000 --- a/src/spectator/test_block.cr +++ /dev/null @@ -1,48 +0,0 @@ -require "./test_expression" - -module Spectator - # Captures an block from a test and its label. - struct TestBlock(ReturnType) < TestExpression(ReturnType) - # Calls the block and retrieves the value. - def value : ReturnType - @proc.call - end - - # Creates the block expression with a custom label. - # Typically the label is the code in the block/proc. - def initialize(@proc : -> ReturnType, label : String) - super(label) - end - - def self.create(proc : -> T, label : String) forall T - {% if T.id == "ReturnType".id %} - wrapper = ->{ proc.call; nil } - TestBlock(Nil).new(wrapper, label) - {% else %} - TestBlock(T).new(proc, label) - {% end %} - end - - # Creates the block expression with a generic label. - # This is used for the "should" syntax and when the label doesn't matter. - def initialize(@proc : -> ReturnType) - super("") - end - - def self.create(proc : -> T) forall T - {% if T.id == "ReturnType".id %} - wrapper = ->{ proc.call; nil } - TestBlock(Nil).new(wrapper) - {% else %} - TestBlock(T).new(proc) - {% end %} - end - - # Reports complete information about the expression. - def inspect(io) - io << label - io << " -> " - io << value - end - end -end diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr deleted file mode 100644 index bf2c612..0000000 --- a/src/spectator/test_context.cr +++ /dev/null @@ -1,82 +0,0 @@ -require "./example_hooks" -require "./test_values" - -module Spectator - class TestContext - getter! parent - - getter values - - getter stubs : Hash(String, Deque(Mocks::MethodStub)) - - def initialize(@parent : TestContext?, - @hooks : ExampleHooks, - @conditions : ExampleConditions, - @values : TestValues, - @stubs : Hash(String, Deque(Mocks::MethodStub))) - @before_all_hooks_run = false - @after_all_hooks_run = false - end - - def run_before_hooks(example : Example) - run_before_all_hooks - run_before_each_hooks(example) - end - - protected def run_before_all_hooks - return if @before_all_hooks_run - - @parent.try &.run_before_all_hooks - @hooks.run_before_all - ensure - @before_all_hooks_run = true - end - - protected def run_before_each_hooks(example : Example) - @parent.try &.run_before_each_hooks(example) - @hooks.run_before_each(example.test_wrapper, example) - end - - def run_after_hooks(example : Example) - run_after_each_hooks(example) - run_after_all_hooks(example.group) - end - - protected def run_after_all_hooks(group : ExampleGroup, *, ignore_unfinished = false) - return if @after_all_hooks_run - return unless ignore_unfinished || group.finished? - - @hooks.run_after_all - @parent.try do |parent_context| - parent_group = group.as(NestedExampleGroup).parent - parent_context.run_after_all_hooks(parent_group, ignore_unfinished: ignore_unfinished) - end - ensure - @after_all_hooks_run = true - end - - protected def run_after_each_hooks(example : Example) - @hooks.run_after_each(example.test_wrapper, example) - @parent.try &.run_after_each_hooks(example) - end - - def wrap_around_each_hooks(test, &block : ->) - wrapper = @hooks.wrap_around_each(test, block) - if (parent = @parent) - parent.wrap_around_each_hooks(test, &wrapper) - else - wrapper - end - end - - def run_pre_conditions(example) - @parent.try &.run_pre_conditions(example) - @conditions.run_pre_conditions(example.test_wrapper, example) - end - - def run_post_conditions(example) - @conditions.run_post_conditions(example.test_wrapper, example) - @parent.try &.run_post_conditions(example) - end - end -end diff --git a/src/spectator/test_expression.cr b/src/spectator/test_expression.cr deleted file mode 100644 index d5e3cdd..0000000 --- a/src/spectator/test_expression.cr +++ /dev/null @@ -1,25 +0,0 @@ -module Spectator - # Base type for capturing an expression from a test. - abstract struct TestExpression(T) - # User-friendly string displayed for the actual expression being tested. - # For instance, in the expectation: - # ``` - # expect(foo).to eq(bar) - # ``` - # This property will be "foo". - # It will be the literal string "foo", - # and not the actual value of the foo. - getter label : String - - # Creates the common base of the expression. - def initialize(@label) - end - - abstract def value : T - - # String representation of the expression. - def to_s(io) - io << label - end - end -end diff --git a/src/spectator/test_value.cr b/src/spectator/test_value.cr deleted file mode 100644 index b621562..0000000 --- a/src/spectator/test_value.cr +++ /dev/null @@ -1,29 +0,0 @@ -require "./test_expression" - -module Spectator - # Captures a value from a test and its label. - struct TestValue(T) < TestExpression(T) - # Actual value. - getter value : T - - # Creates the expression value with a custom label. - def initialize(@value : T, label : String) - super(label) - end - - # Creates the expression with a stringified value. - # This is used for the "should" syntax and when the label doesn't matter. - def initialize(@value : T) - super(@value.to_s) - end - - # Reports complete information about the expression. - def inspect(io) - io << label - io << '=' - io << @value - end - end - - alias LabeledValue = TestValue(String) -end diff --git a/src/spectator/test_wrapper.cr b/src/spectator/test_wrapper.cr deleted file mode 100644 index 79d5265..0000000 --- a/src/spectator/test_wrapper.cr +++ /dev/null @@ -1,42 +0,0 @@ -require "../spectator_test" -require "./source" - -module Spectator - alias TestMethod = ::SpectatorTest -> - - # Stores information about a end-user test. - # Used to instantiate tests and run them. - struct TestWrapper - # Description the user provided for the test. - def description - @description || @source.to_s - end - - # Location of the test in source code. - getter source - - # Creates a wrapper for the test. - def initialize(@description : String?, @source : Source, @test : ::SpectatorTest, @runner : TestMethod) - end - - def description? - !@description.nil? - end - - def run - call(@runner) - end - - def call(method : TestMethod) : Nil - method.call(@test) - end - - def call(method, *args) : Nil - method.call(@test, *args) - end - - def around_hook(context : TestContext) - context.wrap_around_each_hooks(@test) { run } - end - end -end diff --git a/src/spectator/typed_value_wrapper.cr b/src/spectator/typed_value_wrapper.cr deleted file mode 100644 index 8131d82..0000000 --- a/src/spectator/typed_value_wrapper.cr +++ /dev/null @@ -1,17 +0,0 @@ -require "./value_wrapper" - -module Spectator - # Implementation of a value wrapper for a specific type. - # Instances of this class should be created to wrap values. - # Then the wrapper should be stored as a `ValueWrapper` - # so that the type is deferred to runtime. - # This trick allows the DSL to store values without explicitly knowing their type. - class TypedValueWrapper(T) < ValueWrapper - # Wrapped value. - getter value : T - - # Creates a new wrapper for a value. - def initialize(@value : T) - end - end -end diff --git a/src/spectator/value_wrapper.cr b/src/spectator/value_wrapper.cr deleted file mode 100644 index 170951d..0000000 --- a/src/spectator/value_wrapper.cr +++ /dev/null @@ -1,7 +0,0 @@ -module Spectator - # Base class for proxying test values to examples. - # This abstraction is required for inferring types. - # The DSL makes heavy use of this to defer types. - abstract class ValueWrapper - end -end From 950f6b342475d848c3f02cbe83b897b39586ba3f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Jan 2021 00:13:22 -0700 Subject: [PATCH 082/399] Add block expression --- src/spectator/block.cr | 53 ++++++++++++++++++++++++++++++++++++++++ src/spectator/wrapper.cr | 22 +++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 src/spectator/block.cr create mode 100644 src/spectator/wrapper.cr diff --git a/src/spectator/block.cr b/src/spectator/block.cr new file mode 100644 index 0000000..15553dd --- /dev/null +++ b/src/spectator/block.cr @@ -0,0 +1,53 @@ +require "./abstract_expression" +require "./label" +require "./wrapper" + +module Spectator + # Represents a block from a test. + # This is typically captured by an `expect` macro. + # It consists of a label and parameterless block. + # The label should be a string recognizable by the user, + # or nil if one isn't available. + class Block(T) < AbstractExpression + # Cached value returned from the block. + # Is nil if the block hasn't been called. + @wrapper : Wrapper(T)? + + # Creates the block expression from a proc. + # The *proc* will be called to evaluate the value of the expression. + # The *label* is usually the Crystal code for the *proc*. + # It can be nil if it isn't available. + def initialize(@block : -> T, label : Label) + super(label) + end + + # Creates the block expression by capturing a block as a proc. + # The block will be called to evaluate the value of the expression. + # The *label* is usually the Crystal code for the *block*. + # It can be nil if it isn't available. + def initialize(label : Label, &@block : -> T) + super(label) + end + + # Retrieves the value of the block expression. + # This will be the return value of the block. + # The block is lazily evaluated and the value retrieved only once. + # Afterwards, the value is cached and returned by successive calls to this method. + def value + if (wrapper = @wrapper) + wrapper.value + else + call.tap do |value| + @wrapper = Wrapper.new(value) + end + end + end + + # Evaluates the block and returns the value from it. + # This method _does not_ cache the resulting value like `#value` does. + # Successive calls to this method may return different values. + def call : T + @block.call + end + end +end diff --git a/src/spectator/wrapper.cr b/src/spectator/wrapper.cr new file mode 100644 index 0000000..063cefa --- /dev/null +++ b/src/spectator/wrapper.cr @@ -0,0 +1,22 @@ +module Spectator + # Wrapper for a value. + # This is intended to be used as a union with nil. + # It allows storing (caching) a nillable value. + # ``` + # if (wrapper = @wrapper) + # wrapper.value + # else + # value = 42 + # @wrapper = Wrapper.new(value) + # value + # end + # ``` + struct Wrapper(T) + # Original value. + getter value : T + + # Creates the wrapper. + def initialize(@value : T) + end + end +end From 0ee708281f188c2fb748ba751c2195a5952467a6 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Jan 2021 10:27:54 -0700 Subject: [PATCH 083/399] Cleanup hook macros --- src/spectator/events.cr | 59 ++++++++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/src/spectator/events.cr b/src/spectator/events.cr index 86a6132..33857ac 100644 --- a/src/spectator/events.cr +++ b/src/spectator/events.cr @@ -7,61 +7,90 @@ module Spectator # Defines an event for an example group. # This event typically runs before or after an example group finishes. # No contextual information (or example) is provided to the hooks. + # # The *name* defines the name of the event. - # This must be unique across all events. - # Three methods are defined - one to add a hook and the others to trigger the event which calls every hook. + # This must be unique across all events, not just group events. + # Three public methods are defined - one to add a hook and the others to trigger the event which calls every hook. # One trigger method, prefixed with *call_* will always call the event hooks. # The other trigger method, prefixed with *call_once_* will only call the event hooks on the first invocation. + # + # A block must be provided to this macro. + # The block defines the logic for invoking all of the hooks. + # A single argument is yielded to the block - the set of hooks for the event. + # + # ``` + # group_event some_hook do |hooks| + # hooks.each(&.call) + # end + # ``` private macro group_event(name, &block) - @{{name.id}}_hooks = Deque(->).new - @{{name.id}}_called = Atomic::Flag.new + @%hooks = Deque(->).new + @%called = Atomic::Flag.new # Defines a hook for the *{{name.id}}* event. # The block of code given to this method is invoked when the event occurs. def {{name.id}}(&block) : Nil - @{{name.id}}_hooks << block + @%hooks << block end # Signals that the *{{name.id}}* event has occurred. # All hooks associated with the event will be called. def call_{{name.id}} : Nil - {{block.args.first}} = @{{name.id}}_hooks - {{yield}} + %block(@%hooks) end # Signals that the *{{name.id}}* event has occurred. # Only calls the hooks if the event hasn't been triggered before by this method. # Returns true if the hooks were called and false if they weren't (called previously). def call_once_{{name.id}} : Bool - first = @{{name.id}}_called.test_and_set + first = @%called.test_and_set call_{{name.id}} if first first end + + # Logic specific to invoking the *{{name.id}}* hook. + private def %block({{block.args.splat}}) + {{block.body}} + end end # Defines an event for an example. # This event typically runs before or after an example finishes. # The current example is provided to the hooks. + # # The *name* defines the name of the event. # This must be unique across all events. - # Three methods are defined - two to add a hook and the other to trigger the event which calls every hook. - # A hook can be added with an `ExampleContextDelegate` or a block that accepts an example (no context). + # Two public methods are defined - one to add a hook and the other to trigger the event which calls every hook. + # + # A block must be provided to this macro. + # The block defines the logic for invoking all of the hooks. + # Two arguments are yielded to the block - the set of hooks for the event, and the current example. + # + # ``` + # example_event some_hook do |hooks, example| + # hooks.each(&.call(example)) + # end + # ``` private macro example_event(name, &block) - @{{name.id}}_hooks = Deque(Example ->).new + @%hooks = Deque(Example ->).new # Defines a hook for the *{{name.id}}* event. # The block of code given to this method is invoked when the event occurs. # The current example is provided as a block argument. def {{name.id}}(&block : Example ->) : Nil - @{{name.id}}_hooks << block + @%hooks << block end # Signals that the *{{name.id}}* event has occurred. # All hooks associated with the event will be called. # The *example* should be the current example. - def call_{{name.id}}({{block.args[1]}}) : Nil - {{block.args.first}} = @{{name.id}}_hooks - {{yield}} + def call_{{name.id}}(example : Example) : Nil + %block(@%hooks, example) + end + + # Logic specific to invoking the *{{name.id}}* hook. + private def %block({{block.args.splat}}) + {{block.body}} end end end From 7d0ba752e94d2276b3c1146b6a32cb024c5401db Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Jan 2021 10:39:04 -0700 Subject: [PATCH 084/399] Change ExampleNode to SpecNode --- src/spectator/example.cr | 4 ++-- src/spectator/example_group.cr | 14 +++++++------- src/spectator/example_iterator.cr | 8 ++++---- src/spectator/{example_node.cr => spec_node.cr} | 13 ++++++++----- 4 files changed, 21 insertions(+), 18 deletions(-) rename src/spectator/{example_node.cr => spec_node.cr} (84%) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index ba100bf..a90931e 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -1,14 +1,14 @@ require "./example_context_delegate" require "./example_group" -require "./example_node" require "./harness" require "./pending_result" require "./result" require "./source" +require "./spec_node" module Spectator # Standard example that runs a test case. - class Example < ExampleNode + class Example < SpecNode # Currently running example. class_getter! current : Example diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index 0f64801..2482618 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -1,12 +1,12 @@ require "./events" -require "./example_node" +require "./spec_node" module Spectator # Collection of examples and sub-groups. - class ExampleGroup < ExampleNode - include Enumerable(ExampleNode) + class ExampleGroup < SpecNode + include Enumerable(SpecNode) include Events - include Iterable(ExampleNode) + include Iterable(SpecNode) group_event before_all do |hooks| Log.trace { "Processing before_all hooks" } @@ -48,11 +48,11 @@ module Spectator end end - @nodes = [] of ExampleNode + @nodes = [] of SpecNode # Removes the specified *node* from the group. # The node will be unassigned from this group. - def delete(node : ExampleNode) + def delete(node : SpecNode) # Only remove from the group if it is associated with this group. return unless node.group == self @@ -79,7 +79,7 @@ module Spectator # Assigns the node to this group. # If the node already belongs to a group, # it will be removed from the previous group before adding it to this group. - def <<(node : ExampleNode) + def <<(node : SpecNode) # Remove from existing group if the node is part of one. if (previous = node.group?) previous.delete(node) diff --git a/src/spectator/example_iterator.cr b/src/spectator/example_iterator.cr index 217f0a0..f8acbb3 100644 --- a/src/spectator/example_iterator.cr +++ b/src/spectator/example_iterator.cr @@ -1,6 +1,6 @@ require "./example" require "./example_group" -require "./example_node" +require "./spec_node" module Spectator # Iterates through all examples in a group and its nested groups. @@ -9,12 +9,12 @@ module Spectator # Stack that contains the iterators for each group. # A stack is used to track where in the tree this iterator is. - @stack : Array(Iterator(ExampleNode)) + @stack : Array(Iterator(SpecNode)) # Creates a new iterator. # The *group* is the example group to iterate through. def initialize(@group : ExampleGroup) - iter = @group.each.as(Iterator(ExampleNode)) + iter = @group.each.as(Iterator(SpecNode)) @stack = [iter] end @@ -39,7 +39,7 @@ module Spectator # Restart the iterator at the beginning. def rewind # Same code as `#initialize`, but return self. - iter = @group.each.as(Iterator(ExampleNode)) + iter = @group.each.as(Iterator(SpecNode)) @stack = [iter] self end diff --git a/src/spectator/example_node.cr b/src/spectator/spec_node.cr similarity index 84% rename from src/spectator/example_node.cr rename to src/spectator/spec_node.cr index 4444ddc..a559938 100644 --- a/src/spectator/example_node.cr +++ b/src/spectator/spec_node.cr @@ -1,12 +1,15 @@ +require "./label" require "./source" module Spectator - # A single example or collection (group) of examples in an example tree. - abstract class ExampleNode + # A single item in a test spec. + # This is commonly an `Example` or `ExampleGroup`, + # but can be anything that should be iterated over when running the spec. + abstract class SpecNode # Location of the node in source code. getter! source : Source - # User-provided name or description of the test. + # User-provided name or description of the node. # This does not include the group name or descriptions. # Use `#to_s` to get the full name. # @@ -16,7 +19,7 @@ module Spectator # of the first matcher that runs in the test case. # # If this value is a `Symbol`, the user specified a type for the name. - getter! name : String | Symbol + getter! name : Label # Updates the name of the node. protected def name=(@name : String) @@ -35,7 +38,7 @@ module Spectator # It can be a `Symbol` to describe a type. # The *source* tracks where the node exists in source code. # The node will be assigned to *group* if it is provided. - def initialize(@name : String | Symbol? = nil, @source : Source? = nil, group : ExampleGroup? = nil) + def initialize(@name : Label = nil, @source : Source? = nil, group : ExampleGroup? = nil) # Ensure group is linked. group << self if group end From cf422eca02f0b743578029f7171eba0458243d47 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Jan 2021 10:56:33 -0700 Subject: [PATCH 085/399] Add null constructor --- src/spectator/context_delegate.cr | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/spectator/context_delegate.cr b/src/spectator/context_delegate.cr index 1beef83..ac927b9 100644 --- a/src/spectator/context_delegate.cr +++ b/src/spectator/context_delegate.cr @@ -1,5 +1,6 @@ require "./context" require "./context_method" +require "./null_context" module Spectator # Stores a test context and a method to call within it. @@ -10,6 +11,14 @@ module Spectator def initialize(@context : Context, @method : ContextMethod) end + # Creates a delegate with a null context. + # The context will be ignored and the block will be executed in its original scope. + def self.null(&block : -> _) + context = NullContext.new + method = ContextMethod.new { block.call } + new(context, method) + end + # Invokes a method in the test context. def call @method.call(@context) From e5cbc8d63157aa4fc5d1e14193caf8b65ae0d769 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Jan 2021 11:06:59 -0700 Subject: [PATCH 086/399] Promote hooks to fully-fledge types Hook types include a source, label, and context delegate. --- src/spectator/events.cr | 29 +++++++++++++++++------- src/spectator/example_group_hook.cr | 32 +++++++++++++++++++++++++++ src/spectator/example_hook.cr | 34 +++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 src/spectator/example_group_hook.cr create mode 100644 src/spectator/example_hook.cr diff --git a/src/spectator/events.cr b/src/spectator/events.cr index 33857ac..42c6bbd 100644 --- a/src/spectator/events.cr +++ b/src/spectator/events.cr @@ -1,4 +1,5 @@ -require "./example_context_delegate" +require "./example_group_hook" +require "./example_hook" module Spectator # Mix-in for managing events and hooks. @@ -10,7 +11,7 @@ module Spectator # # The *name* defines the name of the event. # This must be unique across all events, not just group events. - # Three public methods are defined - one to add a hook and the others to trigger the event which calls every hook. + # Four public methods are defined - two to add a hook and the others to trigger the event which calls every hook. # One trigger method, prefixed with *call_* will always call the event hooks. # The other trigger method, prefixed with *call_once_* will only call the event hooks on the first invocation. # @@ -24,13 +25,19 @@ module Spectator # end # ``` private macro group_event(name, &block) - @%hooks = Deque(->).new + @%hooks = [] of ExampleGroupHook @%called = Atomic::Flag.new + # Adds a hook to be invoked when the *{{name.id}}* event occurs. + def add_{{name.id}}_hook(hook : ExampleGroupHook) : Nil + @%hooks << hook + end + # Defines a hook for the *{{name.id}}* event. # The block of code given to this method is invoked when the event occurs. - def {{name.id}}(&block) : Nil - @%hooks << block + def {{name.id}}(&block : -> _) : Nil + hook = ExampleGroupHook.new(label: {{name.stringify}}, &block) + add_{{name.id}}_hook(hook) end # Signals that the *{{name.id}}* event has occurred. @@ -60,7 +67,7 @@ module Spectator # # The *name* defines the name of the event. # This must be unique across all events. - # Two public methods are defined - one to add a hook and the other to trigger the event which calls every hook. + # Three public methods are defined - two to add a hook and the other to trigger the event which calls every hook. # # A block must be provided to this macro. # The block defines the logic for invoking all of the hooks. @@ -72,13 +79,19 @@ module Spectator # end # ``` private macro example_event(name, &block) - @%hooks = Deque(Example ->).new + @%hooks = [] of ExampleHook + + # Adds a hook to be invoked when the *{{name.id}}* event occurs. + def add_{{name.id}}_hook(hook : ExampleHook) : Nil + @%hooks << hook + end # Defines a hook for the *{{name.id}}* event. # The block of code given to this method is invoked when the event occurs. # The current example is provided as a block argument. def {{name.id}}(&block : Example ->) : Nil - @%hooks << block + hook = ExampleHook.new(label: {{name.stringify}}, &block) + add_{{name.id}}_hook(hook) end # Signals that the *{{name.id}}* event has occurred. diff --git a/src/spectator/example_group_hook.cr b/src/spectator/example_group_hook.cr new file mode 100644 index 0000000..4748b2a --- /dev/null +++ b/src/spectator/example_group_hook.cr @@ -0,0 +1,32 @@ +require "./context_delegate" +require "./label" +require "./source" + +module Spectator + # Information about a hook tied to an example group and a delegate to invoke it. + class ExampleGroupHook + # Location of the hook in source code. + getter! source : Source + + # User-defined description of the hook. + getter! label : Label + + # Creates the hook with a context delegate. + # The *delegate* will be called when the hook is invoked. + # A *source* and *label* can be provided for debugging. + def initialize(@delegate : ContextDelegate, *, @source : Source? = nil, @label : Label = nil) + end + + # Creates the hook with a block. + # The block will be executed when the hook is invoked. + # A *source* and *label* can be provided for debugging. + def initialize(*, @source : Source? = nil, @label : Label = nil, &block : -> _) + @delegate = ContextDelegate.null(&block) + end + + # Invokes the hook. + def call : Nil + @delegate.call + end + end +end diff --git a/src/spectator/example_hook.cr b/src/spectator/example_hook.cr new file mode 100644 index 0000000..b169b9d --- /dev/null +++ b/src/spectator/example_hook.cr @@ -0,0 +1,34 @@ +require "./example_context_delegate" +require "./label" +require "./source" + +module Spectator + # Information about a hook tied to an example and a delegate to invoke it. + class ExampleHook + # Location of the hook in source code. + getter! source : Source + + # User-defined description of the hook. + getter! label : Label + + # Creates the hook with an example context delegate. + # The *delegate* will be called when the hook is invoked. + # A *source* and *label* can be provided for debugging. + def initialize(@delegate : ExampleContextDelegate, *, @source : Source? = nil, @label : Label = nil) + end + + # Creates the hook with a block. + # The block must take a single argument - the current example. + # The block will be executed when the hook is invoked. + # A *source* and *label* can be provided for debugging. + def initialize(*, @source : Source? = nil, @label : Label = nil, &block : Example -> _) + @delegate = ExampleContextDelegate.null(&block) + end + + # Invokes the hook. + # The *example* refers to the current example. + def call(example : Example) : Nil + @delegate.call(example) + end + end +end From def66acc15ad37b96b05b4acb2c5975d31a26134 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Jan 2021 11:14:27 -0700 Subject: [PATCH 087/399] Trace hook invocation --- src/spectator/example_group.cr | 20 ++++++++++++++++---- src/spectator/example_group_hook.cr | 16 ++++++++++++++++ src/spectator/example_hook.cr | 16 ++++++++++++++++ 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index 2482618..ab27112 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -15,13 +15,19 @@ module Spectator parent.call_once_before_all end - hooks.each(&.call) + hooks.each do |hook| + Log.trace { "Invoking hook #{hook}" } + hook.call + end end group_event after_all do |hooks| Log.trace { "Processing after_all hooks" } - hooks.each(&.call) + hooks.each do |hook| + Log.trace { "Invoking hook #{hook}" } + hook.call + end if (parent = group?) parent.call_once_after_all @@ -35,13 +41,19 @@ module Spectator parent.call_before_each(example) end - hooks.each(&.call(example)) + hooks.each do |hook| + Log.trace { "Invoking hook #{hook}" } + hook.call(example) + end end example_event after_each do |hooks, example| Log.trace { "Processing after_each hooks" } - hooks.each(&.call(example)) + hooks.each do |hook| + Log.trace { "Invoking hook #{hook}" } + hook.call(example) + end if (parent = group?) parent.call_after_each(example) diff --git a/src/spectator/example_group_hook.cr b/src/spectator/example_group_hook.cr index 4748b2a..11b0117 100644 --- a/src/spectator/example_group_hook.cr +++ b/src/spectator/example_group_hook.cr @@ -28,5 +28,21 @@ module Spectator def call : Nil @delegate.call end + + # Produces the string representation of the hook. + # Includes the source and label if they're not nil. + def to_s(io) + io << "example group hook" + + if (label = @label) + io << ' ' + io << label + end + + if (source = @source) + io << " @ " + io << source + end + end end end diff --git a/src/spectator/example_hook.cr b/src/spectator/example_hook.cr index b169b9d..4597981 100644 --- a/src/spectator/example_hook.cr +++ b/src/spectator/example_hook.cr @@ -30,5 +30,21 @@ module Spectator def call(example : Example) : Nil @delegate.call(example) end + + # Produces the string representation of the hook. + # Includes the source and label if they're not nil. + def to_s(io) + io << "example hook" + + if (label = @label) + io << ' ' + io << label + end + + if (source = @source) + io << " @ " + io << source + end + end end end From df096d91aaff405eda3f3ee8a6ede4f9ef198710 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Jan 2021 11:30:00 -0700 Subject: [PATCH 088/399] Capture source info for hooks --- src/spectator/dsl/builder.cr | 28 ++++++++++++---------------- src/spectator/dsl/hooks.cr | 20 ++++++++++++-------- src/spectator/spec_builder.cr | 26 ++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr index 722f2b7..8a2fdd4 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -36,31 +36,27 @@ module Spectator::DSL end # Defines a block of code to execute before any and all examples in the current group. - # - # See `Spec::Builder#before_all` for usage details. - def before_all(&block) - @@builder.before_all(&block) + def before_all(source = nil, label = "before_all", &block) + hook = ExampleGroupHook.new(source: source, label: label, &block) + @@builder.before_all(hook) end # Defines a block of code to execute before every example in the current group - # - # See `Spec::Builder#before_each` for usage details. - def before_each(&block : Example -> _) - @@builder.before_each(&block) + def before_each(source = nil, label = "before_each", &block : Example -> _) + hook = ExampleHook.new(source: source, label: label, &block) + @@builder.before_each(hook) end # Defines a block of code to execute after any and all examples in the current group. - # - # See `Spec::Builder#after_all` for usage details. - def after_all(&block) - @@builder.after_all(&block) + def after_all(source = nil, label = "after_all", &block) + hook = ExampleGroupHook.new(source: source, label: label, &block) + @@builder.after_all(hook) end # Defines a block of code to execute after every example in the current group. - # - # See `Spec::Builder#after_each` for usage details. - def after_each(&block : Example ->) - @@builder.after_each(&block) + def after_each(source = nil, label = "after_each", &block : Example ->) + hook = ExampleHook.new(source: source, label: label, &block) + @@builder.after_each(hook) end # Sets the configuration of the spec. diff --git a/src/spectator/dsl/hooks.cr b/src/spectator/dsl/hooks.cr index fac734f..8cb4d06 100644 --- a/src/spectator/dsl/hooks.cr +++ b/src/spectator/dsl/hooks.cr @@ -11,7 +11,9 @@ module Spectator::DSL {{block.body}} end - ::Spectator::DSL::Builder.before_all { {{@type.name}}.%hook } + ::Spectator::DSL::Builder.before_all( + ::Spectator::Source.new({{block.filename}}, {{block.line_number}}) + ) { {{@type.name}}.%hook } end macro before_each(&block) @@ -21,9 +23,9 @@ module Spectator::DSL {{block.body}} end - ::Spectator::DSL::Builder.before_each do |example| - example.with_context({{@type.name}}) { %hook } - end + ::Spectator::DSL::Builder.before_each( + ::Spectator::Source.new({{block.filename}}, {{block.line_number}}) + ) { |example| example.with_context({{@type.name}}) { %hook } } end macro after_all(&block) @@ -33,7 +35,9 @@ module Spectator::DSL {{block.body}} end - ::Spectator::DSL::Builder.after_all { {{@type.name}}.%hook } + ::Spectator::DSL::Builder.after_all( + ::Spectator::Source.new({{block.filename}}, {{block.line_number}}) + ) { {{@type.name}}.%hook } end macro after_each(&block) @@ -43,9 +47,9 @@ module Spectator::DSL {{block.body}} end - ::Spectator::DSL::Builder.after_each do |example| - example.with_context({{@type.name}}) { %hook } - end + ::Spectator::DSL::Builder.after_each( + ::Spectator::Source.new({{block.filename}}, {{block.line_number}}) + ) { |example| example.with_context({{@type.name}}) { %hook } } end end end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 387fbfd..770a7cb 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -97,12 +97,25 @@ module Spectator # The example is added to the current group by `Example` initializer. end + # Attaches a hook to be invoked before any and all examples in the current group. + def before_all(hook) + Log.trace { "Add before_all hook #{hook}" } + current_group.add_before_all_hook(hook) + end + # Defines a block of code to execute before any and all examples in the current group. def before_all(&block) Log.trace { "Add before_all hook" } current_group.before_all(&block) end + # Attaches a hook to be invoked before every example in the current group. + # The current example is provided as a block argument. + def before_each(hook) + Log.trace { "Add before_each hook #{hook}" } + current_group.add_before_each_hook(hook) + end + # Defines a block of code to execute before every example in the current group. # The current example is provided as a block argument. def before_each(&block : Example -> _) @@ -110,12 +123,25 @@ module Spectator current_group.before_each(&block) end + # Attaches a hook to be invoked after any and all examples in the current group. + def after_all(hook) + Log.trace { "Add after_all hook #{hook}" } + current_group.add_after_all_hook(hook) + end + # Defines a block of code to execute after any and all examples in the current group. def after_all(&block) Log.trace { "Add after_all hook" } current_group.after_all(&block) end + # Attaches a hook to be invoked after every example in the current group. + # The current example is provided as a block argument. + def after_each(hook) + Log.trace { "Add after_each hook #{hook}" } + current_group.add_after_each_hook(hook) + end + # Defines a block of code to execute after every example in the current group. # The current example is provided as a block argument. def after_each(&block : Example -> _) From c4289b82daa2eae3d142413d7d032d38d8fffcf8 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Jan 2021 11:33:29 -0700 Subject: [PATCH 089/399] Better trace message for initial hook processing --- src/spectator/example_group.cr | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index ab27112..6befc14 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -9,7 +9,7 @@ module Spectator include Iterable(SpecNode) group_event before_all do |hooks| - Log.trace { "Processing before_all hooks" } + Log.trace { "Processing before_all hooks for #{self}" } if (parent = group?) parent.call_once_before_all @@ -22,7 +22,7 @@ module Spectator end group_event after_all do |hooks| - Log.trace { "Processing after_all hooks" } + Log.trace { "Processing after_all hooks for #{self}" } hooks.each do |hook| Log.trace { "Invoking hook #{hook}" } @@ -35,7 +35,7 @@ module Spectator end example_event before_each do |hooks, example| - Log.trace { "Processing before_each hooks" } + Log.trace { "Processing before_each hooks for #{self}" } if (parent = group?) parent.call_before_each(example) @@ -48,7 +48,7 @@ module Spectator end example_event after_each do |hooks, example| - Log.trace { "Processing after_each hooks" } + Log.trace { "Processing after_each hooks for #{self}" } hooks.each do |hook| Log.trace { "Invoking hook #{hook}" } From 65dba9f3170e3e54d839b9a3c20bde9d61c023af Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Jan 2021 12:04:27 -0700 Subject: [PATCH 090/399] Don't need delegates stored in hooks Use proc instead. The context can be retrieved from the example instance. --- src/spectator/example_group_hook.cr | 15 ++++++++------- src/spectator/example_hook.cr | 15 ++++++++------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/spectator/example_group_hook.cr b/src/spectator/example_group_hook.cr index 11b0117..06b8c7d 100644 --- a/src/spectator/example_group_hook.cr +++ b/src/spectator/example_group_hook.cr @@ -1,9 +1,8 @@ -require "./context_delegate" require "./label" require "./source" module Spectator - # Information about a hook tied to an example group and a delegate to invoke it. + # Information about a hook tied to an example group and a proc to invoke it. class ExampleGroupHook # Location of the hook in source code. getter! source : Source @@ -11,22 +10,24 @@ module Spectator # User-defined description of the hook. getter! label : Label - # Creates the hook with a context delegate. - # The *delegate* will be called when the hook is invoked. + @proc : -> + + # Creates the hook with a proc. + # The *proc* will be called when the hook is invoked. # A *source* and *label* can be provided for debugging. - def initialize(@delegate : ContextDelegate, *, @source : Source? = nil, @label : Label = nil) + def initialize(@proc : (->), *, @source : Source? = nil, @label : Label = nil) end # Creates the hook with a block. # The block will be executed when the hook is invoked. # A *source* and *label* can be provided for debugging. def initialize(*, @source : Source? = nil, @label : Label = nil, &block : -> _) - @delegate = ContextDelegate.null(&block) + @proc = block end # Invokes the hook. def call : Nil - @delegate.call + @proc.call end # Produces the string representation of the hook. diff --git a/src/spectator/example_hook.cr b/src/spectator/example_hook.cr index 4597981..b61fa7d 100644 --- a/src/spectator/example_hook.cr +++ b/src/spectator/example_hook.cr @@ -1,9 +1,8 @@ -require "./example_context_delegate" require "./label" require "./source" module Spectator - # Information about a hook tied to an example and a delegate to invoke it. + # Information about a hook tied to an example and a proc to invoke it. class ExampleHook # Location of the hook in source code. getter! source : Source @@ -11,10 +10,12 @@ module Spectator # User-defined description of the hook. getter! label : Label - # Creates the hook with an example context delegate. - # The *delegate* will be called when the hook is invoked. + @proc : Example -> + + # Creates the hook with a proc. + # The *proc* will be called when the hook is invoked. # A *source* and *label* can be provided for debugging. - def initialize(@delegate : ExampleContextDelegate, *, @source : Source? = nil, @label : Label = nil) + def initialize(@proc : (Example ->), *, @source : Source? = nil, @label : Label = nil) end # Creates the hook with a block. @@ -22,13 +23,13 @@ module Spectator # The block will be executed when the hook is invoked. # A *source* and *label* can be provided for debugging. def initialize(*, @source : Source? = nil, @label : Label = nil, &block : Example -> _) - @delegate = ExampleContextDelegate.null(&block) + @proc = block end # Invokes the hook. # The *example* refers to the current example. def call(example : Example) : Nil - @delegate.call(example) + @proc.call(example) end # Produces the string representation of the hook. From 2e8036d230f55d03a5c6b8b00d0134a14d3fde57 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Jan 2021 12:48:53 -0700 Subject: [PATCH 091/399] Move instance variable --- src/spectator/example_group.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index 6befc14..056950c 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -8,6 +8,8 @@ module Spectator include Events include Iterable(SpecNode) + @nodes = [] of SpecNode + group_event before_all do |hooks| Log.trace { "Processing before_all hooks for #{self}" } @@ -60,8 +62,6 @@ module Spectator end end - @nodes = [] of SpecNode - # Removes the specified *node* from the group. # The node will be unassigned from this group. def delete(node : SpecNode) From fbd9713d5242e37c93e74392e7c918171562b062 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Jan 2021 12:56:35 -0700 Subject: [PATCH 092/399] Remove unused and deprecated types --- src/spectator/example_conditions.cr | 41 -------------- src/spectator/example_hooks.cr | 75 ------------------------ src/spectator/nested_example_group.cr | 54 ------------------ src/spectator/pending_example.cr | 12 ---- src/spectator/root_example_group.cr | 28 --------- src/spectator/runnable_example.cr | 82 --------------------------- src/spectator/test_values.cr | 64 --------------------- 7 files changed, 356 deletions(-) delete mode 100644 src/spectator/example_conditions.cr delete mode 100644 src/spectator/example_hooks.cr delete mode 100644 src/spectator/nested_example_group.cr delete mode 100644 src/spectator/pending_example.cr delete mode 100644 src/spectator/root_example_group.cr delete mode 100644 src/spectator/runnable_example.cr delete mode 100644 src/spectator/test_values.cr diff --git a/src/spectator/example_conditions.cr b/src/spectator/example_conditions.cr deleted file mode 100644 index 0fbd03d..0000000 --- a/src/spectator/example_conditions.cr +++ /dev/null @@ -1,41 +0,0 @@ -module Spectator - # Collection of checks that run before and after tests. - # The pre-conditions can be used to verify - # that the SUT is in an expected state prior to testing. - # The post-conditions can be used to verify - # that the SUT is in an expected state after tests have finished. - # Each check is just a `Proc` (code block) that runs when invoked. - class ExampleConditions - # Creates an empty set of conditions. - # This will effectively run nothing extra while running a test. - def self.empty - new( - [] of TestMetaMethod, - [] of TestMetaMethod - ) - end - - # Creates a new set of conditions. - def initialize( - @pre_conditions : Array(TestMetaMethod), - @post_conditions : Array(TestMetaMethod) - ) - end - - # Runs all pre-condition checks. - # These should be run before every test. - def run_pre_conditions(wrapper : TestWrapper, example : Example) - @pre_conditions.each do |hook| - wrapper.call(hook, example) - end - end - - # Runs all post-condition checks. - # These should be run after every test. - def run_post_conditions(wrapper : TestWrapper, example : Example) - @post_conditions.each do |hook| - wrapper.call(hook, example) - end - end - end -end diff --git a/src/spectator/example_hooks.cr b/src/spectator/example_hooks.cr deleted file mode 100644 index d345bf5..0000000 --- a/src/spectator/example_hooks.cr +++ /dev/null @@ -1,75 +0,0 @@ -module Spectator - alias TestMetaMethod = ::SpectatorTest, Example -> - - # Collection of hooks that run at various times throughout testing. - # A hook is just a `Proc` (code block) that runs at a specified time. - class ExampleHooks - # Creates an empty set of hooks. - # This will effectively run nothing extra while running a test. - def self.empty - new( - [] of ->, - [] of TestMetaMethod, - [] of ->, - [] of TestMetaMethod, - [] of ::SpectatorTest, Proc(Nil) -> - ) - end - - # Creates a new set of hooks. - def initialize( - @before_all : Array(->), - @before_each : Array(TestMetaMethod), - @after_all : Array(->), - @after_each : Array(TestMetaMethod), - @around_each : Array(::SpectatorTest, Proc(Nil) ->) - ) - end - - # Runs all "before-all" hooks. - # These hooks should be run once before all examples in the group start. - def run_before_all - @before_all.each &.call - end - - # Runs all "before-each" hooks. - # These hooks should be run every time before each example in a group. - def run_before_each(wrapper : TestWrapper, example : Example) - @before_each.each do |hook| - wrapper.call(hook, example) - end - end - - # Runs all "after-all" hooks. - # These hooks should be run once after all examples in group finish. - def run_after_all - @after_all.each &.call - end - - # Runs all "after-all" hooks. - # These hooks should be run every time after each example in a group. - def run_after_each(wrapper : TestWrapper, example : Example) - @after_each.each do |hook| - wrapper.call(hook, example) - end - end - - # Creates a proc that runs the "around-each" hooks - # in addition to a block passed to this method. - # To call the block and all "around-each" hooks, - # just invoke `Proc#call` on the returned proc. - def wrap_around_each(test, block : ->) - wrapper = block - # Must wrap in reverse order, - # otherwise hooks will run in the wrong order. - @around_each.reverse_each do |hook| - wrapper = wrap_foo(test, hook, wrapper) - end - wrapper - end - - private def wrap_foo(test, hook, wrapper) - ->{ hook.call(test, wrapper) } - end - end -end diff --git a/src/spectator/nested_example_group.cr b/src/spectator/nested_example_group.cr deleted file mode 100644 index c21345e..0000000 --- a/src/spectator/nested_example_group.cr +++ /dev/null @@ -1,54 +0,0 @@ -require "./example_group" - -module Spectator - # A collection of examples and other example groups. - # This group can be nested under other groups. - class NestedExampleGroup < ExampleGroup - # Description from the user of the group's contents. - # This is a symbol when referencing a type. - getter description : Symbol | String - - getter source : Source - - # Group that this is nested in. - getter parent : ExampleGroup - - # Creates a new example group. - # The *description* argument is a description from the user. - # The *parent* should contain this group. - # After creating this group, the parent's children should be updated. - # The parent's children must contain this group, - # otherwise there may be unexpected behavior. - # The *hooks* are stored to be triggered later. - def initialize(@description, @source, @parent, context) - super(context) - end - - # Indicates wheter the group references a type. - def symbolic? : Bool - @description.is_a?(Symbol) - end - - # Creates a string representation of the group. - # The string consists of `#description` appended to the parent. - # This results in a string like: - # ```text - # Foo#bar does something - # ``` - # for the following structure: - # ``` - # describe Foo do - # describe "#bar" do - # it "does something" do - # # ... - # end - # end - # end - # ``` - def to_s(io) - parent.to_s(io) - io << ' ' unless (symbolic? || parent.is_a?(RootExampleGroup)) && parent.symbolic? - io << description - end - end -end diff --git a/src/spectator/pending_example.cr b/src/spectator/pending_example.cr deleted file mode 100644 index 743b778..0000000 --- a/src/spectator/pending_example.cr +++ /dev/null @@ -1,12 +0,0 @@ -require "./example" - -module Spectator - # Common class for all examples marked as pending. - # This class will not run example code. - class PendingExample < Example - # Returns a pending result. - private def run_impl : Result - PendingResult.new(self) - end - end -end diff --git a/src/spectator/root_example_group.cr b/src/spectator/root_example_group.cr deleted file mode 100644 index 16380e0..0000000 --- a/src/spectator/root_example_group.cr +++ /dev/null @@ -1,28 +0,0 @@ -require "./example_group" - -module Spectator - # Top-most group of examples and sub-groups. - # The root has no parent. - class RootExampleGroup < ExampleGroup - # Dummy value - this should never be used. - def description : Symbol | String - :root - end - - def source : Source - Source.new(__FILE__, __LINE__) - end - - # Indicates that the group is symbolic. - def symbolic? : Bool - true - end - - # Does nothing. - # This prevents the root group - # from showing up in output. - def to_s(io) - # ... - end - end -end diff --git a/src/spectator/runnable_example.cr b/src/spectator/runnable_example.cr deleted file mode 100644 index fbfb40e..0000000 --- a/src/spectator/runnable_example.cr +++ /dev/null @@ -1,82 +0,0 @@ -require "./example" - -module Spectator - # Includes all the logic for running example hooks, - # the example code, and capturing a result. - class RunnableExample < Example - # Runs the example, hooks, and captures the result - # and translates to a usable result. - def run_impl : Result - result = capture_result - expectations = Harness.current.expectations - translate_result(result, expectations) - end - - # Runs all hooks and the example code. - # A captured result is returned. - private def capture_result - context = group.context - ResultCapture.new.tap do |result| - context.run_before_hooks(self) - run_example(result) - @finished = true - context.run_after_hooks(self) - run_deferred(result) unless result.error - end - end - - # Runs the test code and captures the result. - private def run_example(result) - context = group.context - wrapper = test_wrapper.around_hook(context) - - # Capture how long it takes to run the test code. - result.elapsed = Time.measure do - begin - context.run_pre_conditions(self) - wrapper.call - context.run_post_conditions(self) - rescue ex # Catch all errors and handle them later. - result.error = ex - end - end - end - - # Runs the deferred blocks of code and captures the result. - private def run_deferred(result) - result.elapsed += Time.measure do - begin - Harness.current.run_deferred - rescue ex # Catch all errors and handle them later. - result.error = ex - end - end - end - - # Creates a result instance from captured result information. - private def translate_result(result, expectations) - case (error = result.error) - when Nil - # If no errors occurred, then the example ran successfully. - SuccessfulResult.new(self, result.elapsed, expectations) - when ExpectationFailed - # If a required expectation fails, then a `ExpectationRailed` exception will be raised. - FailedResult.new(self, result.elapsed, expectations, error) - else - # Any other exception that is raised is unexpected and is an errored result. - ErroredResult.new(self, result.elapsed, expectations, error) - end - end - - # Utility class for storing parts of the result while the example is running. - private class ResultCapture - # Length of time that it took to run the test code. - # This does not include hooks. - property elapsed = Time::Span.zero - - # The error that occurred while running the test code. - # If no error occurred, this will be nil. - property error : Exception? - end - end -end diff --git a/src/spectator/test_values.cr b/src/spectator/test_values.cr deleted file mode 100644 index 8503ca8..0000000 --- a/src/spectator/test_values.cr +++ /dev/null @@ -1,64 +0,0 @@ -require "./typed_value_wrapper" -require "./value_wrapper" - -module Spectator - # Collection of test values supplied to examples. - # Each value is labeled by a symbol that the example knows. - # The values also come with a name that can be given to humans. - struct TestValues - # Creates an empty set of sample values. - def self.empty - new({} of Symbol => Entry) - end - - # Creates a collection of sample values. - protected def initialize(@values = {} of Symbol => Entry) - end - - # Adds a new value by duplicating the current set and adding to it. - # The new sample values with the additional value is returned. - # The original set of sample values is not modified. - def add(id : Symbol, name : String, value) : TestValues - wrapper = TypedValueWrapper.new(value) - TestValues.new(@values.merge({ - id => Entry.new(name, wrapper), - })) - end - - # Retrieves the wrapper for a value. - # The symbol for the value is used for retrieval. - def get_wrapper(id : Symbol) - @values[id].wrapper - end - - # Retrieves a value. - # The symbol for the value is used for retrieval. - # The value's type must be provided so that the wrapper can be cast. - def get_value(id : Symbol, value_type : T.class) : T forall T - get_wrapper(id).as(TypedValueWrapper(T)).value - end - - # Iterates over all values and yields them. - def each - @values.each_value do |entry| - yield entry - end - end - - # Represents a single value in the set. - private struct Entry - # Human-friendly name for the value. - getter name : String - - # Wrapper for the value. - getter wrapper : ValueWrapper - - # Creates a new value entry. - def initialize(@name, @wrapper) - end - end - - # This must be after `Entry` is defined. - include Enumerable(Entry) - end -end From 009ca4776a0a8ac6dbb34672e942e4ed0ce7f9b0 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Jan 2021 13:17:42 -0700 Subject: [PATCH 093/399] Cleanup new DSL macros --- src/spectator/dsl/examples.cr | 86 ++++++--------------------- src/spectator/dsl/groups.cr | 106 +--------------------------------- src/spectator/dsl/hooks.cr | 80 +++++++++++++------------ src/spectator/dsl/top.cr | 4 +- 4 files changed, 64 insertions(+), 212 deletions(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 628f0fc..91d196a 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -1,3 +1,4 @@ +require "../context" require "../source" require "./builder" @@ -8,11 +9,23 @@ module Spectator::DSL # The *name* is the name given to the macro. # TODO: Mark example as pending if block is omitted. macro define_example(name) + # Defines an example. + # + # If a block is given, it is treated as the code to test. + # The block is provided the current example instance as an argument. + # + # The first argument names the example (test). + # Typically, this specifies what is being tested. + # It has no effect on the test and is purely used for output. + # If omitted, a name is generated from the first assertion in the test. + # + # The example will be marked as pending if the block is omitted. + # A block or name must be provided. macro {{name.id}}(what = nil, &block) \{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %} \{% raise "A description or block must be provided. Cannot use '{{name.id}}' alone." unless what || block %} - def \%test # TODO: Pass example instance. + def \%test : Nil # TODO: Pass example instance. \{{block.body}} end @@ -27,7 +40,7 @@ module Spectator::DSL # Inserts the correct representation of a example's name. # If *what* is a string, then it is dropped in as-is. # For anything else, it is stringified. - # This is intended to be used to convert a description from the spec DSL to `ExampleNode#name`. + # This is intended to be used to convert a description from the spec DSL to `SpecNode#name`. private macro _spectator_example_name(what) {% if what.is_a?(StringLiteral) || what.is_a?(StringInterpolation) || @@ -38,77 +51,12 @@ module Spectator::DSL {% end %} end - # Defines an example. - # - # If a block is given, it is treated as the code to test. - # The block is provided the current example instance as an argument. - # - # The first argument names the example (test). - # Typically, this specifies what is being tested. - # It has no effect on the test and is purely used for output. - # If omitted, a name is generated from the first assertion in the test. - # - # The example will be marked as pending if the block is omitted. - # A block or name must be provided. define_example :example - # Defines an example. - # - # If a block is given, it is treated as the code to test. - # The block is provided the current example instance as an argument. - # - # The first argument names the example (test). - # Typically, this specifies what is being tested. - # It has no effect on the test and is purely used for output. - # If omitted, a name is generated from the first assertion in the test. - # - # The example will be marked as pending if the block is omitted. - # A block or name must be provided. define_example :it - # Defines an example. - # - # If a block is given, it is treated as the code to test. - # The block is provided the current example instance as an argument. - # - # The first argument names the example (test). - # Typically, this specifies what is being tested. - # It has no effect on the test and is purely used for output. - # If omitted, a name is generated from the first assertion in the test. - # - # The example will be marked as pending if the block is omitted. - # A block or name must be provided. define_example :specify + + # TODO: pending, skip, and xit end - - macro pending(description = nil, _source_file = __FILE__, _source_line = __LINE__, &block) - {% if block.is_a?(Nop) %} - {% if description.is_a?(Call) %} - def %run - {{description}} - end - {% else %} - {% raise "Unrecognized syntax: `pending #{description}` at #{_source_file}:#{_source_line}" %} - {% end %} - {% else %} - def %run - {{block.body}} - end - {% end %} - - %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - ::Spectator::SpecBuilder.add_pending_example( - {{description.is_a?(StringLiteral) || description.is_a?(StringInterpolation) || description.is_a?(NilLiteral) ? description : description.stringify}}, - %source, - {{@type.name}} - ) { |test| test.as({{@type.name}}).%run } - end - - macro skip(description = nil, &block) - pending({{description}}) {{block}} - end - - macro xit(description = nil, &block) - pending({{description}}) {{block}} - end end diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 4a4c0f7..6bc8b47 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -32,7 +32,7 @@ module Spectator::DSL # If *what* appears to be a type name, it will be symbolized. # If it's a string, then it is dropped in as-is. # For anything else, it is stringified. - # This is intended to be used to convert a description from the spec DSL to `ExampleNode#name`. + # This is intended to be used to convert a description from the spec DSL to `SpecNode#name`. private macro _spectator_group_name(what) {% if (what.is_a?(Generic) || what.is_a?(Path) || @@ -85,107 +85,7 @@ module Spectator::DSL define_example_group :describe define_example_group :context + + # TODO: sample, random_sample, and given end - - macro sample(collection, count = nil, _source_file = __FILE__, _source_line = __LINE__, &block) - {% name = block.args.empty? ? :value.id : block.args.first.id %} - - def %collection - {{collection}} - end - - def %to_a - {% if count %} - %collection.first({{count}}) - {% else %} - %collection.to_a - {% end %} - end - - class Context%sample < {{@type.id}} - %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - ::Spectator::SpecBuilder.start_sample_group({{collection.stringify}}, %source, :%sample, {{name.stringify}}) do |values| - sample = {{@type.id}}.new(values) - sample.%to_a - end - - def {{name}} - @spectator_test_values.get_value(:%sample, typeof(%to_a.first)) - end - - {{block.body}} - - ::Spectator::SpecBuilder.end_group - end - end - - macro random_sample(collection, count = nil, _source_file = __FILE__, _source_line = __LINE__, &block) - {% name = block.args.empty? ? :value.id : block.args.first.id %} - - def %collection - {{collection}} - end - - def %to_a - {% if count %} - %collection.first({{count}}) - {% else %} - %collection.to_a - {% end %} - end - - class Context%sample < {{@type.id}} - %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - ::Spectator::SpecBuilder.start_sample_group({{collection.stringify}}, %source, :%sample, {{name.stringify}}) do |values| - sample = {{@type.id}}.new(values) - collection = sample.%to_a - {% if count %} - collection.sample({{count}}, ::Spectator.random) - {% else %} - collection.shuffle(::Spectator.random) - {% end %} - end - - def {{name}} - @spectator_test_values.get_value(:%sample, typeof(%to_a.first)) - end - - {{block.body}} - - ::Spectator::SpecBuilder.end_group - end - end - - macro given(*assignments, &block) - context({{assignments.splat.stringify}}) do - {% for assignment in assignments %} - let({{assignment.target}}) { {{assignment.value}} } - {% end %} - - {% # Trick to get the contents of the block as an array of nodes. -# If there are multiple expressions/statements in the block, -# then the body will be a `Expressions` type. -# If there's only one expression, then the body is just that. - body = if block.is_a?(Nop) - raise "Missing block for 'given'" - elsif block.body.is_a?(Expressions) - # Get the expressions, which is already an array. - block.body.expressions - else - # Wrap the expression in an array. - [block.body] - end %} - - {% for item in body %} - # If the item starts with "it", then leave it as-is. - # Otherwise, prefix it with "it" - # and treat it as the one-liner "it" syntax. - {% if item.is_a?(Call) && item.name == :it.id %} - {{item}} - {% else %} - it {{item}} - {% end %} - {% end %} - end - end end diff --git a/src/spectator/dsl/hooks.cr b/src/spectator/dsl/hooks.cr index 8cb4d06..e84df19 100644 --- a/src/spectator/dsl/hooks.cr +++ b/src/spectator/dsl/hooks.cr @@ -1,55 +1,61 @@ +require "../source" require "./builder" module Spectator::DSL # DSL methods for adding custom logic to key times of the spec execution. module Hooks - # Defines code to run before any and all examples in an example group. - macro before_all(&block) - {% raise "Cannot use 'before_all' inside of a test block" if @def %} + # Defines a macro to create an example group hook. + # The *type* indicates when the hook runs and must be a method on `Spectator::DSL::Builder`. + macro define_example_group_hook(type) + macro {{type.id}}(&block) + \{% raise "Missing block for '{{type.id}}' hook" unless block %} + \{% raise "Cannot use '{{type.id}}' inside of a test block" if @def %} - def self.%hook : Nil - {{block.body}} + def self.\%hook : Nil + \{{block.body}} + end + + ::Spectator::DSL::Builder.{{type.id}}( + ::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}}) + ) { \{{@type.name}}.\%hook } end - - ::Spectator::DSL::Builder.before_all( - ::Spectator::Source.new({{block.filename}}, {{block.line_number}}) - ) { {{@type.name}}.%hook } end - macro before_each(&block) - {% raise "Cannot use 'before_each' inside of a test block" if @def %} + # Defines a macro to create an example hook. + # The *type* indicates when the hook runs and must be a method on `Spectator::DSL::Builder`. + macro define_example_hook(type) + macro {{type.id}}(&block) + \{% raise "Missing block for '{{type.id}}' hook" unless block %} + \{% raise "Cannot use '{{type.id}}' inside of a test block" if @def %} - def %hook : Nil - {{block.body}} + def \%hook : Nil # TODO: Pass example instance. + \{{block.body}} + end + + ::Spectator::DSL::Builder.{{type.id}}( + ::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}}) + ) { |example| example.with_context(\{{@type.name}}) { \%hook } } end - - ::Spectator::DSL::Builder.before_each( - ::Spectator::Source.new({{block.filename}}, {{block.line_number}}) - ) { |example| example.with_context({{@type.name}}) { %hook } } end - macro after_all(&block) - {% raise "Cannot use 'after_all' inside of a test block" if @def %} + # Defines a block of code that will be invoked once before any examples in the group. + # The block will not run in the context of the current running example. + # This means that values defined by `let` and `subject` are not available. + define_example_group_hook :before_all - def self.%hook : Nil - {{block.body}} - end + # Defines a block of code that will be invoked once after all examples in the group. + # The block will not run in the context of the current running example. + # This means that values defined by `let` and `subject` are not available. + define_example_group_hook :after_all - ::Spectator::DSL::Builder.after_all( - ::Spectator::Source.new({{block.filename}}, {{block.line_number}}) - ) { {{@type.name}}.%hook } - end + # Defines a block of code that will be invoked before every example in the group. + # The block will be run in the context of the current running example. + # This means that values defined by `let` and `subject` are available. + define_example_hook :before_each - macro after_each(&block) - {% raise "Cannot use 'after_each' inside of a test block" if @def %} - - def %hook : Nil - {{block.body}} - end - - ::Spectator::DSL::Builder.after_each( - ::Spectator::Source.new({{block.filename}}, {{block.line_number}}) - ) { |example| example.with_context({{@type.name}}) { %hook } } - end + # Defines a block of code that will be invoked after every example in the group. + # The block will be run in the context of the current running example. + # This means that values defined by `let` and `subject` are available. + define_example_hook :after_each end end diff --git a/src/spectator/dsl/top.cr b/src/spectator/dsl/top.cr index d6682b6..467b415 100644 --- a/src/spectator/dsl/top.cr +++ b/src/spectator/dsl/top.cr @@ -16,9 +16,7 @@ module Spectator::DSL # # Your examples for `Foo` go here. # end # ``` - # NOTE: Inside the block, the `Spectator` prefix is no longer needed. - # Actually, prefixing methods and macros with `Spectator` - # most likely won't work and can cause compiler errors. + # NOTE: Inside the block, the `Spectator` prefix _should not_ be used. macro {{method.id}}(description, &block) class ::SpectatorTestContext {{method.id}}(\{{description}}) \{{block}} From 7451769a2967c7e263ab7a9417a0c134b6eb1686 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Jan 2021 13:34:15 -0700 Subject: [PATCH 094/399] Pass current example as block argument --- src/spectator/dsl/examples.cr | 13 +++++++++++-- src/spectator/dsl/hooks.cr | 13 +++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 91d196a..c8c9d8c 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -24,8 +24,9 @@ module Spectator::DSL macro {{name.id}}(what = nil, &block) \{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %} \{% raise "A description or block must be provided. Cannot use '{{name.id}}' alone." unless what || block %} + \{% raise "Block argument count '{{name.id}}' hook must be 0..1" if block.args.size > 1 %} - def \%test : Nil # TODO: Pass example instance. + def \%test(\{{block.args.splat}}) : Nil \{{block.body}} end @@ -33,7 +34,15 @@ module Spectator::DSL _spectator_example_name(\{{what}}), ::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}}), \{{@type.name}}.new.as(::Spectator::Context) - ) { |example| example.with_context(\{{@type.name}}) { \%test } } + ) do |example| + example.with_context(\{{@type.name}}) do + \{% if block.args.empty? %} + \%test + \{% else %} + \%test(example) + \{% end %} + end + end end end diff --git a/src/spectator/dsl/hooks.cr b/src/spectator/dsl/hooks.cr index e84df19..a14ee9e 100644 --- a/src/spectator/dsl/hooks.cr +++ b/src/spectator/dsl/hooks.cr @@ -26,15 +26,24 @@ module Spectator::DSL macro define_example_hook(type) macro {{type.id}}(&block) \{% raise "Missing block for '{{type.id}}' hook" unless block %} + \{% raise "Block argument count '{{type.id}}' hook must be 0..1" if block.args.size > 1 %} \{% raise "Cannot use '{{type.id}}' inside of a test block" if @def %} - def \%hook : Nil # TODO: Pass example instance. + def \%hook(\{{block.args.splat}}) : Nil \{{block.body}} end ::Spectator::DSL::Builder.{{type.id}}( ::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}}) - ) { |example| example.with_context(\{{@type.name}}) { \%hook } } + ) do |example| + example.with_context(\{{@type.name}}) do + \{% if block.args.empty? %} + \%hook + \{% else %} + \%hook(example) + \{% end %} + end + end end end From fb0423ed02fc343b8756b3cd847cb259522687d2 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Jan 2021 13:56:32 -0700 Subject: [PATCH 095/399] Move top-level types into spectator/ sub-directory --- src/spectator.cr | 1 - src/spectator/context.cr | 8 +++++++- src/spectator/includes.cr | 1 + .../test_context.cr} | 11 ++++++----- src/spectator_context.cr | 7 ------- 5 files changed, 14 insertions(+), 14 deletions(-) rename src/{spectator_test_context.cr => spectator/test_context.cr} (65%) delete mode 100644 src/spectator_context.cr diff --git a/src/spectator.cr b/src/spectator.cr index 4f29bc8..a27367c 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -1,6 +1,5 @@ require "log" require "./spectator/includes" -require "./spectator_test_context" Log.setup_from_env diff --git a/src/spectator/context.cr b/src/spectator/context.cr index 9a4607e..c0a12db 100644 --- a/src/spectator/context.cr +++ b/src/spectator/context.cr @@ -1,4 +1,10 @@ -require "../spectator_context" +# Base class that all test cases run in. +# This type is used to store all test case contexts as a single type. +# The instance must be downcast to the correct type before calling a context method. +# This type is intentionally outside the `Spectator` module. +# The reason for this is to prevent name collision when using the DSL to define a spec. +abstract class SpectatorContext +end module Spectator # Base class that all test cases run in. diff --git a/src/spectator/includes.cr b/src/spectator/includes.cr index 9b270a8..5fa8f02 100644 --- a/src/spectator/includes.cr +++ b/src/spectator/includes.cr @@ -8,3 +8,4 @@ require "./command_line_arguments_config_source" require "./config_builder" require "./config" require "./dsl" +require "./test_context" diff --git a/src/spectator_test_context.cr b/src/spectator/test_context.cr similarity index 65% rename from src/spectator_test_context.cr rename to src/spectator/test_context.cr index 0e8fb2b..83905cf 100644 --- a/src/spectator_test_context.cr +++ b/src/spectator/test_context.cr @@ -1,6 +1,10 @@ -require "./spectator_context" -require "./spectator/dsl" +require "./context" +require "./dsl" +# Class used as the base for all specs using the DSL. +# It adds methods and macros necessary to use the DSL from the spec. +# This type is intentionally outside the `Spectator` module. +# The reason for this is to prevent name collision when using the DSL to define a spec. class SpectatorTestContext < SpectatorContext include ::Spectator::DSL::Examples include ::Spectator::DSL::Groups @@ -19,7 +23,4 @@ class SpectatorTestContext < SpectatorContext private def subject _spectator_implicit_subject end - - # def initialize(@spectator_test_values : ::Spectator::TestValues) - # end end diff --git a/src/spectator_context.cr b/src/spectator_context.cr deleted file mode 100644 index 36ff503..0000000 --- a/src/spectator_context.cr +++ /dev/null @@ -1,7 +0,0 @@ -# Base class that all test cases run in. -# This type is used to store all test case contexts as a single type. -# The instance must be downcast to the correct type before calling a context method. -# This type is intentionally outside the `Spectator` module. -# The reason for this is to prevent name collision when using the DSL to define a spec. -abstract class SpectatorContext -end From 5cac4aa5a184c25fe0f3bf20cabae75ddc0542fb Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Jan 2021 14:19:40 -0700 Subject: [PATCH 096/399] Add lazy utility --- src/spectator/block.cr | 13 +++---------- src/spectator/lazy.cr | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 10 deletions(-) create mode 100644 src/spectator/lazy.cr diff --git a/src/spectator/block.cr b/src/spectator/block.cr index 15553dd..312681a 100644 --- a/src/spectator/block.cr +++ b/src/spectator/block.cr @@ -1,6 +1,6 @@ require "./abstract_expression" require "./label" -require "./wrapper" +require "./lazy" module Spectator # Represents a block from a test. @@ -10,8 +10,7 @@ module Spectator # or nil if one isn't available. class Block(T) < AbstractExpression # Cached value returned from the block. - # Is nil if the block hasn't been called. - @wrapper : Wrapper(T)? + @value = Lazy(T).new # Creates the block expression from a proc. # The *proc* will be called to evaluate the value of the expression. @@ -34,13 +33,7 @@ module Spectator # The block is lazily evaluated and the value retrieved only once. # Afterwards, the value is cached and returned by successive calls to this method. def value - if (wrapper = @wrapper) - wrapper.value - else - call.tap do |value| - @wrapper = Wrapper.new(value) - end - end + @value.get { call } end # Evaluates the block and returns the value from it. diff --git a/src/spectator/lazy.cr b/src/spectator/lazy.cr new file mode 100644 index 0000000..afeee5f --- /dev/null +++ b/src/spectator/lazy.cr @@ -0,0 +1,22 @@ +require "./wrapper" + +module Spectator + # Lazily stores a value. + struct Lazy(T) + @wrapper : Wrapper(T)? + + # Retrieves the value, if it was previously fetched. + # On the first invocation of this method, it will yield. + # The block should return the value to store. + # Subsequent calls will return the same value and not yield. + def get(&block : -> T) + if (wrapper = @wrapper) + wrapper.value + else + yield.tap do |value| + @wrapper = Wrapper.new(value) + end + end + end + end +end From aa4c257adec0e2c4374d04bd6ed303c52e46476f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Jan 2021 14:35:58 -0700 Subject: [PATCH 097/399] Change Wrapper to a nested type for Lazy --- src/spectator/lazy.cr | 24 ++++++++++++++++++------ src/spectator/wrapper.cr | 22 ---------------------- 2 files changed, 18 insertions(+), 28 deletions(-) delete mode 100644 src/spectator/wrapper.cr diff --git a/src/spectator/lazy.cr b/src/spectator/lazy.cr index afeee5f..25aaed5 100644 --- a/src/spectator/lazy.cr +++ b/src/spectator/lazy.cr @@ -1,22 +1,34 @@ -require "./wrapper" - module Spectator # Lazily stores a value. struct Lazy(T) - @wrapper : Wrapper(T)? + @value : Value(T)? # Retrieves the value, if it was previously fetched. # On the first invocation of this method, it will yield. # The block should return the value to store. # Subsequent calls will return the same value and not yield. def get(&block : -> T) - if (wrapper = @wrapper) - wrapper.value + if (value = @value) + value.get else yield.tap do |value| - @wrapper = Wrapper.new(value) + @value = Value.new(value) end end end + + # Wrapper for a value. + # This is intended to be used as a union with nil. + # It allows storing (caching) a nillable value. + private struct Value(T) + # Creates the wrapper. + def initialize(@value : T) + end + + # Retrieves the value. + def get : T + @value + end + end end end diff --git a/src/spectator/wrapper.cr b/src/spectator/wrapper.cr deleted file mode 100644 index 063cefa..0000000 --- a/src/spectator/wrapper.cr +++ /dev/null @@ -1,22 +0,0 @@ -module Spectator - # Wrapper for a value. - # This is intended to be used as a union with nil. - # It allows storing (caching) a nillable value. - # ``` - # if (wrapper = @wrapper) - # wrapper.value - # else - # value = 42 - # @wrapper = Wrapper.new(value) - # value - # end - # ``` - struct Wrapper(T) - # Original value. - getter value : T - - # Creates the wrapper. - def initialize(@value : T) - end - end -end From 3e4b77da776ec2438dcb911827d4e2f73553533f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Jan 2021 16:51:37 -0700 Subject: [PATCH 098/399] Create value wrapper --- src/spectator/lazy_wrapper.cr | 19 ++++++++++++++++ src/spectator/wrapper.cr | 41 +++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/spectator/lazy_wrapper.cr create mode 100644 src/spectator/wrapper.cr diff --git a/src/spectator/lazy_wrapper.cr b/src/spectator/lazy_wrapper.cr new file mode 100644 index 0000000..438e58b --- /dev/null +++ b/src/spectator/lazy_wrapper.cr @@ -0,0 +1,19 @@ +require "./lazy" +require "./wrapper" + +module Spectator + # Lazily stores a value of any type. + # Combines `Lazy` and `Wrapper`. + struct LazyWrapper + @lazy = Lazy(Wrapper).new + + # Retrieves the value, if it was previously fetched. + # On the first invocation of this method, it will yield. + # The block should return the value to store. + # Subsequent calls will return the same value and not yield. + def get(type : T.class, &block : -> T) : T forall T + wrapper = @lazy.get { Wrapper.new(yield) } + wrapper.get(type) + end + end +end diff --git a/src/spectator/wrapper.cr b/src/spectator/wrapper.cr new file mode 100644 index 0000000..efee1b6 --- /dev/null +++ b/src/spectator/wrapper.cr @@ -0,0 +1,41 @@ +module Spectator + # Typeless wrapper for a value. + # Stores any value or reference type. + # However, the type must be known when retrieving the value. + struct Wrapper + @value : TypelessValue + + # Creates a wrapper for the specified value. + def initialize(value) + @value = Value.new(value) + end + + # Retrieves the previously wrapped value. + # The *type* of the wrapped value must match otherwise an error will be raised. + def get(type : T.class) : T forall T + value = @value.as(Value(T)) + value.get + end + + # Base type that generic types inherit from. + # This provides a common base type, + # since Crystal doesn't support storing an `Object` (yet). + # Instances of this type must be downcast to `Value` to be useful. + private abstract class TypelessValue + end + + # Generic value wrapper. + # Simply holds a value and inherits from `TypelessValue`, + # so that all types of this class can be stored as one. + private class Value(T) < TypelessValue + # Creates the wrapper with the specified value. + def initialize(@value : T) + end + + # Retrieves the wrapped value. + def get : T + @value + end + end + end +end From 4108a6602d8542b83ef9e4d6a0c3e56366fd3ff3 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Jan 2021 16:57:23 -0700 Subject: [PATCH 099/399] Simplify get method --- src/spectator/lazy_wrapper.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/lazy_wrapper.cr b/src/spectator/lazy_wrapper.cr index 438e58b..ea177f9 100644 --- a/src/spectator/lazy_wrapper.cr +++ b/src/spectator/lazy_wrapper.cr @@ -11,9 +11,9 @@ module Spectator # On the first invocation of this method, it will yield. # The block should return the value to store. # Subsequent calls will return the same value and not yield. - def get(type : T.class, &block : -> T) : T forall T + def get(&block : -> T) : T forall T wrapper = @lazy.get { Wrapper.new(yield) } - wrapper.get(type) + wrapper.get(T) end end end From 391325d431d312c31ef7a47c64f853b8498cc6a3 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Jan 2021 17:01:33 -0700 Subject: [PATCH 100/399] Initial work on values DSL --- src/spectator/dsl.cr | 1 + src/spectator/dsl/values.cr | 22 ++++++++-------------- src/spectator/test_context.cr | 1 + 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/spectator/dsl.cr b/src/spectator/dsl.cr index e76bd7f..46c7308 100644 --- a/src/spectator/dsl.cr +++ b/src/spectator/dsl.cr @@ -4,6 +4,7 @@ require "./dsl/examples" require "./dsl/groups" require "./dsl/hooks" require "./dsl/top" +require "./dsl/values" module Spectator # Namespace containing methods representing the spec domain specific language. diff --git a/src/spectator/dsl/values.cr b/src/spectator/dsl/values.cr index e4c41df..0e0aa7f 100644 --- a/src/spectator/dsl/values.cr +++ b/src/spectator/dsl/values.cr @@ -1,24 +1,18 @@ +require "../lazy_wrapper" + module Spectator::DSL + # DSL methods for defining test values (subjects). module Values - end + macro let(name, &block) + {% raise "Block required for 'let'" unless block %} - macro let(name, &block) - @%wrapper : ::Spectator::ValueWrapper? + @%value = ::Spectator::LazyWrapper.new def {{name.id}} - {{block.body}} - end - - def {{name.id}} - if (wrapper = @%wrapper) - wrapper.as(::Spectator::TypedValueWrapper(typeof(previous_def))).value - else - previous_def.tap do |value| - @%wrapper = ::Spectator::TypedValueWrapper.new(value) - end - end + @%value.get {{block}} end end + end macro let!(name, &block) @%wrapper : ::Spectator::ValueWrapper? diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr index 83905cf..8616ba8 100644 --- a/src/spectator/test_context.cr +++ b/src/spectator/test_context.cr @@ -9,6 +9,7 @@ class SpectatorTestContext < SpectatorContext include ::Spectator::DSL::Examples include ::Spectator::DSL::Groups include ::Spectator::DSL::Hooks + include ::Spectator::DSL::Values # Initial implicit subject for tests. # This method should be overridden by example groups when an object is described. From 122395837fbbb6851e06d20c5747c54908c688fd Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Jan 2021 17:36:50 -0700 Subject: [PATCH 101/399] Implement remaining value DSL macros --- src/spectator/dsl/values.cr | 91 +++++++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 33 deletions(-) diff --git a/src/spectator/dsl/values.cr b/src/spectator/dsl/values.cr index 0e0aa7f..210a9c1 100644 --- a/src/spectator/dsl/values.cr +++ b/src/spectator/dsl/values.cr @@ -3,8 +3,13 @@ require "../lazy_wrapper" module Spectator::DSL # DSL methods for defining test values (subjects). module Values + # Defines a memoized getter. + # The *name* is the name of the getter method. + # The block is evaluated only on the first time the getter is used + # and the return value is saved for subsequent calls. macro let(name, &block) {% raise "Block required for 'let'" unless block %} + {% raise "Cannot use 'let' inside of a test block" if @def %} @%value = ::Spectator::LazyWrapper.new @@ -12,49 +17,69 @@ module Spectator::DSL @%value.get {{block}} end end - end - macro let!(name, &block) - @%wrapper : ::Spectator::ValueWrapper? + # Defines a memoized getter. + # The *name* is the name of the getter method. + # The block is evaluated once before the example runs + # and the return value is saved. + macro let!(name, &block) + {% raise "Block required for 'let!'" unless block %} + {% raise "Cannot use 'let!' inside of a test block" if @def %} - def %wrapper - {{block.body}} - end - - before_each do - @%wrapper = ::Spectator::TypedValueWrapper.new(%wrapper) - end - - def {{name.id}} - @%wrapper.as(::Spectator::TypedValueWrapper(typeof(%wrapper))).value - end + let({{name}}) {{block}} + before_each { {{name.id}} } end - macro subject(&block) - {% if block.is_a?(Nop) %} - self.subject - {% else %} - let(:subject) {{block}} + # Explicitly defines the subject of the tests. + # Creates a memoized getter for the subject. + # The block is evaluated only the first time the subject is referenced + # and the return value is saved for subsequent calls. + macro subject(&block) + {% raise "Block required for 'subject'" unless block %} + {% raise "Cannot use 'subject' inside of a test block" if @def %} + + let(subject) {{block}} + end + + # Explicitly defines the subject of the tests. + # Creates a memoized getter for the subject. + # The subject can be referenced by using `subject` or *name*. + # The block is evaluated only the first time the subject is referenced + # and the return value is saved for subsequent calls. + macro subject(name, &block) + subject {{block}} + + {% if name.id != :subject.id %} + def {{name.id}} + subject + end {% end %} end - macro subject(name, &block) - let({{name.id}}) {{block}} + # Explicitly defines the subject of the tests. + # Creates a memoized getter for the subject. + # The block is evaluated once before the example runs + # and the return value is saved for subsequent calls. + macro subject!(&block) + {% raise "Block required for 'subject!'" unless block %} + {% raise "Cannot use 'subject!' inside of a test block" if @def %} - def subject - {{name.id}} - end + let!(subject) {{block}} end - macro subject!(&block) - let!(:subject) {{block}} - end + # Explicitly defines the subject of the tests. + # Creates a memoized getter for the subject. + # The subject can be referenced by using `subject` or *name*. + # The block is evaluated once before the example runs + # and the return value is saved for subsequent calls. + macro subject!(name, &block) + subject! {{block}} - macro subject!(name, &block) - let!({{name.id}}) {{block}} - - def subject - {{name.id}} - end + {% if name.id != :subject.id %} + def {{name.id}} + subject + end + {% end %} end + end end From 096c31d7f5aca6fbfa424cd05cac5fd05a8b8725 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Jan 2021 19:50:32 -0700 Subject: [PATCH 102/399] Initial work on assertions --- src/spectator/assertion.cr | 15 + src/spectator/assertion_failed.cr | 15 + src/spectator/dsl.cr | 1 + src/spectator/dsl/assertions.cr | 437 +++++++++++++++------------- src/spectator/example_failed.cr | 5 - src/spectator/expectation_failed.cr | 15 - src/spectator/test_context.cr | 1 + 7 files changed, 263 insertions(+), 226 deletions(-) create mode 100644 src/spectator/assertion.cr create mode 100644 src/spectator/assertion_failed.cr delete mode 100644 src/spectator/example_failed.cr delete mode 100644 src/spectator/expectation_failed.cr diff --git a/src/spectator/assertion.cr b/src/spectator/assertion.cr new file mode 100644 index 0000000..d2813ce --- /dev/null +++ b/src/spectator/assertion.cr @@ -0,0 +1,15 @@ +require "./block" +require "./expression" + +module Spectator + class Assertion + struct Target(T) + @expression : Expression(T) | Block(T) + @source : Source? + + def initialize(@expression : Expression(T) | Block(T), @source) + puts "TARGET: #{@expression} @ #{@source}" + end + end + end +end diff --git a/src/spectator/assertion_failed.cr b/src/spectator/assertion_failed.cr new file mode 100644 index 0000000..81b28d2 --- /dev/null +++ b/src/spectator/assertion_failed.cr @@ -0,0 +1,15 @@ +require "./source" + +module Spectator + # Exception that indicates an assertion failed. + # When raised within a test, the test should abort. + class AssertionFailed < Exception + # Location where the assertion failed and the exception raised. + getter source : Source + + # Creates the exception. + def initialize(@source : Source, message : String? = nil, cause : Exception? = nil) + super(message, cause) + end + end +end diff --git a/src/spectator/dsl.cr b/src/spectator/dsl.cr index 46c7308..e206d1b 100644 --- a/src/spectator/dsl.cr +++ b/src/spectator/dsl.cr @@ -1,4 +1,5 @@ # require "./dsl/*" +require "./dsl/assertions" require "./dsl/builder" require "./dsl/examples" require "./dsl/groups" diff --git a/src/spectator/dsl/assertions.cr b/src/spectator/dsl/assertions.cr index 11b5ffe..1c2ee93 100644 --- a/src/spectator/dsl/assertions.cr +++ b/src/spectator/dsl/assertions.cr @@ -1,216 +1,241 @@ -require "../expectations/expectation_partial" +require "../assertion" +require "../assertion_failed" +require "../expression" require "../source" -require "../test_block" -require "../test_value" -module Spectator - module DSL - # Starts an expectation. - # This should be followed up with `Spectator::Expectations::ExpectationPartial#to` - # or `Spectator::Expectations::ExpectationPartial#to_not`. - # The value passed in will be checked - # to see if it satisfies the conditions specified. - # - # This method should be used like so: - # ``` - # expect(actual).to eq(expected) - # ``` - # Where the actual value is returned by the system-under-test, - # and the expected value is what the actual value should be to satisfy the condition. - macro expect(actual, _source_file = __FILE__, _source_line = __LINE__) - %test_value = ::Spectator::TestValue.new({{actual}}, {{actual.stringify}}) - %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - ::Spectator::Expectations::ExpectationPartial.new(%test_value, %source) +module Spectator::DSL + # Methods and macros for asserting that conditions are met. + module Assertions + # Checks that the specified condition is true. + # Raises `AssertionFailed` if *condition* is false. + # The *message* is passed to the exception. + def assert(condition, message, *, _file = __FILE__, _line = __LINE__) + raise AssertionFailed.new(Source.new(_file, _line), message) unless condition end - # Starts an expectation on a block of code. - # This should be followed up with `Spectator::Expectations::ExpectationPartial#to` - # or `Spectator::Expectations::ExpectationPartial#to_not`. - # The block passed in, or its return value, will be checked - # to see if it satisfies the conditions specified. - # - # This method should be used like so: - # ``` - # expect { raise "foo" }.to raise_error - # ``` - # The block of code is passed along for validation to the matchers. - # - # The short, one argument syntax used for passing methods to blocks can be used. - # So instead of doing this: - # ``` - # expect(subject.size).to eq(5) - # ``` - # The following syntax can be used instead: - # ``` - # expect(&.size).to eq(5) - # ``` - # The method passed will always be evaluated on the subject. - macro expect(_source_file = __FILE__, _source_line = __LINE__, &block) - {% if block.is_a?(Nop) %} - {% raise "Argument or block must be provided to expect" %} - {% end %} - - # Check if the short-hand method syntax is used. - # This is a hack, since macros don't get this as a "literal" or something similar. - # The Crystal compiler will translate: - # ``` - # &.foo - # ``` - # to: - # ``` - # { |__arg0| __arg0.foo } - # ``` - # The hack used here is to check if it looks like a compiler-generated block. - {% if block.args.size == 1 && block.args[0] =~ /^__arg\d+$/ && block.body.is_a?(Call) && block.body.id =~ /^__arg\d+\./ %} - # Extract the method name to make it clear to the user what is tested. - # The raw block can't be used because it's not clear to the user. - {% method_name = block.body.id.split('.')[1..-1].join('.') %} - %proc = ->{ subject.{{method_name.id}} } - %test_block = ::Spectator::TestBlock.create(%proc, {{"#" + method_name}}) - {% elsif block.args.empty? %} - # In this case, it looks like the short-hand method syntax wasn't used. - # Capture the block as a proc and pass along. - %proc = ->{{block}} - %test_block = ::Spectator::TestBlock.create(%proc, {{"`" + block.body.stringify + "`"}}) - {% else %} - {% raise "Unexpected block arguments in expect call" %} - {% end %} - - %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - ::Spectator::Expectations::ExpectationPartial.new(%test_block, %source) + # Checks that the specified condition is true. + # Raises `AssertionFailed` if *condition* is false. + # The message of the exception is the *condition*. + macro assert(condition) + assert({{condition}}, {{condition.stringify}}, _file: {{condition.filename}}, _line: {{condition.line_number}}) end - # Starts an expectation. - # This should be followed up with `Spectator::Expectations::ExpectationPartial#to` - # or `Spectator::Expectations::ExpectationPartial#to_not`. - # The value passed in will be checked - # to see if it satisfies the conditions specified. - # - # This method is identical to `#expect`, - # but is grammatically correct for the one-liner syntax. - # It can be used like so: - # ``` - # it expects(actual).to eq(expected) - # ``` - # Where the actual value is returned by the system-under-test, - # and the expected value is what the actual value should be to satisfy the condition. - macro expects(actual) - expect({{actual}}) - end + macro expect(actual) + %actual = begin + {{actual}} + end - # Starts an expectation on a block of code. - # This should be followed up with `Spectator::Expectations::ExpectationPartial#to` - # or `Spectator::Expectations::ExpectationPartial#to_not`. - # The block passed in, or its return value, will be checked - # to see if it satisfies the conditions specified. - # - # This method is identical to `#expect`, - # but is grammatically correct for the one-liner syntax. - # It can be used like so: - # ``` - # it expects { 5 / 0 }.to raise_error - # ``` - # The block of code is passed along for validation to the matchers. - # - # The short, one argument syntax used for passing methods to blocks can be used. - # So instead of doing this: - # ``` - # it expects(subject.size).to eq(5) - # ``` - # The following syntax can be used instead: - # ``` - # it expects(&.size).to eq(5) - # ``` - # The method passed will always be evaluated on the subject. - macro expects(&block) - expect {{block}} - end - - # Short-hand for expecting something of the subject. - # These two are functionally equivalent: - # ``` - # expect(subject).to eq("foo") - # is_expected.to eq("foo") - # ``` - macro is_expected - expect(subject) - end - - # Short-hand form of `#is_expected` that can be used for one-liner syntax. - # For instance: - # ``` - # it "is 42" do - # expect(subject).to eq(42) - # end - # ``` - # Can be shortened to: - # ``` - # it is(42) - # ``` - # - # These three are functionally equivalent: - # ``` - # expect(subject).to eq("foo") - # is_expected.to eq("foo") - # is("foo") - # ``` - # - # See also: `#is_not` - macro is(expected) - is_expected.to eq({{expected}}) - end - - # Short-hand, negated form of `#is_expected` that can be used for one-liner syntax. - # For instance: - # ``` - # it "is not 42" do - # expect(subject).to_not eq(42) - # end - # ``` - # Can be shortened to: - # ``` - # it is_not(42) - # ``` - # - # These three are functionally equivalent: - # ``` - # expect(subject).to_not eq("foo") - # is_expected.to_not eq("foo") - # is_not("foo") - # ``` - # - # See also: `#is` - macro is_not(expected) - is_expected.to_not eq({{expected}}) - end - - macro should(matcher) - is_expected.to({{matcher}}) - end - - macro should_not(matcher) - is_expected.to_not({{matcher}}) - end - - macro should_eventually(matcher) - is_expected.to_eventually({{matcher}}) - end - - macro should_never(matcher) - is_expected.to_never({{matcher}}) - end - - # Immediately fail the current test. - # A reason can be passed, - # which is reported in the output. - def fail(reason : String) - raise ExampleFailed.new(reason) - end - - # :ditto: - @[AlwaysInline] - def fail - fail("Example failed") + %expression = ::Spectator::Expression.new(%actual, {{actual.stringify}}) + %source = ::Spectator::Source.new({{actual.filename}}, {{actual.line_number}}) + ::Spectator::Assertion::Target.new(%expression, %source) end end + + # Starts an expectation. + # This should be followed up with `Spectator::Expectations::ExpectationPartial#to` + # or `Spectator::Expectations::ExpectationPartial#to_not`. + # The value passed in will be checked + # to see if it satisfies the conditions specified. + # + # This method should be used like so: + # ``` + # expect(actual).to eq(expected) + # ``` + # Where the actual value is returned by the system-under-test, + # and the expected value is what the actual value should be to satisfy the condition. + macro expect(actual, _source_file = __FILE__, _source_line = __LINE__) + %test_value = ::Spectator::TestValue.new({{actual}}, {{actual.stringify}}) + %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) + ::Spectator::Expectations::ExpectationPartial.new(%test_value, %source) + end + + # Starts an expectation on a block of code. + # This should be followed up with `Spectator::Expectations::ExpectationPartial#to` + # or `Spectator::Expectations::ExpectationPartial#to_not`. + # The block passed in, or its return value, will be checked + # to see if it satisfies the conditions specified. + # + # This method should be used like so: + # ``` + # expect { raise "foo" }.to raise_error + # ``` + # The block of code is passed along for validation to the matchers. + # + # The short, one argument syntax used for passing methods to blocks can be used. + # So instead of doing this: + # ``` + # expect(subject.size).to eq(5) + # ``` + # The following syntax can be used instead: + # ``` + # expect(&.size).to eq(5) + # ``` + # The method passed will always be evaluated on the subject. + macro expect(_source_file = __FILE__, _source_line = __LINE__, &block) + {% if block.is_a?(Nop) %} + {% raise "Argument or block must be provided to expect" %} + {% end %} + + # Check if the short-hand method syntax is used. + # This is a hack, since macros don't get this as a "literal" or something similar. + # The Crystal compiler will translate: + # ``` + # &.foo + # ``` + # to: + # ``` + # { |__arg0| __arg0.foo } + # ``` + # The hack used here is to check if it looks like a compiler-generated block. + {% if block.args.size == 1 && block.args[0] =~ /^__arg\d+$/ && block.body.is_a?(Call) && block.body.id =~ /^__arg\d+\./ %} + # Extract the method name to make it clear to the user what is tested. + # The raw block can't be used because it's not clear to the user. + {% method_name = block.body.id.split('.')[1..-1].join('.') %} + %proc = ->{ subject.{{method_name.id}} } + %test_block = ::Spectator::TestBlock.create(%proc, {{"#" + method_name}}) + {% elsif block.args.empty? %} + # In this case, it looks like the short-hand method syntax wasn't used. + # Capture the block as a proc and pass along. + %proc = ->{{block}} + %test_block = ::Spectator::TestBlock.create(%proc, {{"`" + block.body.stringify + "`"}}) + {% else %} + {% raise "Unexpected block arguments in expect call" %} + {% end %} + + %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) + ::Spectator::Expectations::ExpectationPartial.new(%test_block, %source) + end + + # Starts an expectation. + # This should be followed up with `Spectator::Expectations::ExpectationPartial#to` + # or `Spectator::Expectations::ExpectationPartial#to_not`. + # The value passed in will be checked + # to see if it satisfies the conditions specified. + # + # This method is identical to `#expect`, + # but is grammatically correct for the one-liner syntax. + # It can be used like so: + # ``` + # it expects(actual).to eq(expected) + # ``` + # Where the actual value is returned by the system-under-test, + # and the expected value is what the actual value should be to satisfy the condition. + macro expects(actual) + expect({{actual}}) + end + + # Starts an expectation on a block of code. + # This should be followed up with `Spectator::Expectations::ExpectationPartial#to` + # or `Spectator::Expectations::ExpectationPartial#to_not`. + # The block passed in, or its return value, will be checked + # to see if it satisfies the conditions specified. + # + # This method is identical to `#expect`, + # but is grammatically correct for the one-liner syntax. + # It can be used like so: + # ``` + # it expects { 5 / 0 }.to raise_error + # ``` + # The block of code is passed along for validation to the matchers. + # + # The short, one argument syntax used for passing methods to blocks can be used. + # So instead of doing this: + # ``` + # it expects(subject.size).to eq(5) + # ``` + # The following syntax can be used instead: + # ``` + # it expects(&.size).to eq(5) + # ``` + # The method passed will always be evaluated on the subject. + macro expects(&block) + expect {{block}} + end + + # Short-hand for expecting something of the subject. + # These two are functionally equivalent: + # ``` + # expect(subject).to eq("foo") + # is_expected.to eq("foo") + # ``` + macro is_expected + expect(subject) + end + + # Short-hand form of `#is_expected` that can be used for one-liner syntax. + # For instance: + # ``` + # it "is 42" do + # expect(subject).to eq(42) + # end + # ``` + # Can be shortened to: + # ``` + # it is(42) + # ``` + # + # These three are functionally equivalent: + # ``` + # expect(subject).to eq("foo") + # is_expected.to eq("foo") + # is("foo") + # ``` + # + # See also: `#is_not` + macro is(expected) + is_expected.to eq({{expected}}) + end + + # Short-hand, negated form of `#is_expected` that can be used for one-liner syntax. + # For instance: + # ``` + # it "is not 42" do + # expect(subject).to_not eq(42) + # end + # ``` + # Can be shortened to: + # ``` + # it is_not(42) + # ``` + # + # These three are functionally equivalent: + # ``` + # expect(subject).to_not eq("foo") + # is_expected.to_not eq("foo") + # is_not("foo") + # ``` + # + # See also: `#is` + macro is_not(expected) + is_expected.to_not eq({{expected}}) + end + + macro should(matcher) + is_expected.to({{matcher}}) + end + + macro should_not(matcher) + is_expected.to_not({{matcher}}) + end + + macro should_eventually(matcher) + is_expected.to_eventually({{matcher}}) + end + + macro should_never(matcher) + is_expected.to_never({{matcher}}) + end + + # Immediately fail the current test. + # A reason can be passed, + # which is reported in the output. + def fail(reason : String) + raise ExampleFailed.new(reason) + end + + # :ditto: + @[AlwaysInline] + def fail + fail("Example failed") + end end diff --git a/src/spectator/example_failed.cr b/src/spectator/example_failed.cr deleted file mode 100644 index a1b74f7..0000000 --- a/src/spectator/example_failed.cr +++ /dev/null @@ -1,5 +0,0 @@ -module Spectator - # Exception that indicates an example failed and should abort. - class ExampleFailed < Exception - end -end diff --git a/src/spectator/expectation_failed.cr b/src/spectator/expectation_failed.cr deleted file mode 100644 index 05246da..0000000 --- a/src/spectator/expectation_failed.cr +++ /dev/null @@ -1,15 +0,0 @@ -require "./example_failed" - -module Spectator - # Exception that indicates a required expectation was not met in an example. - class ExpectationFailed < ExampleFailed - # Expectation that failed. - getter expectation : Expectations::Expectation - - # Creates the exception. - # The exception string is generated from the expecation message. - def initialize(@expectation) - super(@expectation.failure_message) - end - end -end diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr index 8616ba8..c6ca933 100644 --- a/src/spectator/test_context.cr +++ b/src/spectator/test_context.cr @@ -6,6 +6,7 @@ require "./dsl" # This type is intentionally outside the `Spectator` module. # The reason for this is to prevent name collision when using the DSL to define a spec. class SpectatorTestContext < SpectatorContext + include ::Spectator::DSL::Assertions include ::Spectator::DSL::Examples include ::Spectator::DSL::Groups include ::Spectator::DSL::Hooks From 4ed8c4a573ca18ce99eab1596298822c5bfcea01 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 10 Jan 2021 11:09:28 -0700 Subject: [PATCH 103/399] Implement remaining assertion macros Move "should" methods. --- src/spectator/dsl/assertions.cr | 326 +++++++++++++------------------- src/spectator/should.cr | 18 ++ 2 files changed, 147 insertions(+), 197 deletions(-) diff --git a/src/spectator/dsl/assertions.cr b/src/spectator/dsl/assertions.cr index 1c2ee93..791ad73 100644 --- a/src/spectator/dsl/assertions.cr +++ b/src/spectator/dsl/assertions.cr @@ -6,20 +6,45 @@ require "../source" module Spectator::DSL # Methods and macros for asserting that conditions are met. module Assertions + # Immediately fail the current test. + # A reason can be specified with *message*. + def fail(message = "Example failed", *, _file = __FILE__, _line = __LINE__) + raise AssertionFailed.new(Source.new(_file, _line), message) + end + # Checks that the specified condition is true. # Raises `AssertionFailed` if *condition* is false. # The *message* is passed to the exception. + # + # ``` + # assert(value == 42, "That's not the answer to everything.") + # ``` def assert(condition, message, *, _file = __FILE__, _line = __LINE__) - raise AssertionFailed.new(Source.new(_file, _line), message) unless condition + fail(message, _file: _file, _line: _line) unless condition end # Checks that the specified condition is true. # Raises `AssertionFailed` if *condition* is false. # The message of the exception is the *condition*. + # + # ``` + # assert(value == 42) + # ``` macro assert(condition) assert({{condition}}, {{condition.stringify}}, _file: {{condition.filename}}, _line: {{condition.line_number}}) end + # Starts an expectation. + # This should be followed up with `Assertion::Target#to` or `Assertion::Target#to_not`. + # The value passed in will be checked to see if it satisfies the conditions of the specified matcher. + # + # This macro should be used like so: + # ``` + # expect(actual).to eq(expected) + # ``` + # + # Where the actual value is returned by the system under test, + # and the expected value is what the actual value should be to satisfy the condition. macro expect(actual) %actual = begin {{actual}} @@ -29,213 +54,120 @@ module Spectator::DSL %source = ::Spectator::Source.new({{actual.filename}}, {{actual.line_number}}) ::Spectator::Assertion::Target.new(%expression, %source) end - end - # Starts an expectation. - # This should be followed up with `Spectator::Expectations::ExpectationPartial#to` - # or `Spectator::Expectations::ExpectationPartial#to_not`. - # The value passed in will be checked - # to see if it satisfies the conditions specified. - # - # This method should be used like so: - # ``` - # expect(actual).to eq(expected) - # ``` - # Where the actual value is returned by the system-under-test, - # and the expected value is what the actual value should be to satisfy the condition. - macro expect(actual, _source_file = __FILE__, _source_line = __LINE__) - %test_value = ::Spectator::TestValue.new({{actual}}, {{actual.stringify}}) - %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - ::Spectator::Expectations::ExpectationPartial.new(%test_value, %source) - end - - # Starts an expectation on a block of code. - # This should be followed up with `Spectator::Expectations::ExpectationPartial#to` - # or `Spectator::Expectations::ExpectationPartial#to_not`. - # The block passed in, or its return value, will be checked - # to see if it satisfies the conditions specified. - # - # This method should be used like so: - # ``` - # expect { raise "foo" }.to raise_error - # ``` - # The block of code is passed along for validation to the matchers. - # - # The short, one argument syntax used for passing methods to blocks can be used. - # So instead of doing this: - # ``` - # expect(subject.size).to eq(5) - # ``` - # The following syntax can be used instead: - # ``` - # expect(&.size).to eq(5) - # ``` - # The method passed will always be evaluated on the subject. - macro expect(_source_file = __FILE__, _source_line = __LINE__, &block) - {% if block.is_a?(Nop) %} - {% raise "Argument or block must be provided to expect" %} - {% end %} - - # Check if the short-hand method syntax is used. - # This is a hack, since macros don't get this as a "literal" or something similar. + # Starts an expectation. + # This should be followed up with `Assertion::Target#to` or `Assertion::Target#not_to`. + # The value passed in will be checked to see if it satisfies the conditions of the specified matcher. + # + # This macro should be used like so: + # ``` + # expect { raise "foo" }.to raise_error + # ``` + # + # The block of code is passed along for validation to the matchers. + # + # The short, one argument syntax used for passing methods to blocks can be used. + # So instead of doing this: + # ``` + # expect(subject.size).to eq(5) + # ``` + # + # The following syntax can be used instead: + # ``` + # expect(&.size).to eq(5) + # ``` + # + # The method passed will always be evaluated on the subject. + # + # TECHNICAL NOTE: + # This macro uses an ugly hack to detect the short-hand syntax. + # # The Crystal compiler will translate: # ``` # &.foo # ``` - # to: + # + # effectively to: # ``` # { |__arg0| __arg0.foo } # ``` - # The hack used here is to check if it looks like a compiler-generated block. - {% if block.args.size == 1 && block.args[0] =~ /^__arg\d+$/ && block.body.is_a?(Call) && block.body.id =~ /^__arg\d+\./ %} - # Extract the method name to make it clear to the user what is tested. - # The raw block can't be used because it's not clear to the user. - {% method_name = block.body.id.split('.')[1..-1].join('.') %} - %proc = ->{ subject.{{method_name.id}} } - %test_block = ::Spectator::TestBlock.create(%proc, {{"#" + method_name}}) - {% elsif block.args.empty? %} - # In this case, it looks like the short-hand method syntax wasn't used. - # Capture the block as a proc and pass along. - %proc = ->{{block}} - %test_block = ::Spectator::TestBlock.create(%proc, {{"`" + block.body.stringify + "`"}}) - {% else %} - {% raise "Unexpected block arguments in expect call" %} - {% end %} + macro expect(&block) + {% if block.args.size == 1 && block.args[0] =~ /^__arg\d+$/ && block.body.is_a?(Call) && block.body.id =~ /^__arg\d+\./ %} + {% method_name = block.body.id.split('.')[1..-1].join('.') %} + %block = ::Spectator::Block.new({{"#" + method_name}}) do + subject.{{method_name.id}} + end + {% elsif block.args.empty? %} + %block = ::Spectator::Block.new({{"`" + block.body.stringify + "`"}}) {{block}} + {% else %} + {% raise "Unexpected block arguments in 'expect' call" %} + {% end %} - %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - ::Spectator::Expectations::ExpectationPartial.new(%test_block, %source) - end + %source = ::Spectator::Source.new({{block.filename}}, {{block.line_number}}) + ::Spectator::Assertion::Target.new(%block, %source) + end - # Starts an expectation. - # This should be followed up with `Spectator::Expectations::ExpectationPartial#to` - # or `Spectator::Expectations::ExpectationPartial#to_not`. - # The value passed in will be checked - # to see if it satisfies the conditions specified. - # - # This method is identical to `#expect`, - # but is grammatically correct for the one-liner syntax. - # It can be used like so: - # ``` - # it expects(actual).to eq(expected) - # ``` - # Where the actual value is returned by the system-under-test, - # and the expected value is what the actual value should be to satisfy the condition. - macro expects(actual) - expect({{actual}}) - end + # Short-hand for expecting something of the subject. + # + # These two are functionally equivalent: + # ``` + # expect(subject).to eq("foo") + # is_expected.to eq("foo") + # ``` + macro is_expected + expect(subject) + end - # Starts an expectation on a block of code. - # This should be followed up with `Spectator::Expectations::ExpectationPartial#to` - # or `Spectator::Expectations::ExpectationPartial#to_not`. - # The block passed in, or its return value, will be checked - # to see if it satisfies the conditions specified. - # - # This method is identical to `#expect`, - # but is grammatically correct for the one-liner syntax. - # It can be used like so: - # ``` - # it expects { 5 / 0 }.to raise_error - # ``` - # The block of code is passed along for validation to the matchers. - # - # The short, one argument syntax used for passing methods to blocks can be used. - # So instead of doing this: - # ``` - # it expects(subject.size).to eq(5) - # ``` - # The following syntax can be used instead: - # ``` - # it expects(&.size).to eq(5) - # ``` - # The method passed will always be evaluated on the subject. - macro expects(&block) - expect {{block}} - end + # Short-hand form of `#is_expected` that can be used for one-liner syntax. + # + # For instance: + # ``` + # it "is 42" do + # expect(subject).to eq(42) + # end + # ``` + # + # Can be shortened to: + # ``` + # it { is(42) } + # ``` + # + # These three are functionally equivalent: + # ``` + # expect(subject).to eq("foo") + # is_expected.to eq("foo") + # is("foo") + # ``` + # + # See also: `#is_not` + macro is(expected) + expect(subject).to(eq({{expected}})) + end - # Short-hand for expecting something of the subject. - # These two are functionally equivalent: - # ``` - # expect(subject).to eq("foo") - # is_expected.to eq("foo") - # ``` - macro is_expected - expect(subject) - end - - # Short-hand form of `#is_expected` that can be used for one-liner syntax. - # For instance: - # ``` - # it "is 42" do - # expect(subject).to eq(42) - # end - # ``` - # Can be shortened to: - # ``` - # it is(42) - # ``` - # - # These three are functionally equivalent: - # ``` - # expect(subject).to eq("foo") - # is_expected.to eq("foo") - # is("foo") - # ``` - # - # See also: `#is_not` - macro is(expected) - is_expected.to eq({{expected}}) - end - - # Short-hand, negated form of `#is_expected` that can be used for one-liner syntax. - # For instance: - # ``` - # it "is not 42" do - # expect(subject).to_not eq(42) - # end - # ``` - # Can be shortened to: - # ``` - # it is_not(42) - # ``` - # - # These three are functionally equivalent: - # ``` - # expect(subject).to_not eq("foo") - # is_expected.to_not eq("foo") - # is_not("foo") - # ``` - # - # See also: `#is` - macro is_not(expected) - is_expected.to_not eq({{expected}}) - end - - macro should(matcher) - is_expected.to({{matcher}}) - end - - macro should_not(matcher) - is_expected.to_not({{matcher}}) - end - - macro should_eventually(matcher) - is_expected.to_eventually({{matcher}}) - end - - macro should_never(matcher) - is_expected.to_never({{matcher}}) - end - - # Immediately fail the current test. - # A reason can be passed, - # which is reported in the output. - def fail(reason : String) - raise ExampleFailed.new(reason) - end - - # :ditto: - @[AlwaysInline] - def fail - fail("Example failed") + # Short-hand, negated form of `#is_expected` that can be used for one-liner syntax. + # + # For instance: + # ``` + # it "is not 42" do + # expect(subject).to_not eq(42) + # end + # ``` + # + # Can be shortened to: + # ``` + # it { is_not(42) } + # ``` + # + # These three are functionally equivalent: + # ``` + # expect(subject).not_to eq("foo") + # is_expected.not_to eq("foo") + # is_not("foo") + # ``` + # + # See also: `#is` + macro is_not(expected) + expect(subject).not_to(eq({{expected}})) + end end end diff --git a/src/spectator/should.cr b/src/spectator/should.cr index b8cda4a..4c63d2c 100644 --- a/src/spectator/should.cr +++ b/src/spectator/should.cr @@ -64,3 +64,21 @@ struct Proc(*T, R) ::Spectator::Expectations::BlockExpectationPartial.new(actual, source).to_not(matcher) end end + +module Spectator::DSL::Assertions + macro should(matcher) + expect(subject).to({{matcher}}) + end + + macro should_not(matcher) + expect(subject).to_not({{matcher}}) + end + + macro should_eventually(matcher) + expect(subject).to_eventually({{matcher}}) + end + + macro should_never(matcher) + expect(subject).to_never({{matcher}}) + end +end From a74957204b4b03babdce8a0ea5dc2f4f4fa719d8 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 15 Jan 2021 22:32:02 -0700 Subject: [PATCH 104/399] Introduce abstract generic value type Sits between AbstractExpression and Value and Block. --- src/spectator/abstract_expression.cr | 19 ++++++++++++------- src/spectator/block.cr | 4 ++-- src/spectator/dsl/assertions.cr | 5 +++-- src/spectator/expression.cr | 18 +++++++----------- src/spectator/value.cr | 22 ++++++++++++++++++++++ 5 files changed, 46 insertions(+), 22 deletions(-) create mode 100644 src/spectator/value.cr diff --git a/src/spectator/abstract_expression.cr b/src/spectator/abstract_expression.cr index 2a58e06..3bb20f2 100644 --- a/src/spectator/abstract_expression.cr +++ b/src/spectator/abstract_expression.cr @@ -6,25 +6,30 @@ module Spectator # It consists of a label and the value of the expression. # The label should be a string recognizable by the user, # or nil if one isn't available. + # # This base class is provided so that all generic sub-classes can be stored as this one type. # The value of the expression can be retrieved by downcasting to the expected type with `#cast`. + # + # NOTE: This is intentionally a class and not a struct. + # If it were a struct, changes made to the value held by an instance may not be kept when passing it around. + # See commit ca564619ad2ae45f832a058d514298c868fdf699. abstract class AbstractExpression # User recognizable string for the expression. # This can be something like a variable name or a snippet of Crystal code. getter label : Label # Creates the expression. - # The *label* is usually the Crystal code evaluating to the `#value`. + # The *label* is usually the Crystal code evaluating to the `#raw_value`. # It can be nil if it isn't available. def initialize(@label : Label) end - # Retrieves the real value of the expression. - abstract def value + # Retrieves the evaluated value of the expression. + abstract def raw_value - # Attempts to cast `#value` to the type *T* and return it. + # Attempts to cast `#raw_value` to the type *T* and return it. def cast(type : T.class) : T forall T - value.as(T) + raw_value.as(T) end # Produces a string representation of the expression. @@ -35,7 +40,7 @@ module Spectator io << ':' io << ' ' end - io << value + raw_value.to_s(io) end # Produces a detailed string representation of the expression. @@ -46,7 +51,7 @@ module Spectator io << ':' io << ' ' end - value.inspect(io) + raw_value.inspect(io) end end end diff --git a/src/spectator/block.cr b/src/spectator/block.cr index 312681a..5deb82c 100644 --- a/src/spectator/block.cr +++ b/src/spectator/block.cr @@ -1,4 +1,4 @@ -require "./abstract_expression" +require "./expression" require "./label" require "./lazy" @@ -8,7 +8,7 @@ module Spectator # It consists of a label and parameterless block. # The label should be a string recognizable by the user, # or nil if one isn't available. - class Block(T) < AbstractExpression + class Block(T) < Expression(T) # Cached value returned from the block. @value = Lazy(T).new diff --git a/src/spectator/dsl/assertions.cr b/src/spectator/dsl/assertions.cr index 791ad73..d868d16 100644 --- a/src/spectator/dsl/assertions.cr +++ b/src/spectator/dsl/assertions.cr @@ -1,7 +1,8 @@ require "../assertion" require "../assertion_failed" -require "../expression" +require "../block" require "../source" +require "../value" module Spectator::DSL # Methods and macros for asserting that conditions are met. @@ -50,7 +51,7 @@ module Spectator::DSL {{actual}} end - %expression = ::Spectator::Expression.new(%actual, {{actual.stringify}}) + %expression = ::Spectator::Value.new(%actual, {{actual.stringify}}) %source = ::Spectator::Source.new({{actual.filename}}, {{actual.line_number}}) ::Spectator::Assertion::Target.new(%expression, %source) end diff --git a/src/spectator/expression.cr b/src/spectator/expression.cr index b12efce..0937b8e 100644 --- a/src/spectator/expression.cr +++ b/src/spectator/expression.cr @@ -1,22 +1,18 @@ require "./abstract_expression" -require "./label" module Spectator # Represents an expression from a test. # This is typically captured by an `expect` macro. - # It consists of a label and the value of the expression. + # It consists of a label and a typed expression. # The label should be a string recognizable by the user, # or nil if one isn't available. - class Expression(T) < AbstractExpression - # Raw value of the expression. - getter value + abstract class Expression(T) < AbstractExpression + # Retrieves the underlying value of the expression. + abstract def value : T - # Creates the expression. - # Expects the *value* of the expression and a *label* describing it. - # The *label* is usually the Crystal code evaluating to the *value*. - # It can be nil if it isn't available. - def initialize(@value : T, label : Label) - super(label) + # Retrieves the evaluated value of the expression. + def raw_value + value end end end diff --git a/src/spectator/value.cr b/src/spectator/value.cr new file mode 100644 index 0000000..9d04871 --- /dev/null +++ b/src/spectator/value.cr @@ -0,0 +1,22 @@ +require "./expression" +require "./label" + +module Spectator + # Represents a value from a test. + # This is typically captured by an `expect` macro. + # It consists of a label and the value of the expression. + # The label should be a string recognizable by the user, + # or nil if one isn't available. + class Value(T) < Expression(T) + # Raw value of the expression. + getter value : T + + # Creates the value. + # Expects the *value* of the expression and a *label* describing it. + # The *label* is usually the Crystal code evaluating to the *value*. + # It can be nil if it isn't available. + def initialize(@value : T, label : Label) + super(label) + end + end +end From 50d1f692300a1c7e5a13c85e4e3202d37aae5f44 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 15 Jan 2021 23:15:07 -0700 Subject: [PATCH 105/399] Don't cache the block return value Let the matcher handle this if it needs to. --- src/spectator/block.cr | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/spectator/block.cr b/src/spectator/block.cr index 5deb82c..135c537 100644 --- a/src/spectator/block.cr +++ b/src/spectator/block.cr @@ -1,6 +1,5 @@ require "./expression" require "./label" -require "./lazy" module Spectator # Represents a block from a test. @@ -9,9 +8,6 @@ module Spectator # The label should be a string recognizable by the user, # or nil if one isn't available. class Block(T) < Expression(T) - # Cached value returned from the block. - @value = Lazy(T).new - # Creates the block expression from a proc. # The *proc* will be called to evaluate the value of the expression. # The *label* is usually the Crystal code for the *proc*. @@ -28,18 +24,10 @@ module Spectator super(label) end - # Retrieves the value of the block expression. - # This will be the return value of the block. - # The block is lazily evaluated and the value retrieved only once. - # Afterwards, the value is cached and returned by successive calls to this method. - def value - @value.get { call } - end - # Evaluates the block and returns the value from it. # This method _does not_ cache the resulting value like `#value` does. # Successive calls to this method may return different values. - def call : T + def value : T @block.call end end From e09f5c960a1b4d4289a985794fa3b5fcea3ab490 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Jan 2021 10:22:23 -0700 Subject: [PATCH 106/399] Change Assertions to Expectations Start expectation rework. --- src/spectator/assertion.cr | 15 --- src/spectator/dsl.cr | 2 +- .../dsl/{assertions.cr => expectations.cr} | 8 +- src/spectator/expectation.cr | 114 ++++++++++++++++++ src/spectator/expectations.cr | 7 -- .../expectations/example_expectations.cr | 62 ---------- src/spectator/expectations/expectation.cr | 74 ------------ .../expectations/expectation_partial.cr | 101 ---------------- .../expectations/expectation_reporter.cr | 34 ------ src/spectator/test_context.cr | 2 +- 10 files changed, 120 insertions(+), 299 deletions(-) delete mode 100644 src/spectator/assertion.cr rename src/spectator/dsl/{assertions.cr => expectations.cr} (96%) create mode 100644 src/spectator/expectation.cr delete mode 100644 src/spectator/expectations.cr delete mode 100644 src/spectator/expectations/example_expectations.cr delete mode 100644 src/spectator/expectations/expectation.cr delete mode 100644 src/spectator/expectations/expectation_partial.cr delete mode 100644 src/spectator/expectations/expectation_reporter.cr diff --git a/src/spectator/assertion.cr b/src/spectator/assertion.cr deleted file mode 100644 index d2813ce..0000000 --- a/src/spectator/assertion.cr +++ /dev/null @@ -1,15 +0,0 @@ -require "./block" -require "./expression" - -module Spectator - class Assertion - struct Target(T) - @expression : Expression(T) | Block(T) - @source : Source? - - def initialize(@expression : Expression(T) | Block(T), @source) - puts "TARGET: #{@expression} @ #{@source}" - end - end - end -end diff --git a/src/spectator/dsl.cr b/src/spectator/dsl.cr index e206d1b..4fe0e43 100644 --- a/src/spectator/dsl.cr +++ b/src/spectator/dsl.cr @@ -1,7 +1,7 @@ # require "./dsl/*" -require "./dsl/assertions" require "./dsl/builder" require "./dsl/examples" +require "./dsl/expectations" require "./dsl/groups" require "./dsl/hooks" require "./dsl/top" diff --git a/src/spectator/dsl/assertions.cr b/src/spectator/dsl/expectations.cr similarity index 96% rename from src/spectator/dsl/assertions.cr rename to src/spectator/dsl/expectations.cr index d868d16..851b4f3 100644 --- a/src/spectator/dsl/assertions.cr +++ b/src/spectator/dsl/expectations.cr @@ -1,12 +1,12 @@ -require "../assertion" require "../assertion_failed" require "../block" +require "../expectation" require "../source" require "../value" module Spectator::DSL # Methods and macros for asserting that conditions are met. - module Assertions + module Expectations # Immediately fail the current test. # A reason can be specified with *message*. def fail(message = "Example failed", *, _file = __FILE__, _line = __LINE__) @@ -53,7 +53,7 @@ module Spectator::DSL %expression = ::Spectator::Value.new(%actual, {{actual.stringify}}) %source = ::Spectator::Source.new({{actual.filename}}, {{actual.line_number}}) - ::Spectator::Assertion::Target.new(%expression, %source) + ::Spectator::Expectation::Target.new(%expression, %source) end # Starts an expectation. @@ -105,7 +105,7 @@ module Spectator::DSL {% end %} %source = ::Spectator::Source.new({{block.filename}}, {{block.line_number}}) - ::Spectator::Assertion::Target.new(%block, %source) + ::Spectator::Expectation::Target.new(%block, %source) end # Short-hand for expecting something of the subject. diff --git a/src/spectator/expectation.cr b/src/spectator/expectation.cr new file mode 100644 index 0000000..fcb2961 --- /dev/null +++ b/src/spectator/expectation.cr @@ -0,0 +1,114 @@ +require "./expression" +require "./source" + +module Spectator + # Result of evaluating a matcher on a target. + # Contains information about the match, + # such as whether it was successful and a description of the operation. + struct Expectation + # Location of the expectation in source code. + # This can be nil if the source isn't capturable, + # for instance using the *should* syntax or dynamically created expectations. + getter source : Source? + + # Creates the expectation. + # The *match_data* comes from the result of calling `Matcher#match`. + # The *source* is the location of the expectation in source code, if available. + def initialize(@match_data : Matchers::MatchData, @source : Source? = nil) + end + + # Stores part of an expectation. + # This covers the actual value (or block) being inspected and its source. + # This is the type returned by an `expect` block in the DSL. + # It is not intended to be used directly, but instead by chaining methods. + # Typically `#to` and `#not_to` are used. + struct Target(T) + # Creates the expectation target. + # The *expression* is the actual value being tested and its label. + # The *source* is the location of where this expectation was defined. + def initialize(@expression : Expression(T), @source : Source) + puts "TARGET: #{@expression} @ #{@source}" + end + + # Asserts that some criteria defined by the matcher is satisfied. + def to(matcher) : Nil + match_data = matcher.match(@expression) + report(match_data) + end + + def to(stub : Mocks::MethodStub) : Nil + Harness.current.mocks.expect(@expression.value, stub) + value = TestValue.new(stub.name, stub.to_s) + matcher = Matchers::ReceiveMatcher.new(value, stub.arguments?) + to_eventually(matcher) + end + + def to(stubs : Enumerable(Mocks::MethodStub)) : Nil + stubs.each { |stub| to(stub) } + end + + # Asserts that some criteria defined by the matcher is not satisfied. + # This is effectively the opposite of `#to`. + def to_not(matcher) : Nil + match_data = matcher.negated_match(@expression) + report(match_data) + end + + def to_not(stub : Mocks::MethodStub) : Nil + value = TestValue.new(stub.name, stub.to_s) + matcher = Matchers::ReceiveMatcher.new(value, stub.arguments?) + to_never(matcher) + end + + def to_not(stubs : Enumerable(Mocks::MethodStub)) : Nil + stubs.each { |stub| to_not(stub) } + end + + # :ditto: + @[AlwaysInline] + def not_to(matcher) : Nil + to_not(matcher) + end + + # Asserts that some criteria defined by the matcher is eventually satisfied. + # The expectation is checked after the example finishes and all hooks have run. + def to_eventually(matcher) : Nil + Harness.current.defer { to(matcher) } + end + + def to_eventually(stub : Mocks::MethodStub) : Nil + to(stub) + end + + def to_eventually(stubs : Enumerable(Mocks::MethodStub)) : Nil + to(stub) + end + + # Asserts that some criteria defined by the matcher is never satisfied. + # The expectation is checked after the example finishes and all hooks have run. + def to_never(matcher) : Nil + Harness.current.defer { to_not(matcher) } + end + + def to_never(stub : Mocks::MethodStub) : Nil + to_not(stub) + end + + def to_never(stub : Enumerable(Mocks::MethodStub)) : Nil + to_not(stub) + end + + # :ditto: + @[AlwaysInline] + def never_to(matcher) : Nil + to_never(matcher) + end + + # Reports an expectation to the current harness. + private def report(match_data : Matchers::MatchData) + expectation = Expectation.new(match_data, @source) + Harness.current.report_expectation(expectation) + end + end + end +end diff --git a/src/spectator/expectations.cr b/src/spectator/expectations.cr deleted file mode 100644 index b835f10..0000000 --- a/src/spectator/expectations.cr +++ /dev/null @@ -1,7 +0,0 @@ -require "./expectations/*" - -module Spectator - # Namespace that contains all expectations, partials, and handling of them. - module Expectations - end -end diff --git a/src/spectator/expectations/example_expectations.cr b/src/spectator/expectations/example_expectations.cr deleted file mode 100644 index 31f1f98..0000000 --- a/src/spectator/expectations/example_expectations.cr +++ /dev/null @@ -1,62 +0,0 @@ -require "./expectation" - -module Spectator::Expectations - # Collection of expectations from an example. - class ExampleExpectations - include Enumerable(Expectation) - - # Creates the collection. - def initialize(@expectations : Array(Expectation)) - end - - # Iterates through all expectations. - def each - @expectations.each do |expectation| - yield expectation - end - end - - # Returns a collection of only the satisfied expectations. - def satisfied : Enumerable(Expectation) - @expectations.select(&.satisfied?) - end - - # Iterates over only the satisfied expectations. - def each_satisfied - @expectations.each do |expectation| - yield expectation if expectation.satisfied? - end - end - - # Returns a collection of only the unsatisfied expectations. - def unsatisfied : Enumerable(Expectation) - @expectations.reject(&.satisfied?) - end - - # Iterates over only the unsatisfied expectations. - def each_unsatisfied - @expectations.each do |expectation| - yield expectation unless expectation.satisfied? - end - end - - # Determines whether the example was successful - # based on if all expectations were satisfied. - def successful? - @expectations.all?(&.satisfied?) - end - - # Determines whether the example failed - # based on if any expectations were not satisfied. - def failed? - !successful? - end - - # Creates the JSON representation of the expectations. - def to_json(json : ::JSON::Builder) - json.array do - each &.to_json(json) - end - end - end -end diff --git a/src/spectator/expectations/expectation.cr b/src/spectator/expectations/expectation.cr deleted file mode 100644 index 72d3dbd..0000000 --- a/src/spectator/expectations/expectation.cr +++ /dev/null @@ -1,74 +0,0 @@ -require "../matchers/failed_match_data" -require "../matchers/match_data" -require "../source" - -module Spectator::Expectations - # Result of evaluating a matcher on an expectation partial. - struct Expectation - # Location where this expectation was defined. - getter source : Source - - # Creates the expectation. - def initialize(@match_data : Matchers::MatchData, @source : Source) - end - - # Indicates whether the matcher was satisified. - def satisfied? - @match_data.matched? - end - - # Indicates that the expectation was not satisified. - def failure? - !satisfied? - end - - # Description of why the match failed. - # If nil, then the match was successful. - def failure_message? - @match_data.as?(Matchers::FailedMatchData).try(&.failure_message) - end - - # Description of why the match failed. - def failure_message - failure_message?.not_nil! - end - - # Additional information about the match, useful for debug. - # If nil, then the match was successful. - def values? - @match_data.as?(Matchers::FailedMatchData).try(&.values) - end - - # Additional information about the match, useful for debug. - def values - values?.not_nil! - end - - def description - @match_data.description - end - - # Creates the JSON representation of the expectation. - def to_json(json : ::JSON::Builder) - json.object do - json.field("source") { @source.to_json(json) } - json.field("satisfied", satisfied?) - if (failed = @match_data.as?(Matchers::FailedMatchData)) - failed_to_json(failed, json) - end - end - end - - # Adds failure information to a JSON structure. - private def failed_to_json(failed : Matchers::FailedMatchData, json : ::JSON::Builder) - json.field("failure", failed.failure_message) - json.field("values") do - json.object do - failed.values.each do |pair| - json.field(pair.first, pair.last) - end - end - end - end - end -end diff --git a/src/spectator/expectations/expectation_partial.cr b/src/spectator/expectations/expectation_partial.cr deleted file mode 100644 index c837c86..0000000 --- a/src/spectator/expectations/expectation_partial.cr +++ /dev/null @@ -1,101 +0,0 @@ -require "../matchers/match_data" -require "../source" -require "../test_expression" - -module Spectator::Expectations - # Stores part of an expectation (obviously). - # The part of the expectation this type covers is the actual value and source. - # This can also cover a block's behavior. - struct ExpectationPartial(T) - # The actual value being tested. - # This also contains its label. - getter actual : TestExpression(T) - - # Location where this expectation was defined. - getter source : Source - - # Creates the partial. - def initialize(@actual : TestExpression(T), @source : Source) - end - - # Asserts that some criteria defined by the matcher is satisfied. - def to(matcher) : Nil - match_data = matcher.match(@actual) - report(match_data) - end - - def to(stub : Mocks::MethodStub) : Nil - Harness.current.mocks.expect(@actual.value, stub) - value = TestValue.new(stub.name, stub.to_s) - matcher = Matchers::ReceiveMatcher.new(value, stub.arguments?) - to_eventually(matcher) - end - - def to(stubs : Enumerable(Mocks::MethodStub)) : Nil - stubs.each { |stub| to(stub) } - end - - # Asserts that some criteria defined by the matcher is not satisfied. - # This is effectively the opposite of `#to`. - def to_not(matcher) : Nil - match_data = matcher.negated_match(@actual) - report(match_data) - end - - def to_not(stub : Mocks::MethodStub) : Nil - value = TestValue.new(stub.name, stub.to_s) - matcher = Matchers::ReceiveMatcher.new(value, stub.arguments?) - to_never(matcher) - end - - def to_not(stubs : Enumerable(Mocks::MethodStub)) : Nil - stubs.each { |stub| to_not(stub) } - end - - # :ditto: - @[AlwaysInline] - def not_to(matcher) : Nil - to_not(matcher) - end - - # Asserts that some criteria defined by the matcher is eventually satisfied. - # The expectation is checked after the example finishes and all hooks have run. - def to_eventually(matcher) : Nil - Harness.current.defer { to(matcher) } - end - - def to_eventually(stub : Mocks::MethodStub) : Nil - to(stub) - end - - def to_eventually(stubs : Enumerable(Mocks::MethodStub)) : Nil - to(stub) - end - - # Asserts that some criteria defined by the matcher is never satisfied. - # The expectation is checked after the example finishes and all hooks have run. - def to_never(matcher) : Nil - Harness.current.defer { to_not(matcher) } - end - - def to_never(stub : Mocks::MethodStub) : Nil - to_not(stub) - end - - def to_never(stub : Enumerable(Mocks::MethodStub)) : Nil - to_not(stub) - end - - # :ditto: - @[AlwaysInline] - def never_to(matcher) : Nil - to_never(matcher) - end - - # Reports an expectation to the current harness. - private def report(match_data : Matchers::MatchData) - expectation = Expectation.new(match_data, @source) - Harness.current.report_expectation(expectation) - end - end -end diff --git a/src/spectator/expectations/expectation_reporter.cr b/src/spectator/expectations/expectation_reporter.cr deleted file mode 100644 index 2830b8e..0000000 --- a/src/spectator/expectations/expectation_reporter.cr +++ /dev/null @@ -1,34 +0,0 @@ -module Spectator::Expectations - # Tracks the expectations and their outcomes in an example. - # A single instance of this class should be associated with one example. - class ExpectationReporter - # All expectations are stored in this array. - # The initial capacity is set to one, - # as that is the typical (and recommended) - # number of expectations per example. - @expectations = Array(Expectation).new(1) - - # Creates the reporter. - # When the *raise_on_failure* flag is set to true, - # which is the default, an exception will be raised - # on the first failure that is reported. - # To store failures and continue, set the flag to false. - def initialize(@raise_on_failure = true) - end - - # Stores the outcome of an expectation. - # If the raise on failure flag is set to true, - # then this method will raise an exception - # when a failing result is given. - def report(expectation : Expectation) : Nil - @expectations << expectation - raise ExpectationFailed.new(expectation) if !expectation.satisfied? && @raise_on_failure - end - - # Returns the reported expectations from the example. - # This should be run after the example has finished. - def expectations : ExampleExpectations - ExampleExpectations.new(@expectations) - end - end -end diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr index c6ca933..0595ee8 100644 --- a/src/spectator/test_context.cr +++ b/src/spectator/test_context.cr @@ -6,8 +6,8 @@ require "./dsl" # This type is intentionally outside the `Spectator` module. # The reason for this is to prevent name collision when using the DSL to define a spec. class SpectatorTestContext < SpectatorContext - include ::Spectator::DSL::Assertions include ::Spectator::DSL::Examples + include ::Spectator::DSL::Expectations include ::Spectator::DSL::Groups include ::Spectator::DSL::Hooks include ::Spectator::DSL::Values From 58e7981b0c5ea56860512cda15c21e74a7f96605 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Jan 2021 11:00:54 -0700 Subject: [PATCH 107/399] Fix type warning --- src/spectator/block.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/block.cr b/src/spectator/block.cr index 5deb82c..ad6a861 100644 --- a/src/spectator/block.cr +++ b/src/spectator/block.cr @@ -32,7 +32,7 @@ module Spectator # This will be the return value of the block. # The block is lazily evaluated and the value retrieved only once. # Afterwards, the value is cached and returned by successive calls to this method. - def value + def value : T @value.get { call } end From 4500ebcddc7df24793748fdc2a04055ed301d8ea Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Jan 2021 11:02:29 -0700 Subject: [PATCH 108/399] Update old references to Value and Block --- src/spectator/dsl/matchers.cr | 169 ++++++++---------- src/spectator/expectation.cr | 6 +- src/spectator/matchers/all_matcher.cr | 12 +- src/spectator/matchers/array_matcher.cr | 6 +- src/spectator/matchers/attributes_matcher.cr | 8 +- src/spectator/matchers/case_matcher.cr | 4 +- .../matchers/change_exact_matcher.cr | 6 +- src/spectator/matchers/change_from_matcher.cr | 6 +- src/spectator/matchers/change_matcher.cr | 6 +- .../matchers/change_relative_matcher.cr | 6 +- src/spectator/matchers/change_to_matcher.cr | 6 +- src/spectator/matchers/collection_matcher.cr | 8 +- src/spectator/matchers/contain_matcher.cr | 6 +- src/spectator/matchers/empty_matcher.cr | 2 +- src/spectator/matchers/end_with_matcher.cr | 6 +- src/spectator/matchers/equality_matcher.cr | 2 +- src/spectator/matchers/exception_matcher.cr | 16 +- .../matchers/greater_than_equal_matcher.cr | 2 +- .../matchers/greater_than_matcher.cr | 2 +- src/spectator/matchers/have_key_matcher.cr | 2 +- src/spectator/matchers/have_matcher.cr | 6 +- .../matchers/have_predicate_matcher.cr | 4 +- src/spectator/matchers/have_value_matcher.cr | 2 +- src/spectator/matchers/inequality_matcher.cr | 2 +- src/spectator/matchers/instance_matcher.cr | 2 +- .../matchers/less_than_equal_matcher.cr | 2 +- src/spectator/matchers/less_than_matcher.cr | 2 +- src/spectator/matchers/matcher.cr | 4 +- src/spectator/matchers/nil_matcher.cr | 2 +- src/spectator/matchers/predicate_matcher.cr | 6 +- src/spectator/matchers/range_matcher.cr | 6 +- src/spectator/matchers/receive_matcher.cr | 12 +- .../matchers/receive_type_matcher.cr | 14 +- src/spectator/matchers/reference_matcher.cr | 2 +- src/spectator/matchers/respond_matcher.cr | 4 +- src/spectator/matchers/size_matcher.cr | 2 +- src/spectator/matchers/size_of_matcher.cr | 2 +- src/spectator/matchers/standard_matcher.cr | 18 +- src/spectator/matchers/start_with_matcher.cr | 6 +- src/spectator/matchers/truthy_matcher.cr | 14 +- src/spectator/matchers/type_matcher.cr | 2 +- .../matchers/unordered_array_matcher.cr | 6 +- src/spectator/matchers/value_matcher.cr | 6 +- src/spectator/mocks/expect_any_instance.cr | 4 +- 44 files changed, 196 insertions(+), 215 deletions(-) diff --git a/src/spectator/dsl/matchers.cr b/src/spectator/dsl/matchers.cr index 9deb404..8d51193 100644 --- a/src/spectator/dsl/matchers.cr +++ b/src/spectator/dsl/matchers.cr @@ -1,9 +1,9 @@ +require "../block" require "../matchers" -require "../test_block" -require "../test_value" +require "../value" -module Spectator - module DSL +module Spectator::DSL + module Matchers # Indicates that some value should equal another. # The == operator is used for this check. # The value passed to this method is the expected value. @@ -13,8 +13,8 @@ module Spectator # expect(1 + 2).to eq(3) # ``` macro eq(expected) - %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::EqualityMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::EqualityMatcher.new(%value) end # Indicates that some value should not equal another. @@ -26,8 +26,8 @@ module Spectator # expect(1 + 2).to ne(5) # ``` macro ne(expected) - %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::InequalityMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::InequalityMatcher.new(%value) end # Indicates that some value when compared to another satisfies an operator. @@ -61,8 +61,8 @@ module Spectator # expect(obj.dup).to_not be(obj) # ``` macro be(expected) - %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::ReferenceMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::ReferenceMatcher.new(%value) end # Indicates that some value should be of a specified type. @@ -173,8 +173,8 @@ module Spectator # expect(3 - 1).to be_lt(3) # ``` macro be_lt(expected) - %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::LessThanMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::LessThanMatcher.new(%value) end # Indicates that some value should be less than or equal to another. @@ -186,8 +186,8 @@ module Spectator # expect(3 - 1).to be_le(3) # ``` macro be_le(expected) - %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::LessThanEqualMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::LessThanEqualMatcher.new(%value) end # Indicates that some value should be greater than another. @@ -199,8 +199,8 @@ module Spectator # expect(3 + 1).to be_gt(3) # ``` macro be_gt(expected) - %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::GreaterThanMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::GreaterThanMatcher.new(%value) end # Indicates that some value should be greater than or equal to another. @@ -212,8 +212,8 @@ module Spectator # expect(3 + 1).to be_ge(3) # ``` macro be_ge(expected) - %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::GreaterThanEqualMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::GreaterThanEqualMatcher.new(%value) end # Indicates that some value should match another. @@ -230,8 +230,8 @@ module Spectator # expect({:foo, 5}).to match({Symbol, Int32}) # ``` macro match(expected) - %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::CaseMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::CaseMatcher.new(%value) end # Indicates that some value should be true. @@ -321,8 +321,8 @@ module Spectator # NOTE: Do not attempt to mix the two use cases. # It likely won't work and will result in a compilation error. macro be_within(expected) - %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::CollectionMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::CollectionMatcher.new(%value) end # Indicates that some value should be between a lower and upper-bound. @@ -344,8 +344,8 @@ module Spectator macro be_between(min, max) %range = Range.new({{min}}, {{max}}) %label = [{{min.stringify}}, {{max.stringify}}].join(" to ") - %test_value = ::Spectator::TestValue.new(%range, %label) - ::Spectator::Matchers::RangeMatcher.new(%test_value) + %value = ::Spectator::Value.new(%range, %label) + ::Spectator::Matchers::RangeMatcher.new(%value) end # Indicates that some value should be within a delta of an expected value. @@ -403,8 +403,8 @@ module Spectator # expect(%w[foo bar]).to start_with(/foo/) # ``` macro start_with(expected) - %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::StartWithMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::StartWithMatcher.new(%value) end # Indicates that some value or set should end with another value. @@ -426,8 +426,8 @@ module Spectator # expect(%w[foo bar]).to end_with(/bar/) # ``` macro end_with(expected) - %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::EndWithMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::EndWithMatcher.new(%value) end # Indicates that some value or set should contain another value. @@ -451,11 +451,11 @@ module Spectator # ``` macro contain(*expected) {% if expected.id.starts_with?("{*") %} - %test_value = ::Spectator::TestValue.new({{expected.id[2...-1]}}, {{expected.splat.stringify}}) - ::Spectator::Matchers::ContainMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected.id[2...-1]}}, {{expected.splat.stringify}}) + ::Spectator::Matchers::ContainMatcher.new(%value) {% else %} - %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.splat.stringify}}) - ::Spectator::Matchers::ContainMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected}}, {{expected.splat.stringify}}) + ::Spectator::Matchers::ContainMatcher.new(%value) {% end %} end @@ -475,8 +475,8 @@ module Spectator # expect(%i[a b c]).to contain_elements(%i[a b]) # ``` macro contain_elements(expected) - %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::ContainMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::ContainMatcher.new(%value) end # Indicates that some range (or collection) should contain another value. @@ -497,11 +497,11 @@ module Spectator # ``` macro cover(*expected) {% if expected.id.starts_with?("{*") %} - %test_value = ::Spectator::TestValue.new({{expected.id[2...-1]}}, {{expected.splat.stringify}}) - ::Spectator::Matchers::ContainMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected.id[2...-1]}}, {{expected.splat.stringify}}) + ::Spectator::Matchers::ContainMatcher.new(%value) {% else %} - %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.splat.stringify}}) - ::Spectator::Matchers::ContainMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected}}, {{expected.splat.stringify}}) + ::Spectator::Matchers::ContainMatcher.new(%value) {% end %} end @@ -532,11 +532,11 @@ module Spectator # ``` macro have(*expected) {% if expected.id.starts_with?("{*") %} - %test_value = ::Spectator::TestValue.new({{expected.id[2...-1]}}, {{expected.splat.stringify}}) - ::Spectator::Matchers::HaveMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected.id[2...-1]}}, {{expected.splat.stringify}}) + ::Spectator::Matchers::HaveMatcher.new(%value) {% else %} - %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.splat.stringify}}) - ::Spectator::Matchers::HaveMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected}}, {{expected.splat.stringify}}) + ::Spectator::Matchers::HaveMatcher.new(%value) {% end %} end @@ -559,8 +559,8 @@ module Spectator # expect([1, 2, 3, :a, :b, :c]).to have_elements([Int32, Symbol]) # ``` macro have_elements(expected) - %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::HaveMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::HaveMatcher.new(%value) end # Indicates that some set, such as a `Hash`, has a given key. @@ -572,8 +572,8 @@ module Spectator # expect({"lucky" => 7}).to have_key("lucky") # ``` macro have_key(expected) - %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::HaveKeyMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::HaveKeyMatcher.new(%value) end # :ditto: @@ -590,8 +590,8 @@ module Spectator # expect({"lucky" => 7}).to have_value(7) # ``` macro have_value(expected) - %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::HaveValueMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::HaveValueMatcher.new(%value) end # :ditto: @@ -607,11 +607,11 @@ module Spectator # ``` macro contain_exactly(*expected) {% if expected.id.starts_with?("{*") %} - %test_value = ::Spectator::TestValue.new(({{expected.id[2...-1]}}).to_a, {{expected.stringify}}) - ::Spectator::Matchers::ArrayMatcher.new(%test_value) + %value = ::Spectator::Value.new(({{expected.id[2...-1]}}).to_a, {{expected.stringify}}) + ::Spectator::Matchers::ArrayMatcher.new(%value) {% else %} - %test_value = ::Spectator::TestValue.new(({{expected}}).to_a, {{expected.stringify}}) - ::Spectator::Matchers::ArrayMatcher.new(%test_value) + %value = ::Spectator::Value.new(({{expected}}).to_a, {{expected.stringify}}) + ::Spectator::Matchers::ArrayMatcher.new(%value) {% end %} end @@ -623,8 +623,8 @@ module Spectator # expect([1, 2, 3]).to match_array([3, 2, 1]) # ``` macro match_array(expected) - %test_value = ::Spectator::TestValue.new(({{expected}}).to_a, {{expected.stringify}}) - ::Spectator::Matchers::ArrayMatcher.new(%test_value) + %value = ::Spectator::Value.new(({{expected}}).to_a, {{expected.stringify}}) + ::Spectator::Matchers::ArrayMatcher.new(%value) end # Indicates that some set should have a specified size. @@ -634,8 +634,8 @@ module Spectator # expect([1, 2, 3]).to have_size(3) # ``` macro have_size(expected) - %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::SizeMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::SizeMatcher.new(%value) end # Indicates that some set should have the same size (number of elements) as another set. @@ -645,8 +645,8 @@ module Spectator # expect([1, 2, 3]).to have_size_of(%i[x y z]) # ``` macro have_size_of(expected) - %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) - ::Spectator::Matchers::SizeOfMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) + ::Spectator::Matchers::SizeOfMatcher.new(%value) end # Indicates that some value should have a set of attributes matching some conditions. @@ -661,11 +661,11 @@ module Spectator # ``` macro have_attributes(**expected) {% if expected.id.starts_with?("{**") %} - %test_value = ::Spectator::TestValue.new({{expected.id[3...-1]}}, {{expected.double_splat.stringify}}) - ::Spectator::Matchers::AttributesMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected.id[3...-1]}}, {{expected.double_splat.stringify}}) + ::Spectator::Matchers::AttributesMatcher.new(%value) {% else %} - %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.double_splat.stringify}}) - ::Spectator::Matchers::AttributesMatcher.new(%test_value) + %value = ::Spectator::Value.new({{expected}}, {{expected.double_splat.stringify}}) + ::Spectator::Matchers::AttributesMatcher.new(%value) {% end %} end @@ -718,37 +718,18 @@ module Spectator # expect { subject << :foo }.to change(&.size).by(1) # ``` macro change(&expression) - {% if expression.is_a?(Nop) %} - {% raise "Block must be provided to change matcher" %} - {% end %} - - # Check if the short-hand method syntax is used. - # This is a hack, since macros don't get this as a "literal" or something similar. - # The Crystal compiler will translate: - # ``` - # &.foo - # ``` - # to: - # ``` - # { |__arg0| __arg0.foo } - # ``` - # The hack used here is to check if it looks like a compiler-generated block. - {% if expression.args == ["__arg0".id] && expression.body.is_a?(Call) && expression.body.id =~ /^__arg0\./ %} - # Extract the method name to make it clear to the user what is tested. - # The raw block can't be used because it's not clear to the user. - {% method_name = expression.body.id.split('.')[1..-1].join('.') %} - %proc = ->{ subject.{{method_name.id}} } - %test_block = ::Spectator::TestBlock.create(%proc, {{"#" + method_name}}) - {% elsif expression.args.empty? %} - # In this case, it looks like the short-hand method syntax wasn't used. - # Capture the block as a proc and pass along. - %proc = ->{{expression}} - %test_block = ::Spectator::TestBlock.create(%proc, {{"`" + expression.body.stringify + "`"}}) + {% if block.args.size == 1 && block.args[0] =~ /^__arg\d+$/ && block.body.is_a?(Call) && block.body.id =~ /^__arg\d+\./ %} + {% method_name = block.body.id.split('.')[1..-1].join('.') %} + %block = ::Spectator::Block.new({{"#" + method_name}}) do + subject.{{method_name.id}} + end + {% elsif block.args.empty? %} + %block = ::Spectator::Block.new({{"`" + block.body.stringify + "`"}}) {{block}} {% else %} - {% raise "Unexpected block arguments in change matcher" %} + {% raise "Unexpected block arguments in 'expect' call" %} {% end %} - ::Spectator::Matchers::ChangeMatcher.new(%test_block) + ::Spectator::Matchers::ChangeMatcher.new(%block) end # Indicates that some block should raise an error. @@ -828,8 +809,8 @@ module Spectator end macro have_received(method) - %test_value = ::Spectator::TestValue.new(({{method.id.symbolize}}), {{method.id.stringify}}) - ::Spectator::Matchers::ReceiveMatcher.new(%test_value) + %value = ::Spectator::Value.new(({{method.id.symbolize}}), {{method.id.stringify}}) + ::Spectator::Matchers::ReceiveMatcher.new(%value) end # Used to create predicate matchers. @@ -872,8 +853,8 @@ module Spectator {% end %} label << ')' {% end %} - test_value = ::Spectator::TestValue.new(descriptor, label.to_s) - ::Spectator::Matchers::{{matcher.id}}.new(test_value) + value = ::Spectator::Value.new(descriptor, label.to_s) + ::Spectator::Matchers::{{matcher.id}}.new(value) end end end diff --git a/src/spectator/expectation.cr b/src/spectator/expectation.cr index fcb2961..091ba2b 100644 --- a/src/spectator/expectation.cr +++ b/src/spectator/expectation.cr @@ -38,7 +38,7 @@ module Spectator def to(stub : Mocks::MethodStub) : Nil Harness.current.mocks.expect(@expression.value, stub) - value = TestValue.new(stub.name, stub.to_s) + value = Value.new(stub.name, stub.to_s) matcher = Matchers::ReceiveMatcher.new(value, stub.arguments?) to_eventually(matcher) end @@ -55,7 +55,7 @@ module Spectator end def to_not(stub : Mocks::MethodStub) : Nil - value = TestValue.new(stub.name, stub.to_s) + value = Value.new(stub.name, stub.to_s) matcher = Matchers::ReceiveMatcher.new(value, stub.arguments?) to_never(matcher) end @@ -107,7 +107,7 @@ module Spectator # Reports an expectation to the current harness. private def report(match_data : Matchers::MatchData) expectation = Expectation.new(match_data, @source) - Harness.current.report_expectation(expectation) + Harness.current.report(expectation) end end end diff --git a/src/spectator/matchers/all_matcher.cr b/src/spectator/matchers/all_matcher.cr index d99ade4..becefad 100644 --- a/src/spectator/matchers/all_matcher.cr +++ b/src/spectator/matchers/all_matcher.cr @@ -1,4 +1,4 @@ -require "../test_value" +require "../value" require "./failed_match_data" require "./matcher" require "./successful_match_data" @@ -21,8 +21,8 @@ module Spectator::Matchers end # Actually performs the test against the expression. - def match(actual : TestExpression(T)) : MatchData forall T - found = test_values(actual).each do |element| + def match(actual : Expression(T)) : MatchData forall T + found = values(actual).each do |element| match_data = matcher.match(element) break match_data unless match_data.matched? end @@ -39,18 +39,18 @@ module Spectator::Matchers # What if the collection is empty? # # RSpec doesn't support this syntax either. - def negated_match(actual : TestExpression(T)) : MatchData forall T + def negated_match(actual : Expression(T)) : MatchData forall T {% raise "The `expect { }.to_not all()` syntax is not supported (ambiguous)." %} end # Maps all values in the test collection to their own test values. # Each value is given their own label, # which is the original label with an index appended. - private def test_values(actual) + private def values(actual) label_prefix = actual.label actual.value.map_with_index do |value, index| label = "#{label_prefix}[#{index}]" - TestValue.new(value, label) + Value.new(value, label) end end end diff --git a/src/spectator/matchers/array_matcher.cr b/src/spectator/matchers/array_matcher.cr index 2f0d264..77dc14c 100644 --- a/src/spectator/matchers/array_matcher.cr +++ b/src/spectator/matchers/array_matcher.cr @@ -11,7 +11,7 @@ module Spectator::Matchers private getter expected # Creates the matcher with an expected value. - def initialize(@expected : TestValue(Array(ExpectedType))) + def initialize(@expected : Value(Array(ExpectedType))) end # Short text about the matcher's purpose. @@ -22,7 +22,7 @@ module Spectator::Matchers end # Actually performs the test against the expression. - def match(actual : TestExpression(T)) : MatchData forall T + def match(actual : Expression(T)) : MatchData forall T actual_value = actual.value return unexpected(actual_value, actual.label) unless actual_value.responds_to?(:to_a) @@ -46,7 +46,7 @@ module Spectator::Matchers # Performs the test against the expression, but inverted. # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : TestExpression(T)) : MatchData forall T + def negated_match(actual : Expression(T)) : MatchData forall T actual_value = actual.value return unexpected(actual_value, actual.label) unless actual_value.responds_to?(:to_a) diff --git a/src/spectator/matchers/attributes_matcher.cr b/src/spectator/matchers/attributes_matcher.cr index ec8826f..86349b4 100644 --- a/src/spectator/matchers/attributes_matcher.cr +++ b/src/spectator/matchers/attributes_matcher.cr @@ -1,4 +1,4 @@ -require "../test_value" +require "../value" require "./failed_match_data" require "./matcher" require "./successful_match_data" @@ -14,7 +14,7 @@ module Spectator::Matchers private getter expected # Creates the matcher with an expected value. - def initialize(@expected : TestValue(ExpectedType)) + def initialize(@expected : Value(ExpectedType)) end # Short text about the matcher's purpose. @@ -25,7 +25,7 @@ module Spectator::Matchers end # Actually performs the test against the expression. - def match(actual : TestExpression(T)) : MatchData forall T + def match(actual : Expression(T)) : MatchData forall T snapshot = snapshot_values(actual.value) if match?(snapshot) SuccessfulMatchData.new(description) @@ -36,7 +36,7 @@ module Spectator::Matchers # Performs the test against the expression, but inverted. # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : TestExpression(T)) : MatchData forall T + def negated_match(actual : Expression(T)) : MatchData forall T snapshot = snapshot_values(actual.value) if match?(snapshot) FailedMatchData.new(description, "#{actual.label} has attributes #{expected.label}", negated_values(snapshot).to_a) diff --git a/src/spectator/matchers/case_matcher.cr b/src/spectator/matchers/case_matcher.cr index fcb5c23..00618be 100644 --- a/src/spectator/matchers/case_matcher.cr +++ b/src/spectator/matchers/case_matcher.cr @@ -12,13 +12,13 @@ module Spectator::Matchers end # Checks whether the matcher is satisifed with the expression given to it. - private def match?(actual : TestExpression(T)) : Bool forall T + private def match?(actual : Expression(T)) : Bool forall T expected.value === actual.value end # Overload that takes a regex so that the operands are flipped. # This mimics RSpec's behavior. - private def match?(actual : TestExpression(Regex)) : Bool forall T + private def match?(actual : Expression(Regex)) : Bool forall T actual.value === expected.value end diff --git a/src/spectator/matchers/change_exact_matcher.cr b/src/spectator/matchers/change_exact_matcher.cr index 6aa562d..94c391a 100644 --- a/src/spectator/matchers/change_exact_matcher.cr +++ b/src/spectator/matchers/change_exact_matcher.cr @@ -15,7 +15,7 @@ module Spectator::Matchers private getter expected_after # Creates a new change matcher. - def initialize(@expression : TestBlock(ExpressionType), @expected_before : FromType, @expected_after : ToType) + def initialize(@expression : Block(ExpressionType), @expected_before : FromType, @expected_after : ToType) end # Short text about the matcher's purpose. @@ -26,7 +26,7 @@ module Spectator::Matchers end # Actually performs the test against the expression. - def match(actual : TestExpression(T)) : MatchData forall T + def match(actual : Expression(T)) : MatchData forall T before, after = change(actual) if expected_before == before if before == after @@ -53,7 +53,7 @@ module Spectator::Matchers # Performs the test against the expression, but inverted. # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : TestExpression(T)) : MatchData forall T + def negated_match(actual : Expression(T)) : MatchData forall T before, after = change(actual) if expected_before == before if expected_after == after diff --git a/src/spectator/matchers/change_from_matcher.cr b/src/spectator/matchers/change_from_matcher.cr index c0e08c1..cfbea50 100644 --- a/src/spectator/matchers/change_from_matcher.cr +++ b/src/spectator/matchers/change_from_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers private getter expected # Creates a new change matcher. - def initialize(@expression : TestBlock(ExpressionType), @expected : FromType) + def initialize(@expression : Block(ExpressionType), @expected : FromType) end # Short text about the matcher's purpose. @@ -24,7 +24,7 @@ module Spectator::Matchers end # Actually performs the test against the expression. - def match(actual : TestExpression(T)) : MatchData forall T + def match(actual : Expression(T)) : MatchData forall T before, after = change(actual) if expected != before FailedMatchData.new(description, "#{expression.label} was not initially #{expected}", @@ -44,7 +44,7 @@ module Spectator::Matchers # Performs the test against the expression, but inverted. # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : TestExpression(T)) : MatchData forall T + def negated_match(actual : Expression(T)) : MatchData forall T before, after = change(actual) if expected != before FailedMatchData.new(description, "#{expression.label} was not initially #{expected}", diff --git a/src/spectator/matchers/change_matcher.cr b/src/spectator/matchers/change_matcher.cr index a60891b..1f60ac5 100644 --- a/src/spectator/matchers/change_matcher.cr +++ b/src/spectator/matchers/change_matcher.cr @@ -11,7 +11,7 @@ module Spectator::Matchers private getter expression # Creates a new change matcher. - def initialize(@expression : TestBlock(ExpressionType)) + def initialize(@expression : Block(ExpressionType)) end # Short text about the matcher's purpose. @@ -22,7 +22,7 @@ module Spectator::Matchers end # Actually performs the test against the expression. - def match(actual : TestExpression(T)) : MatchData forall T + def match(actual : Expression(T)) : MatchData forall T before, after = change(actual) if before == after FailedMatchData.new(description, "#{actual.label} did not change #{expression.label}", @@ -36,7 +36,7 @@ module Spectator::Matchers # Performs the test against the expression, but inverted. # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : TestExpression(T)) : MatchData forall T + def negated_match(actual : Expression(T)) : MatchData forall T before, after = change(actual) if before == after SuccessfulMatchData.new(description) diff --git a/src/spectator/matchers/change_relative_matcher.cr b/src/spectator/matchers/change_relative_matcher.cr index 4ac6e38..539c971 100644 --- a/src/spectator/matchers/change_relative_matcher.cr +++ b/src/spectator/matchers/change_relative_matcher.cr @@ -9,7 +9,7 @@ module Spectator::Matchers private getter expression # Creates a new change matcher. - def initialize(@expression : TestBlock(ExpressionType), @relativity : String, + def initialize(@expression : Block(ExpressionType), @relativity : String, &evaluator : ExpressionType, ExpressionType -> Bool) @evaluator = evaluator end @@ -22,7 +22,7 @@ module Spectator::Matchers end # Actually performs the test against the expression. - def match(actual : TestExpression(T)) : MatchData forall T + def match(actual : Expression(T)) : MatchData forall T before, after = change(actual) if before == after FailedMatchData.new(description, "#{actual.label} did not change #{expression.label}", @@ -41,7 +41,7 @@ module Spectator::Matchers # Performs the test against the expression, but inverted. # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : TestExpression(T)) : MatchData forall T + def negated_match(actual : Expression(T)) : MatchData forall T {% raise "The `expect { }.to_not change { }.by_...()` syntax is not supported (ambiguous)." %} end diff --git a/src/spectator/matchers/change_to_matcher.cr b/src/spectator/matchers/change_to_matcher.cr index fb43204..51504ec 100644 --- a/src/spectator/matchers/change_to_matcher.cr +++ b/src/spectator/matchers/change_to_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers private getter expected # Creates a new change matcher. - def initialize(@expression : TestBlock(ExpressionType), @expected : ToType) + def initialize(@expression : Block(ExpressionType), @expected : ToType) end # Short text about the matcher's purpose. @@ -24,7 +24,7 @@ module Spectator::Matchers end # Actually performs the test against the expression. - def match(actual : TestExpression(T)) : MatchData forall T + def match(actual : Expression(T)) : MatchData forall T before, after = change(actual) if before == after FailedMatchData.new(description, "#{actual.label} did not change #{expression.label}", @@ -52,7 +52,7 @@ module Spectator::Matchers # but it is the expected value? # # RSpec doesn't support this syntax either. - def negated_match(actual : TestExpression(T)) : MatchData forall T + def negated_match(actual : Expression(T)) : MatchData forall T {% raise "The `expect { }.to_not change { }.to()` syntax is not supported (ambiguous)." %} end diff --git a/src/spectator/matchers/collection_matcher.cr b/src/spectator/matchers/collection_matcher.cr index 52b4378..277c5c3 100644 --- a/src/spectator/matchers/collection_matcher.cr +++ b/src/spectator/matchers/collection_matcher.cr @@ -1,4 +1,4 @@ -require "../test_value" +require "../value" require "./range_matcher" require "./value_matcher" @@ -13,7 +13,7 @@ module Spectator::Matchers end # Checks whether the matcher is satisifed with the expression given to it. - private def match?(actual : TestExpression(T)) : Bool forall T + private def match?(actual : Expression(T)) : Bool forall T expected.value.includes?(actual.value) end @@ -55,8 +55,8 @@ module Spectator::Matchers lower = center - diff upper = center + diff range = Range.new(lower, upper) - test_value = TestValue.new(range, "#{center} ± #{expected.label}") - RangeMatcher.new(test_value) + value = Value.new(range, "#{center} ± #{expected.label}") + RangeMatcher.new(value) end end end diff --git a/src/spectator/matchers/contain_matcher.cr b/src/spectator/matchers/contain_matcher.cr index afc73fd..fe21182 100644 --- a/src/spectator/matchers/contain_matcher.cr +++ b/src/spectator/matchers/contain_matcher.cr @@ -8,7 +8,7 @@ module Spectator::Matchers private getter expected # Creates the matcher with an expected value. - def initialize(@expected : TestValue(ExpectedType)) + def initialize(@expected : Value(ExpectedType)) end # Short text about the matcher's purpose. @@ -19,7 +19,7 @@ module Spectator::Matchers end # Actually performs the test against the expression. - def match(actual : TestExpression(T)) : MatchData forall T + def match(actual : Expression(T)) : MatchData forall T actual_value = actual.value return unexpected(actual_value, actual.label) unless actual_value.responds_to?(:includes?) @@ -42,7 +42,7 @@ module Spectator::Matchers # Performs the test against the expression, but inverted. # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : TestExpression(T)) : MatchData forall T + def negated_match(actual : Expression(T)) : MatchData forall T actual_value = actual.value return unexpected(actual_value, actual.label) unless actual_value.responds_to?(:includes?) diff --git a/src/spectator/matchers/empty_matcher.cr b/src/spectator/matchers/empty_matcher.cr index e032794..bdfa80d 100644 --- a/src/spectator/matchers/empty_matcher.cr +++ b/src/spectator/matchers/empty_matcher.cr @@ -12,7 +12,7 @@ module Spectator::Matchers end # Checks whether the matcher is satisifed with the expression given to it. - private def match?(actual : TestExpression(T)) : Bool forall T + private def match?(actual : Expression(T)) : Bool forall T actual_value = actual.value return unexpected(actual_value, actual.label) unless actual_value.responds_to?(:empty?) diff --git a/src/spectator/matchers/end_with_matcher.cr b/src/spectator/matchers/end_with_matcher.cr index 319c559..7ff3cd8 100644 --- a/src/spectator/matchers/end_with_matcher.cr +++ b/src/spectator/matchers/end_with_matcher.cr @@ -11,7 +11,7 @@ module Spectator::Matchers private getter expected # Creates the matcher with an expected value. - def initialize(@expected : TestValue(ExpectedType)) + def initialize(@expected : Value(ExpectedType)) end # Short text about the matcher's purpose. @@ -22,7 +22,7 @@ module Spectator::Matchers end # Actually performs the test against the expression. - def match(actual : TestExpression(T)) : MatchData forall T + def match(actual : Expression(T)) : MatchData forall T value = actual.value if value.is_a?(String) || value.responds_to?(:ends_with?) match_ends_with(value, actual.label) @@ -33,7 +33,7 @@ module Spectator::Matchers # Performs the test against the expression, but inverted. # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : TestExpression(T)) : MatchData forall T + def negated_match(actual : Expression(T)) : MatchData forall T value = actual.value if value.is_a?(String) || value.responds_to?(:ends_with?) negated_match_ends_with(value, actual.label) diff --git a/src/spectator/matchers/equality_matcher.cr b/src/spectator/matchers/equality_matcher.cr index bcdcd44..62f9624 100644 --- a/src/spectator/matchers/equality_matcher.cr +++ b/src/spectator/matchers/equality_matcher.cr @@ -12,7 +12,7 @@ module Spectator::Matchers end # Checks whether the matcher is satisifed with the expression given to it. - private def match?(actual : TestExpression(T)) : Bool forall T + private def match?(actual : Expression(T)) : Bool forall T expected.value == actual.value end diff --git a/src/spectator/matchers/exception_matcher.cr b/src/spectator/matchers/exception_matcher.cr index 82d67ab..a27412b 100644 --- a/src/spectator/matchers/exception_matcher.cr +++ b/src/spectator/matchers/exception_matcher.cr @@ -1,4 +1,4 @@ -require "../test_value" +require "../value" require "./failed_match_data" require "./matcher" require "./successful_match_data" @@ -11,11 +11,11 @@ module Spectator::Matchers # Creates the matcher with no expectation of the message. def initialize - @expected = TestValue.new(nil, ExceptionType.to_s) + @expected = Value.new(nil, ExceptionType.to_s) end # Creates the matcher with an expected message. - def initialize(@expected : TestValue(ExpectedType)) + def initialize(@expected : Value(ExpectedType)) end # Short text about the matcher's purpose. @@ -30,7 +30,7 @@ module Spectator::Matchers end # Actually performs the test against the expression. - def match(actual : TestExpression(T)) : MatchData forall T + def match(actual : Expression(T)) : MatchData forall T exception = capture_exception { actual.value } if exception.nil? FailedMatchData.new(description, "#{actual.label} did not raise", expected: ExceptionType.inspect) @@ -61,7 +61,7 @@ module Spectator::Matchers # Performs the test against the expression, but inverted. # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : TestExpression(T)) : MatchData forall T + def negated_match(actual : Expression(T)) : MatchData forall T exception = capture_exception { actual.value } if exception.nil? SuccessfulMatchData.new(description) @@ -91,7 +91,7 @@ module Spectator::Matchers end def with_message(message : T) forall T - value = TestValue.new(message) + value = Value.new(message) ExceptionMatcher(ExceptionType, T).new(value) end @@ -114,13 +114,13 @@ module Spectator::Matchers # Creates a new exception matcher with a message check. def self.create(value, label : String) - expected = TestValue.new(value, label) + expected = Value.new(value, label) ExceptionMatcher(Exception, typeof(value)).new(expected) end # Creates a new exception matcher with a type and message check. def self.create(exception_type : T.class, value, label : String) forall T - expected = TestValue.new(value, label) + expected = Value.new(value, label) ExceptionMatcher(T, typeof(value)).new(expected) end end diff --git a/src/spectator/matchers/greater_than_equal_matcher.cr b/src/spectator/matchers/greater_than_equal_matcher.cr index 08eb88c..cfaf4f1 100644 --- a/src/spectator/matchers/greater_than_equal_matcher.cr +++ b/src/spectator/matchers/greater_than_equal_matcher.cr @@ -12,7 +12,7 @@ module Spectator::Matchers end # Checks whether the matcher is satisifed with the expression given to it. - private def match?(actual : TestExpression(T)) : Bool forall T + private def match?(actual : Expression(T)) : Bool forall T actual.value >= expected.value end diff --git a/src/spectator/matchers/greater_than_matcher.cr b/src/spectator/matchers/greater_than_matcher.cr index 5dfc90c..4aaf6c1 100644 --- a/src/spectator/matchers/greater_than_matcher.cr +++ b/src/spectator/matchers/greater_than_matcher.cr @@ -12,7 +12,7 @@ module Spectator::Matchers end # Checks whether the matcher is satisifed with the expression given to it. - private def match?(actual : TestExpression(T)) : Bool forall T + private def match?(actual : Expression(T)) : Bool forall T actual.value > expected.value end diff --git a/src/spectator/matchers/have_key_matcher.cr b/src/spectator/matchers/have_key_matcher.cr index 5c07590..57224d3 100644 --- a/src/spectator/matchers/have_key_matcher.cr +++ b/src/spectator/matchers/have_key_matcher.cr @@ -12,7 +12,7 @@ module Spectator::Matchers end # Checks whether the matcher is satisifed with the expression given to it. - private def match?(actual : TestExpression(T)) : Bool forall T + private def match?(actual : Expression(T)) : Bool forall T actual_value = actual.value return unexpected(actual_value, actual.label) unless actual_value.responds_to?(:has_key?) diff --git a/src/spectator/matchers/have_matcher.cr b/src/spectator/matchers/have_matcher.cr index 6e535ca..46cd446 100644 --- a/src/spectator/matchers/have_matcher.cr +++ b/src/spectator/matchers/have_matcher.cr @@ -9,7 +9,7 @@ module Spectator::Matchers private getter expected # Creates the matcher with an expected value. - def initialize(@expected : TestValue(ExpectedType)) + def initialize(@expected : Value(ExpectedType)) end # Short text about the matcher's purpose. @@ -20,7 +20,7 @@ module Spectator::Matchers end # Entrypoint for the matcher, forwards to the correct method for string or enumerable. - def match(actual : TestExpression(T)) : MatchData forall T + def match(actual : Expression(T)) : MatchData forall T if (value = actual.value).is_a?(String) match_string(value, actual.label) else @@ -70,7 +70,7 @@ module Spectator::Matchers # Performs the test against the expression, but inverted. # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : TestExpression(T)) : MatchData forall T + def negated_match(actual : Expression(T)) : MatchData forall T if (value = actual.value).is_a?(String) negated_match_string(value, actual.label) else diff --git a/src/spectator/matchers/have_predicate_matcher.cr b/src/spectator/matchers/have_predicate_matcher.cr index 5cb3f94..df4d90e 100644 --- a/src/spectator/matchers/have_predicate_matcher.cr +++ b/src/spectator/matchers/have_predicate_matcher.cr @@ -15,7 +15,7 @@ module Spectator::Matchers end # Actually performs the test against the expression. - def match(actual : TestExpression(T)) : MatchData forall T + def match(actual : Expression(T)) : MatchData forall T snapshot = snapshot_values(actual.value) if match?(snapshot) SuccessfulMatchData.new(description) @@ -26,7 +26,7 @@ module Spectator::Matchers # Performs the test against the expression, but inverted. # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : TestExpression(T)) : MatchData forall T + def negated_match(actual : Expression(T)) : MatchData forall T snapshot = snapshot_values(actual.value) if match?(snapshot) FailedMatchData.new(description, "#{actual.label} has #{expected.label}", values(snapshot).to_a) diff --git a/src/spectator/matchers/have_value_matcher.cr b/src/spectator/matchers/have_value_matcher.cr index 54d5f40..aa059d8 100644 --- a/src/spectator/matchers/have_value_matcher.cr +++ b/src/spectator/matchers/have_value_matcher.cr @@ -12,7 +12,7 @@ module Spectator::Matchers end # Checks whether the matcher is satisifed with the expression given to it. - private def match?(actual : TestExpression(T)) : Bool forall T + private def match?(actual : Expression(T)) : Bool forall T actual_value = actual.value return unexpected(actual_value, actual.label) unless actual_value.responds_to?(:has_value?) diff --git a/src/spectator/matchers/inequality_matcher.cr b/src/spectator/matchers/inequality_matcher.cr index f721ab4..145f5a1 100644 --- a/src/spectator/matchers/inequality_matcher.cr +++ b/src/spectator/matchers/inequality_matcher.cr @@ -12,7 +12,7 @@ module Spectator::Matchers end # Checks whether the matcher is satisifed with the expression given to it. - private def match?(actual : TestExpression(T)) : Bool forall T + private def match?(actual : Expression(T)) : Bool forall T expected.value != actual.value end diff --git a/src/spectator/matchers/instance_matcher.cr b/src/spectator/matchers/instance_matcher.cr index c77531e..bd8f51f 100644 --- a/src/spectator/matchers/instance_matcher.cr +++ b/src/spectator/matchers/instance_matcher.cr @@ -11,7 +11,7 @@ module Spectator::Matchers end # Checks whether the matcher is satisifed with the expression given to it. - private def match?(actual : TestExpression(T)) : Bool forall T + private def match?(actual : Expression(T)) : Bool forall T actual.value.class == Expected end diff --git a/src/spectator/matchers/less_than_equal_matcher.cr b/src/spectator/matchers/less_than_equal_matcher.cr index bc56dab..620afb3 100644 --- a/src/spectator/matchers/less_than_equal_matcher.cr +++ b/src/spectator/matchers/less_than_equal_matcher.cr @@ -12,7 +12,7 @@ module Spectator::Matchers end # Checks whether the matcher is satisifed with the expression given to it. - private def match?(actual : TestExpression(T)) : Bool forall T + private def match?(actual : Expression(T)) : Bool forall T actual.value <= expected.value end diff --git a/src/spectator/matchers/less_than_matcher.cr b/src/spectator/matchers/less_than_matcher.cr index 4e14cd4..05256b1 100644 --- a/src/spectator/matchers/less_than_matcher.cr +++ b/src/spectator/matchers/less_than_matcher.cr @@ -12,7 +12,7 @@ module Spectator::Matchers end # Checks whether the matcher is satisifed with the expression given to it. - private def match?(actual : TestExpression(T)) : Bool forall T + private def match?(actual : Expression(T)) : Bool forall T actual.value < expected.value end diff --git a/src/spectator/matchers/matcher.cr b/src/spectator/matchers/matcher.cr index f5d0d4c..898d319 100644 --- a/src/spectator/matchers/matcher.cr +++ b/src/spectator/matchers/matcher.cr @@ -16,10 +16,10 @@ module Spectator::Matchers abstract def description : String # Actually performs the test against the expression (value or block). - abstract def match(actual : TestExpression(T)) : MatchData forall T + abstract def match(actual : Expression(T)) : MatchData forall T # Performs the test against the expression (value or block), but inverted. # A successful match with `#match` should normally fail for this method, and vice-versa. - abstract def negated_match(actual : TestExpression(T)) : MatchData forall T + abstract def negated_match(actual : Expression(T)) : MatchData forall T end end diff --git a/src/spectator/matchers/nil_matcher.cr b/src/spectator/matchers/nil_matcher.cr index 5334037..187f809 100644 --- a/src/spectator/matchers/nil_matcher.cr +++ b/src/spectator/matchers/nil_matcher.cr @@ -12,7 +12,7 @@ module Spectator::Matchers end # Checks whether the matcher is satisifed with the expression given to it. - private def match?(actual : TestExpression(T)) : Bool forall T + private def match?(actual : Expression(T)) : Bool forall T actual.value.nil? end diff --git a/src/spectator/matchers/predicate_matcher.cr b/src/spectator/matchers/predicate_matcher.cr index bda4f9b..da77dc8 100644 --- a/src/spectator/matchers/predicate_matcher.cr +++ b/src/spectator/matchers/predicate_matcher.cr @@ -10,7 +10,7 @@ module Spectator::Matchers private getter expected # Creates the matcher with a expected values. - def initialize(@expected : TestValue(ExpectedType)) + def initialize(@expected : Value(ExpectedType)) end # Short text about the matcher's purpose. @@ -21,7 +21,7 @@ module Spectator::Matchers end # Actually performs the test against the expression. - def match(actual : TestExpression(T)) : MatchData forall T + def match(actual : Expression(T)) : MatchData forall T snapshot = snapshot_values(actual.value) if match?(snapshot) SuccessfulMatchData.new(description) @@ -32,7 +32,7 @@ module Spectator::Matchers # Performs the test against the expression, but inverted. # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : TestExpression(T)) : MatchData forall T + def negated_match(actual : Expression(T)) : MatchData forall T snapshot = snapshot_values(actual.value) if match?(snapshot) FailedMatchData.new(description, "#{actual.label} is #{expected.label}", values(snapshot).to_a) diff --git a/src/spectator/matchers/range_matcher.cr b/src/spectator/matchers/range_matcher.cr index fe54b7e..b4850f5 100644 --- a/src/spectator/matchers/range_matcher.cr +++ b/src/spectator/matchers/range_matcher.cr @@ -15,7 +15,7 @@ module Spectator::Matchers def inclusive label = expected.label new_range = Range.new(range.begin, range.end, exclusive: false) - expected = TestValue.new(new_range, label) + expected = Value.new(new_range, label) RangeMatcher.new(expected) end @@ -23,12 +23,12 @@ module Spectator::Matchers def exclusive label = expected.label new_range = Range.new(range.begin, range.end, exclusive: true) - expected = TestValue.new(new_range, label) + expected = Value.new(new_range, label) RangeMatcher.new(expected) end # Checks whether the matcher is satisifed with the expression given to it. - private def match?(actual : TestExpression(T)) : Bool forall T + private def match?(actual : Expression(T)) : Bool forall T expected.value.includes?(actual.value) end diff --git a/src/spectator/matchers/receive_matcher.cr b/src/spectator/matchers/receive_matcher.cr index 9a96f82..7f3d233 100644 --- a/src/spectator/matchers/receive_matcher.cr +++ b/src/spectator/matchers/receive_matcher.cr @@ -5,7 +5,7 @@ module Spectator::Matchers struct ReceiveMatcher < StandardMatcher alias Range = ::Range(Int32, Int32) | ::Range(Nil, Int32) | ::Range(Int32, Nil) - def initialize(@expected : TestExpression(Symbol), @args : Mocks::Arguments? = nil, @range : Range? = nil) + def initialize(@expected : Expression(Symbol), @args : Mocks::Arguments? = nil, @range : Range? = nil) end def description : String @@ -13,7 +13,7 @@ module Spectator::Matchers "received message #{@expected.label} #{range ? "#{humanize_range(range)} time(s)" : "At least once"} with #{@args || "any arguments"}" end - def match?(actual : TestExpression(T)) : Bool forall T + def match?(actual : Expression(T)) : Bool forall T calls = Harness.current.mocks.calls_for(actual.value, @expected.value) calls.select! { |call| @args === call.args } if @args if (range = @range) @@ -23,7 +23,7 @@ module Spectator::Matchers end end - def failure_message(actual : TestExpression(T)) : String forall T + def failure_message(actual : Expression(T)) : String forall T range = @range "#{actual.label} did not receive #{@expected.label} #{range ? "#{humanize_range(range)} time(s)" : "at least once"} with #{@args || "any arguments"}" end @@ -33,7 +33,7 @@ module Spectator::Matchers "#{actual.label} received #{@expected.label} #{range ? "#{humanize_range(range)} time(s)" : "at least once"} with #{@args || "any arguments"}" end - def values(actual : TestExpression(T)) forall T + def values(actual : Expression(T)) forall T calls = Harness.current.mocks.calls_for(actual.value, @expected.value) calls.select! { |call| @args === call.args } if @args range = @range @@ -43,7 +43,7 @@ module Spectator::Matchers } end - def negated_values(actual : TestExpression(T)) forall T + def negated_values(actual : Expression(T)) forall T calls = Harness.current.mocks.calls_for(actual.value, @expected.value) calls.select! { |call| @args === call.args } if @args range = @range @@ -115,7 +115,7 @@ module Spectator::Matchers end private struct Count - def initialize(@expected : TestExpression(Symbol), @args : Mocks::Arguments?, @range : Range) + def initialize(@expected : Expression(Symbol), @args : Mocks::Arguments?, @range : Range) end def times diff --git a/src/spectator/matchers/receive_type_matcher.cr b/src/spectator/matchers/receive_type_matcher.cr index c716f05..362707d 100644 --- a/src/spectator/matchers/receive_type_matcher.cr +++ b/src/spectator/matchers/receive_type_matcher.cr @@ -5,7 +5,7 @@ module Spectator::Matchers struct ReceiveTypeMatcher < StandardMatcher alias Range = ::Range(Int32, Int32) | ::Range(Nil, Int32) | ::Range(Int32, Nil) - def initialize(@expected : TestExpression(Symbol), @args : Mocks::Arguments? = nil, @range : Range? = nil) + def initialize(@expected : Expression(Symbol), @args : Mocks::Arguments? = nil, @range : Range? = nil) end def description : String @@ -13,7 +13,7 @@ module Spectator::Matchers "received message #{@expected.label} #{range ? "#{humanize_range(range)} time(s)" : "At least once"} with #{@args || "any arguments"}" end - def match?(actual : TestExpression(T)) : Bool forall T + def match?(actual : Expression(T)) : Bool forall T calls = Harness.current.mocks.calls_for_type(actual.value, @expected.value) calls.select! { |call| @args === call.args } if @args if (range = @range) @@ -23,17 +23,17 @@ module Spectator::Matchers end end - def failure_message(actual : TestExpression(T)) : String forall T + def failure_message(actual : Expression(T)) : String forall T range = @range "#{actual.label} did not receive #{@expected.label} #{range ? "#{humanize_range(range)} time(s)" : "at least once"} with #{@args || "any arguments"}" end - def failure_message_when_negated(actual : TestExpression(T)) : String forall T + def failure_message_when_negated(actual : Expression(T)) : String forall T range = @range "#{actual.label} received #{@expected.label} #{range ? "#{humanize_range(range)} time(s)" : "at least once"} with #{@args || "any arguments"}" end - def values(actual : TestExpression(T)) forall T + def values(actual : Expression(T)) forall T calls = Harness.current.mocks.calls_for_type(T, @expected.value) calls.select! { |call| @args === call.args } if @args range = @range @@ -43,7 +43,7 @@ module Spectator::Matchers } end - def negated_values(actual : TestExpression(T)) forall T + def negated_values(actual : Expression(T)) forall T calls = Harness.current.mocks.calls_for_type(T, @expected.value) calls.select! { |call| @args === call.args } if @args range = @range @@ -115,7 +115,7 @@ module Spectator::Matchers end private struct Count - def initialize(@expected : TestExpression(Symbol), @args : Mocks::Arguments?, @range : Range) + def initialize(@expected : Expression(Symbol), @args : Mocks::Arguments?, @range : Range) end def times diff --git a/src/spectator/matchers/reference_matcher.cr b/src/spectator/matchers/reference_matcher.cr index 74ce6e7..8385eb6 100644 --- a/src/spectator/matchers/reference_matcher.cr +++ b/src/spectator/matchers/reference_matcher.cr @@ -12,7 +12,7 @@ module Spectator::Matchers end # Checks whether the matcher is satisifed with the expression given to it. - private def match?(actual : TestExpression(T)) : Bool forall T + private def match?(actual : Expression(T)) : Bool forall T value = expected.value if value && value.responds_to?(:same?) value.same?(actual.value) diff --git a/src/spectator/matchers/respond_matcher.cr b/src/spectator/matchers/respond_matcher.cr index 87f95a9..6d36f80 100644 --- a/src/spectator/matchers/respond_matcher.cr +++ b/src/spectator/matchers/respond_matcher.cr @@ -14,7 +14,7 @@ module Spectator::Matchers end # Actually performs the test against the expression. - def match(actual : TestExpression(T)) : MatchData forall T + def match(actual : Expression(T)) : MatchData forall T snapshot = snapshot_values(actual.value) if snapshot.values.all? SuccessfulMatchData.new(description) @@ -25,7 +25,7 @@ module Spectator::Matchers # Performs the test against the expression, but inverted. # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : TestExpression(T)) : MatchData forall T + def negated_match(actual : Expression(T)) : MatchData forall T snapshot = snapshot_values(actual.value) if snapshot.values.any? FailedMatchData.new(description, "#{actual.label} responds to #{label}", values(snapshot).to_a) diff --git a/src/spectator/matchers/size_matcher.cr b/src/spectator/matchers/size_matcher.cr index f707159..ea51c89 100644 --- a/src/spectator/matchers/size_matcher.cr +++ b/src/spectator/matchers/size_matcher.cr @@ -12,7 +12,7 @@ module Spectator::Matchers end # Checks whether the matcher is satisifed with the expression given to it. - private def match?(actual : TestExpression(T)) : Bool forall T + private def match?(actual : Expression(T)) : Bool forall T actual_value = actual.value return unexpected(actual_value, actual.label) unless actual_value.responds_to?(:size?) diff --git a/src/spectator/matchers/size_of_matcher.cr b/src/spectator/matchers/size_of_matcher.cr index 8784cea..ad06f99 100644 --- a/src/spectator/matchers/size_of_matcher.cr +++ b/src/spectator/matchers/size_of_matcher.cr @@ -12,7 +12,7 @@ module Spectator::Matchers end # Checks whether the matcher is satisifed with the expression given to it. - private def match?(actual : TestExpression(T)) : Bool forall T + private def match?(actual : Expression(T)) : Bool forall T actual_value = actual.value return unexpected(actual_value, actual.label) unless actual_value.responds_to?(:size?) diff --git a/src/spectator/matchers/standard_matcher.cr b/src/spectator/matchers/standard_matcher.cr index 52cdc67..5b9f026 100644 --- a/src/spectator/matchers/standard_matcher.cr +++ b/src/spectator/matchers/standard_matcher.cr @@ -1,4 +1,4 @@ -require "../test_value" +require "../expression" require "./failed_match_data" require "./matcher" require "./successful_match_data" @@ -23,7 +23,7 @@ module Spectator::Matchers # If it returns true, then a `SuccessfulMatchData` instance is returned. # Otherwise, a `FailedMatchData` instance is returned. # Additionally, `#failure_message` and `#values` are called for a failed match. - def match(actual : TestExpression(T)) : MatchData forall T + def match(actual : Expression(T)) : MatchData forall T if match?(actual) SuccessfulMatchData.new(description) else @@ -38,7 +38,7 @@ module Spectator::Matchers # If it returns true, then a `SuccessfulMatchData` instance is returned. # Otherwise, a `FailedMatchData` instance is returned. # Additionally, `#failure_message_when_negated` and `#negated_values` are called for a failed match. - def negated_match(actual : TestExpression(T)) : MatchData forall T + def negated_match(actual : Expression(T)) : MatchData forall T # TODO: Invert description. if does_not_match?(actual) SuccessfulMatchData.new(description) @@ -53,7 +53,7 @@ module Spectator::Matchers # # The message should typically only contain the test expression labels. # Actual values should be returned by `#values`. - private abstract def failure_message(actual : TestExpression(T)) : String forall T + private abstract def failure_message(actual : Expression(T)) : String forall T # Message displayed when the matcher isn't satisifed and is negated. # This is essentially what would satisfy the matcher if it wasn't negated. @@ -66,12 +66,12 @@ module Spectator::Matchers # # The message should typically only contain the test expression labels. # Actual values should be returned by `#values`. - private def failure_message_when_negated(actual : TestExpression(T)) : String forall T + private def failure_message_when_negated(actual : Expression(T)) : String forall T raise "Negation with #{self.class} is not supported." end # Checks whether the matcher is satisifed with the expression given to it. - private abstract def match?(actual : TestExpression(T)) : Bool forall T + private abstract def match?(actual : Expression(T)) : Bool forall T # If the expectation is negated, then this method is called instead of `#match?`. # @@ -79,7 +79,7 @@ module Spectator::Matchers # If the matcher requires custom handling of negated matches, # then this method should be overriden. # Remember to override `#failure_message_when_negated` as well. - private def does_not_match?(actual : TestExpression(T)) : Bool forall T + private def does_not_match?(actual : Expression(T)) : Bool forall T !match?(actual) end @@ -101,7 +101,7 @@ module Spectator::Matchers # # The values should typically only contain the test expression values, not the labels. # Labeled should be returned by `#failure_message`. - private def values(actual : TestExpression(T)) forall T + private def values(actual : Expression(T)) forall T {actual: actual.value.inspect} end @@ -123,7 +123,7 @@ module Spectator::Matchers # # The values should typically only contain the test expression values, not the labels. # Labeled should be returned by `#failure_message_when_negated`. - private def negated_values(actual : TestExpression(T)) forall T + private def negated_values(actual : Expression(T)) forall T values(actual) end end diff --git a/src/spectator/matchers/start_with_matcher.cr b/src/spectator/matchers/start_with_matcher.cr index b459bb4..e8d0059 100644 --- a/src/spectator/matchers/start_with_matcher.cr +++ b/src/spectator/matchers/start_with_matcher.cr @@ -10,7 +10,7 @@ module Spectator::Matchers private getter expected # Creates the matcher with an expected value. - def initialize(@expected : TestValue(ExpectedType)) + def initialize(@expected : Value(ExpectedType)) end # Short text about the matcher's purpose. @@ -21,7 +21,7 @@ module Spectator::Matchers end # Actually performs the test against the expression. - def match(actual : TestExpression(T)) : MatchData forall T + def match(actual : Expression(T)) : MatchData forall T value = actual.value if value.is_a?(String) || value.responds_to?(:starts_with?) match_starts_with(value, actual.label) @@ -32,7 +32,7 @@ module Spectator::Matchers # Performs the test against the expression, but inverted. # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : TestExpression(T)) : MatchData forall T + def negated_match(actual : Expression(T)) : MatchData forall T value = actual.value if value.is_a?(String) || value.responds_to?(:starts_with?) negated_match_starts_with(value, actual.label) diff --git a/src/spectator/matchers/truthy_matcher.cr b/src/spectator/matchers/truthy_matcher.cr index bc26a6e..380dd26 100644 --- a/src/spectator/matchers/truthy_matcher.cr +++ b/src/spectator/matchers/truthy_matcher.cr @@ -28,7 +28,7 @@ module Spectator::Matchers # expect(0).to be < 1 # ``` def <(value) - expected = TestValue.new(value) + expected = Value.new(value) LessThanMatcher.new(expected) end @@ -38,7 +38,7 @@ module Spectator::Matchers # expect(0).to be <= 1 # ``` def <=(value) - expected = TestValue.new(value) + expected = Value.new(value) LessThanEqualMatcher.new(expected) end @@ -48,7 +48,7 @@ module Spectator::Matchers # expect(2).to be > 1 # ``` def >(value) - expected = TestValue.new(value) + expected = Value.new(value) GreaterThanMatcher.new(expected) end @@ -58,7 +58,7 @@ module Spectator::Matchers # expect(2).to be >= 1 # ``` def >=(value) - expected = TestValue.new(value) + expected = Value.new(value) GreaterThanEqualMatcher.new(expected) end @@ -68,7 +68,7 @@ module Spectator::Matchers # expect(0).to be == 0 # ``` def ==(value) - expected = TestValue.new(value) + expected = Value.new(value) EqualityMatcher.new(expected) end @@ -78,12 +78,12 @@ module Spectator::Matchers # expect(0).to be != 1 # ``` def !=(value) - expected = TestValue.new(value) + expected = Value.new(value) InequalityMatcher.new(expected) end # Checks whether the matcher is satisifed with the expression given to it. - private def match?(actual : TestExpression(T)) : Bool forall T + private def match?(actual : Expression(T)) : Bool forall T @truthy == !!actual.value end diff --git a/src/spectator/matchers/type_matcher.cr b/src/spectator/matchers/type_matcher.cr index f8401d4..dc88a24 100644 --- a/src/spectator/matchers/type_matcher.cr +++ b/src/spectator/matchers/type_matcher.cr @@ -12,7 +12,7 @@ module Spectator::Matchers end # Checks whether the matcher is satisifed with the expression given to it. - private def match?(actual : TestExpression(T)) : Bool forall T + private def match?(actual : Expression(T)) : Bool forall T actual.value.is_a?(Expected) end diff --git a/src/spectator/matchers/unordered_array_matcher.cr b/src/spectator/matchers/unordered_array_matcher.cr index e408809..aeb8b2b 100644 --- a/src/spectator/matchers/unordered_array_matcher.cr +++ b/src/spectator/matchers/unordered_array_matcher.cr @@ -8,7 +8,7 @@ module Spectator::Matchers private getter expected # Creates the matcher with an expected value. - def initialize(@expected : TestValue(Array(ExpectedType))) + def initialize(@expected : Value(Array(ExpectedType))) end # Short text about the matcher's purpose. @@ -19,7 +19,7 @@ module Spectator::Matchers end # Actually performs the test against the expression. - def match(actual : TestExpression(T)) : MatchData forall T + def match(actual : Expression(T)) : MatchData forall T actual_value = actual.value return unexpected(actual_value, actual.label) unless actual_value.responds_to?(:to_a) @@ -41,7 +41,7 @@ module Spectator::Matchers # Performs the test against the expression, but inverted. # A successful match with `#match` should normally fail for this method, and vice-versa. - def negated_match(actual : TestExpression(T)) : MatchData forall T + def negated_match(actual : Expression(T)) : MatchData forall T actual_value = actual.value return unexpected(actual_value, actual.label) unless actual_value.responds_to?(:to_a) diff --git a/src/spectator/matchers/value_matcher.cr b/src/spectator/matchers/value_matcher.cr index 5049320..c7dffe9 100644 --- a/src/spectator/matchers/value_matcher.cr +++ b/src/spectator/matchers/value_matcher.cr @@ -22,7 +22,7 @@ module Spectator::Matchers # Creates the value matcher. # The expected value is stored for later use. - def initialize(@expected : TestValue(ExpectedType)) + def initialize(@expected : Value(ExpectedType)) end # Additional information about the match failure. @@ -40,7 +40,7 @@ module Spectator::Matchers # actual: "bar", # } # ``` - private def values(actual : TestExpression(T)) forall T + private def values(actual : Expression(T)) forall T super.merge(expected: expected.value.inspect) end @@ -60,7 +60,7 @@ module Spectator::Matchers # actual: "bar", # } # ``` - private def negated_values(actual : TestExpression(T)) forall T + private def negated_values(actual : Expression(T)) forall T super.merge(expected: "Not #{expected.value.inspect}") end end diff --git a/src/spectator/mocks/expect_any_instance.cr b/src/spectator/mocks/expect_any_instance.cr index 9d9769e..0ccee05 100644 --- a/src/spectator/mocks/expect_any_instance.cr +++ b/src/spectator/mocks/expect_any_instance.cr @@ -6,9 +6,9 @@ module Spectator::Mocks end def to(stub : MethodStub) : Nil - actual = TestValue.new(T) + actual = Value.new(T) Harness.current.mocks.expect(T, stub) - value = TestValue.new(stub.name, stub.to_s) + value = Value.new(stub.name, stub.to_s) matcher = Matchers::ReceiveTypeMatcher.new(value, stub.arguments?) partial = Expectations::ExpectationPartial.new(actual, @source) partial.to_eventually(matcher) From 175ce8f293084838d538e64632255ea4627c939e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Jan 2021 11:02:49 -0700 Subject: [PATCH 109/399] Include matchers DSL --- src/spectator/dsl.cr | 1 + src/spectator/test_context.cr | 1 + 2 files changed, 2 insertions(+) diff --git a/src/spectator/dsl.cr b/src/spectator/dsl.cr index 4fe0e43..e7b4b79 100644 --- a/src/spectator/dsl.cr +++ b/src/spectator/dsl.cr @@ -4,6 +4,7 @@ require "./dsl/examples" require "./dsl/expectations" require "./dsl/groups" require "./dsl/hooks" +require "./dsl/matchers" require "./dsl/top" require "./dsl/values" diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr index 0595ee8..46fab64 100644 --- a/src/spectator/test_context.cr +++ b/src/spectator/test_context.cr @@ -10,6 +10,7 @@ class SpectatorTestContext < SpectatorContext include ::Spectator::DSL::Expectations include ::Spectator::DSL::Groups include ::Spectator::DSL::Hooks + include ::Spectator::DSL::Matchers include ::Spectator::DSL::Values # Initial implicit subject for tests. From 0992bad7eb92ba24970a0e45b12b982c7cd15e63 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Jan 2021 11:03:05 -0700 Subject: [PATCH 110/399] Get harness compiling again --- src/spectator/harness.cr | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 37bf1eb..2b42c8f 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -15,18 +15,20 @@ module Spectator # This sets up the harness so that the test code can use it. # The framework does the following: # ``` - # result = Harness.run { delegate.call(example) } + # result = Harness.run { run_example_code } # # Do something with the result. # ``` + # # Then from the test code, the harness can be accessed via `.current` like so: # ``` # harness = ::Spectator::Harness.current # # Do something with the harness. # ``` + # # Of course, the end-user shouldn't see this or work directly with the harness. - # Instead, methods the user calls can access it. - # For instance, an assertion reporting a result. - private class Harness + # Instead, methods the test calls can access it. + # For instance, an expectation reporting a result. + class Harness # Retrieves the harness for the current running example. class_getter! current : self @@ -54,6 +56,10 @@ module Spectator translate(*outcome) end + def report(expectation) + # TODO + end + # Stores a block of code to be executed later. # All deferred blocks run just before the `#run` method completes. def defer(&block) : Nil From 97923d6bcdb44e32da7290dd2a134310b89edd13 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Jan 2021 11:11:42 -0700 Subject: [PATCH 111/399] Handle nil labels --- src/spectator/block.cr | 4 ++-- src/spectator/value.cr | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/spectator/block.cr b/src/spectator/block.cr index ad6a861..5b365a1 100644 --- a/src/spectator/block.cr +++ b/src/spectator/block.cr @@ -16,7 +16,7 @@ module Spectator # The *proc* will be called to evaluate the value of the expression. # The *label* is usually the Crystal code for the *proc*. # It can be nil if it isn't available. - def initialize(@block : -> T, label : Label) + def initialize(@block : -> T, label : Label = nil) super(label) end @@ -24,7 +24,7 @@ module Spectator # The block will be called to evaluate the value of the expression. # The *label* is usually the Crystal code for the *block*. # It can be nil if it isn't available. - def initialize(label : Label, &@block : -> T) + def initialize(label : Label = nil, &@block : -> T) super(label) end diff --git a/src/spectator/value.cr b/src/spectator/value.cr index 9d04871..dd81de7 100644 --- a/src/spectator/value.cr +++ b/src/spectator/value.cr @@ -14,9 +14,16 @@ module Spectator # Creates the value. # Expects the *value* of the expression and a *label* describing it. # The *label* is usually the Crystal code evaluating to the *value*. - # It can be nil if it isn't available. def initialize(@value : T, label : Label) super(label) end + + # Creates the value. + # Expects the *value* of the expression. + # It can be nil if it isn't available. + # A label is generated by calling `#inspect` on the *value*. + def initialize(@value : T) + super(@value.inspect) + end end end From 3ec267abbb675e3a3e19f16029913051c22dcc28 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Jan 2021 11:12:41 -0700 Subject: [PATCH 112/399] Fix reporting for should syntax --- src/spectator/should.cr | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/spectator/should.cr b/src/spectator/should.cr index 4c63d2c..3be851f 100644 --- a/src/spectator/should.cr +++ b/src/spectator/should.cr @@ -17,21 +17,19 @@ class Object # require "spectator/should" # ``` def should(matcher) - # First argument of the `Expectation` initializer is the expression label. - # However, since this isn't a macro and we can't "look behind" this method call - # to see what it was invoked on, the argument is an empty string. - # Additionally, the source file and line can't be obtained. - actual = ::Spectator::TestValue.new(self) - source = ::Spectator::Source.new(__FILE__, __LINE__) - ::Spectator::Expectations::ExpectationPartial.new(actual, source).to(matcher) + actual = ::Spectator::Value.new(self) + match_data = matcher.match(actual) + expectation = ::Spectator::Expectation.new(match_data) + ::Spectator::Harness.current.report(expectation) end # Works the same as `#should` except the condition is inverted. # When `#should` succeeds, this method will fail, and vice-versa. def should_not(matcher) - actual = ::Spectator::TestValue.new(self) - source = ::Spectator::Source.new(__FILE__, __LINE__) - ::Spectator::Expectations::ExpectationPartial.new(actual, source).to_not(matcher) + actual = ::Spectator::Value.new(self) + match_data = matcher.negated_match(actual) + expectation = ::Spectator::Expectation.new(match_data) + ::Spectator::Harness.current.report(expectation) end # Works the same as `#should` except that the condition check is postphoned. @@ -51,17 +49,19 @@ struct Proc(*T, R) # Extension method to create an expectation for a block of code (proc). # Depending on the matcher, the proc may be executed multiple times. def should(matcher) - actual = ::Spectator::TestBlock.new(self) - source = ::Spectator::Source.new(__FILE__, __LINE__) - ::Spectator::Expectations::ExpectationPartial.new(actual, source).to(matcher) + actual = ::Spectator::Block.new(self) + match_data = matcher.match(actual) + expectation = ::Spectator::Expectation.new(match_data) + ::Spectator::Harness.current.report(expectation) end # Works the same as `#should` except the condition is inverted. # When `#should` succeeds, this method will fail, and vice-versa. def should_not(matcher) - actual = ::Spectator::TestBlock.new(self) - source = ::Spectator::Source.new(__FILE__, __LINE__) - ::Spectator::Expectations::BlockExpectationPartial.new(actual, source).to_not(matcher) + actual = ::Spectator::Block.new(self) + match_data = matcher.negated_match(actual) + expectation = ::Spectator::Expectation.new(match_data) + ::Spectator::Harness.current.report(expectation) end end From 3527507639edcb9e20684341d7d0c3b4af8025a8 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Jan 2021 11:13:20 -0700 Subject: [PATCH 113/399] Remove debug --- src/spectator/expectation.cr | 1 - 1 file changed, 1 deletion(-) diff --git a/src/spectator/expectation.cr b/src/spectator/expectation.cr index 091ba2b..031e41d 100644 --- a/src/spectator/expectation.cr +++ b/src/spectator/expectation.cr @@ -27,7 +27,6 @@ module Spectator # The *expression* is the actual value being tested and its label. # The *source* is the location of where this expectation was defined. def initialize(@expression : Expression(T), @source : Source) - puts "TARGET: #{@expression} @ #{@source}" end # Asserts that some criteria defined by the matcher is satisfied. From d738494fdf1baea8eb77f679c2f2de98ea3b31f2 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Jan 2021 11:49:43 -0700 Subject: [PATCH 114/399] Cleanup example name output --- src/spectator/example.cr | 10 ++++++++++ src/spectator/spec_node.cr | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index a90931e..5ff9750 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -82,6 +82,16 @@ module Spectator with context yield end + # Constructs the full name or description of the example. + # This prepends names of groups this example is part of. + def to_s(io) + if name? + super + else + io << "" + end + end + # Exposes information about the example useful for debugging. def inspect(io) # Full example name. diff --git a/src/spectator/spec_node.cr b/src/spectator/spec_node.cr index a559938..7a00173 100644 --- a/src/spectator/spec_node.cr +++ b/src/spectator/spec_node.cr @@ -53,7 +53,7 @@ module Spectator # Prefix with group's full name if the node belongs to a group. if (group = @group) - io << group + group.to_s(io) # Add padding between the node names # only if the names don't appear to be symbolic. @@ -63,7 +63,7 @@ module Spectator (name.starts_with?('#') || name.starts_with?('.'))) end - io << name + name.to_s(io) end end end From 5ea83f51bb91a19b4b93aec952b1d112a4ecda1a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Jan 2021 12:10:52 -0700 Subject: [PATCH 115/399] Cache implicit subject --- src/spectator/test_context.cr | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr index 46fab64..c32119f 100644 --- a/src/spectator/test_context.cr +++ b/src/spectator/test_context.cr @@ -13,6 +13,8 @@ class SpectatorTestContext < SpectatorContext include ::Spectator::DSL::Matchers include ::Spectator::DSL::Values + @subject = ::Spectator::LazyWrapper.new + # Initial implicit subject for tests. # This method should be overridden by example groups when an object is described. private def _spectator_implicit_subject @@ -22,8 +24,7 @@ class SpectatorTestContext < SpectatorContext # Initial subject for tests. # Returns the implicit subject. # This method should be overridden when an explicit subject is defined by the DSL. - # TODO: Subject needs to be cached. private def subject - _spectator_implicit_subject + @subject.get { _spectator_implicit_subject } end end From 36c2a5d3688a5703e39197c0e6e86df195e9644e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Jan 2021 16:28:33 -0700 Subject: [PATCH 116/399] Logic for around_each hooks --- src/spectator/example.cr | 83 +++++++++++++++++++++++----- src/spectator/example_group.cr | 48 ++++++++++++++++ src/spectator/example_procsy_hook.cr | 56 +++++++++++++++++++ 3 files changed, 173 insertions(+), 14 deletions(-) create mode 100644 src/spectator/example_procsy_hook.cr diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 5ff9750..6afac44 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -46,25 +46,48 @@ module Spectator # Returns the result of the execution. # The result will also be stored in `#result`. def run : Result - @@current = self Log.debug { "Running example #{self}" } Log.warn { "Example #{self} already ran" } if @finished - @result = Harness.run do - if (parent = group?) - parent.call_once_before_all - parent.call_before_each(self) - end - @entrypoint.call(self) + previous_example = @@current + @@current = self + + begin + @result = Harness.run do + if (parent = group?) + parent.call_around_each(self) { run_internal } + else + run_internal + end + end + ensure + @@current = previous_example @finished = true - - if (parent = group?) - parent.call_after_each(self) - parent.call_once_after_all if parent.finished? - end end - ensure - @@current = nil + end + + private def run_internal + run_before_hooks + run_test + run_after_hooks + end + + private def run_before_hooks : Nil + return unless (parent = group?) + + parent.call_once_before_all + parent.call_before_each(self) + end + + private def run_after_hooks : Nil + return unless (parent = group?) + + parent.call_after_each(self) + parent.call_once_after_all if parent.finished? + end + + private def run_test : Nil + @entrypoint.call(self) @finished = true end @@ -107,5 +130,37 @@ module Spectator io << result end + + # Wraps an example to behave like a `Proc`. + # This is typically used for an *around_each* hook. + # Invoking `#call` or `#run` will run the example. + struct Procsy + # Underlying example that will run. + getter example : Example + + # Creates the example proxy. + # The *example* should be run eventually. + # The *proc* defines the block of code to run when `#call` or `#run` is invoked. + def initialize(@example : Example, &@proc : ->) + end + + # Invokes the proc. + def call : Nil + @proc.call + end + + # Invokes the proc. + def run : Nil + @proc.call + end + + # Creates a new procsy for a block and the example from this instance. + def wrap(&block : ->) : self + self.class.new(@example, &block) + end + + # Allow instance to behave like an example. + forward_missing_to @example + end end end diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index 056950c..afd89b6 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -1,5 +1,6 @@ require "./events" require "./spec_node" +require "./example_procsy_hook" module Spectator # Collection of examples and sub-groups. @@ -101,5 +102,52 @@ module Spectator @nodes << node node.group = self end + + @around_hooks = [] of ExampleProcsyHook + + # Adds a hook to be invoked when the *{{name.id}}* event occurs. + def add_around_each_hook(hook : ExampleProcsyHook) : Nil + @around_hooks << hook + end + + # Defines a hook for the *around_each* event. + # The block of code given to this method is invoked when the event occurs. + # The current example is provided as a block argument. + def around_each(&block : Example::Procsy ->) : Nil + hook = ExampleProcsyHook.new(label: "around_each", &block) + add_around_each_hook(hook) + end + + + # Signals that the *around_each* event has occurred. + # All hooks associated with the event will be called. + def call_around_each(example : Example, &block : -> _) : Nil + # Avoid overhead if there's no hooks. + return yield if @around_hooks.empty? + + # Start with a procsy that wraps the original code. + procsy = Example::Procsy.new(example, &block) + procsy = wrap_around_each(procsy) + procsy.call + end + + # Wraps a procsy with the *around_each* hooks from this example group. + # The returned procsy will call each hook then *procsy*. + protected def wrap_around_each(procsy : Example::Procsy) : Example::Procsy + # Avoid overhead if there's no hooks. + return procsy if @around_hooks.empty? + + # Wrap each hook with the next. + outer = procsy + @around_hooks.each do |hook| + outer = hook.wrap(outer) + end + + # If there's a parent, wrap the procsy with its hooks. + # Otherwise, return the outermost procsy. + return outer unless (parent = group?) + + parent.wrap_around_each(outer) + end end end diff --git a/src/spectator/example_procsy_hook.cr b/src/spectator/example_procsy_hook.cr new file mode 100644 index 0000000..0b6ea63 --- /dev/null +++ b/src/spectator/example_procsy_hook.cr @@ -0,0 +1,56 @@ +require "./label" +require "./source" + +module Spectator + # Information about a hook tied to an example and a proc to invoke it. + class ExampleProcsyHook + # Location of the hook in source code. + getter! source : Source + + # User-defined description of the hook. + getter! label : Label + + @proc : Example::Procsy -> + + # Creates the hook with a proc. + # The *proc* will be called when the hook is invoked. + # A *source* and *label* can be provided for debugging. + def initialize(@proc : (Example::Procsy ->), *, @source : Source? = nil, @label : Label = nil) + end + + # Creates the hook with a block. + # The block must take a single argument - the current example wrapped in a procsy. + # The block will be executed when the hook is invoked. + # A *source* and *label* can be provided for debugging. + def initialize(*, @source : Source? = nil, @label : Label = nil, &block : Example::Procsy -> _) + @proc = block + end + + # Invokes the hook. + # The *example* refers to the current example. + def call(procsy : Example::Procsy) : Nil + @proc.call(procsy) + end + + # Creates an example procsy that invokes this hook. + def wrap(procsy : Example::Procsy) : Example::Procsy + procsy.wrap { call(procsy) } + end + + # Produces the string representation of the hook. + # Includes the source and label if they're not nil. + def to_s(io) + io << "example hook" + + if (label = @label) + io << ' ' + io << label + end + + if (source = @source) + io << " @ " + io << source + end + end + end +end From 153933b0449cd76734613d6072318e472e6d2a18 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Jan 2021 16:47:40 -0700 Subject: [PATCH 117/399] Add DSL and code to create around_each hooks --- src/spectator/dsl/builder.cr | 6 ++++++ src/spectator/dsl/hooks.cr | 10 ++++++++++ src/spectator/spec_builder.cr | 14 ++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr index 8a2fdd4..ebeeca5 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -59,6 +59,12 @@ module Spectator::DSL @@builder.after_each(hook) end + # Defines a block of code to execute around every example in the current group. + def around_each(source = nil, label = "around_each", &block : Example::Procsy ->) + hook = ExampleProcsyHook.new(source: source, label: label, &block) + @@builder.around_each(hook) + end + # Sets the configuration of the spec. # # See `Spec::Builder#config=` for usage details. diff --git a/src/spectator/dsl/hooks.cr b/src/spectator/dsl/hooks.cr index a14ee9e..33b3428 100644 --- a/src/spectator/dsl/hooks.cr +++ b/src/spectator/dsl/hooks.cr @@ -66,5 +66,15 @@ module Spectator::DSL # The block will be run in the context of the current running example. # This means that values defined by `let` and `subject` are available. define_example_hook :after_each + + # Defines a block of code that will be invoked around every example in the group. + # The block will be run in the context of the current running example. + # This means that values defined by `let` and `subject` are available. + # + # The block will execute before the example. + # An `Example::Procsy` is passed to the block. + # The `Example::Procsy#run` method should be called to ensure the example runs. + # More code can run afterwards (in the block). + define_example_hook :around_each end end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 770a7cb..8afbb6e 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -149,6 +149,20 @@ module Spectator current_group.after_each(&block) end + # Attaches a hook to be invoked around every example in the current group. + # The current example in procsy form is provided as a block argument. + def around_each(hook) + Log.trace { "Add around_each hook #{hook}" } + current_group.add_around_each_hook(hook) + end + + # Defines a block of code to execute around every example in the current group. + # The current example in procsy form is provided as a block argument. + def around_each(&block : Example -> _) + Log.trace { "Add around_each hook" } + current_group.around_each(&block) + end + # Builds the configuration to use for the spec. # A `ConfigBuilder` is yielded to the block provided to this method. # That builder will be used to create the configuration. From 13061cfb381a1e0b7d1f0775ecf5e2fe0668482a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Jan 2021 16:52:16 -0700 Subject: [PATCH 118/399] Workaround context scope not used in method delegation --- src/spectator/example.cr | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 6afac44..c1fa995 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -99,12 +99,28 @@ module Spectator # An error will be raised if *klass* doesn't match the test context's type. # The block given to this method will be executed within the test context. # + # The context casted to an instance of *klass* is provided as a block argument. + # # TODO: Benchmark compiler performance using this method versus client-side casting in a proc. - def with_context(klass) + protected def with_context(klass) context = klass.cast(@context) with context yield end + # Casts the example's test context to a specific type. + # This is an advanced method intended for internal usage only. + # + # The *klass* defines the type of the test context. + # This is typically only known by the code constructing the example. + # An error will be raised if *klass* doesn't match the test context's type. + # + # The context casted to an instance of *klass* is returned. + # + # TODO: Benchmark compiler performance using this method versus client-side casting in a proc. + protected def cast_context(klass) + klass.cast(@context) + end + # Constructs the full name or description of the example. # This prepends names of groups this example is part of. def to_s(io) @@ -159,6 +175,20 @@ module Spectator self.class.new(@example, &block) end + # Executes code within the example's test context. + # This is an advanced method intended for internal usage only. + # + # The *klass* defines the type of the test context. + # This is typically only known by the code constructing the example. + # An error will be raised if *klass* doesn't match the test context's type. + # The block given to this method will be executed within the test context. + # + # TODO: Benchmark compiler performance using this method versus client-side casting in a proc. + protected def with_context(klass) + context = @example.cast_context(klass) + with context yield + end + # Allow instance to behave like an example. forward_missing_to @example end From cd519178acb80e681bdea8e1cc1616907ba8a03a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Jan 2021 16:52:41 -0700 Subject: [PATCH 119/399] Remove whitespace --- src/spectator/example_group.cr | 1 - 1 file changed, 1 deletion(-) diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index afd89b6..aac71e8 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -118,7 +118,6 @@ module Spectator add_around_each_hook(hook) end - # Signals that the *around_each* event has occurred. # All hooks associated with the event will be called. def call_around_each(example : Example, &block : -> _) : Nil From 57c9333c1f4d86fb082824af64c6c0c78b7646b8 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Jan 2021 17:04:42 -0700 Subject: [PATCH 120/399] Match hook ordering of RSpec Addresses https://github.com/icy-arctic-fox/spectator/issues/12 --- src/spectator/example.cr | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index c1fa995..f5169a5 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -54,11 +54,15 @@ module Spectator begin @result = Harness.run do + group?.try(&.call_once_before_all) if (parent = group?) parent.call_around_each(self) { run_internal } else run_internal end + if (parent = group?) + parent.call_once_after_all if parent.finished? + end end ensure @@current = previous_example @@ -67,9 +71,9 @@ module Spectator end private def run_internal - run_before_hooks + group?.try(&.call_before_each(self)) run_test - run_after_hooks + group?.try(&.call_after_each(self)) end private def run_before_hooks : Nil From a7ac170153abd4beaed1b86964ab9745910ec792 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Jan 2021 17:07:07 -0700 Subject: [PATCH 121/399] Remove unused methods --- src/spectator/example.cr | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index f5169a5..e26a5f4 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -72,27 +72,9 @@ module Spectator private def run_internal group?.try(&.call_before_each(self)) - run_test - group?.try(&.call_after_each(self)) - end - - private def run_before_hooks : Nil - return unless (parent = group?) - - parent.call_once_before_all - parent.call_before_each(self) - end - - private def run_after_hooks : Nil - return unless (parent = group?) - - parent.call_after_each(self) - parent.call_once_after_all if parent.finished? - end - - private def run_test : Nil @entrypoint.call(self) @finished = true + group?.try(&.call_after_each(self)) end # Executes code within the example's test context. From 73dc7ae811728e827075fa18f593dd9efb3843de Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Jan 2021 17:16:31 -0700 Subject: [PATCH 122/399] Fix copy/paste docs --- src/spectator/example_group.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index aac71e8..a21de28 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -105,7 +105,7 @@ module Spectator @around_hooks = [] of ExampleProcsyHook - # Adds a hook to be invoked when the *{{name.id}}* event occurs. + # Adds a hook to be invoked when the *around_each* event occurs. def add_around_each_hook(hook : ExampleProcsyHook) : Nil @around_hooks << hook end From 98a29309ff7746999b8ee7769c6ea218165fc53e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 20 Jan 2021 21:36:18 -0700 Subject: [PATCH 123/399] Remove assert methods --- src/spectator/dsl/expectations.cr | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/spectator/dsl/expectations.cr b/src/spectator/dsl/expectations.cr index 851b4f3..bebbb9f 100644 --- a/src/spectator/dsl/expectations.cr +++ b/src/spectator/dsl/expectations.cr @@ -13,28 +13,6 @@ module Spectator::DSL raise AssertionFailed.new(Source.new(_file, _line), message) end - # Checks that the specified condition is true. - # Raises `AssertionFailed` if *condition* is false. - # The *message* is passed to the exception. - # - # ``` - # assert(value == 42, "That's not the answer to everything.") - # ``` - def assert(condition, message, *, _file = __FILE__, _line = __LINE__) - fail(message, _file: _file, _line: _line) unless condition - end - - # Checks that the specified condition is true. - # Raises `AssertionFailed` if *condition* is false. - # The message of the exception is the *condition*. - # - # ``` - # assert(value == 42) - # ``` - macro assert(condition) - assert({{condition}}, {{condition.stringify}}, _file: {{condition.filename}}, _line: {{condition.line_number}}) - end - # Starts an expectation. # This should be followed up with `Assertion::Target#to` or `Assertion::Target#to_not`. # The value passed in will be checked to see if it satisfies the conditions of the specified matcher. From ce6f77656a1561e8ff0853a8d85372ef1035b417 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 20 Jan 2021 21:38:34 -0700 Subject: [PATCH 124/399] Use ExpectationFailed instead of AssertionFailed --- src/spectator/assertion_failed.cr | 15 --------------- src/spectator/dsl/expectations.cr | 4 ++-- src/spectator/expectation_failed.cr | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 17 deletions(-) delete mode 100644 src/spectator/assertion_failed.cr create mode 100644 src/spectator/expectation_failed.cr diff --git a/src/spectator/assertion_failed.cr b/src/spectator/assertion_failed.cr deleted file mode 100644 index 81b28d2..0000000 --- a/src/spectator/assertion_failed.cr +++ /dev/null @@ -1,15 +0,0 @@ -require "./source" - -module Spectator - # Exception that indicates an assertion failed. - # When raised within a test, the test should abort. - class AssertionFailed < Exception - # Location where the assertion failed and the exception raised. - getter source : Source - - # Creates the exception. - def initialize(@source : Source, message : String? = nil, cause : Exception? = nil) - super(message, cause) - end - end -end diff --git a/src/spectator/dsl/expectations.cr b/src/spectator/dsl/expectations.cr index bebbb9f..94a90b7 100644 --- a/src/spectator/dsl/expectations.cr +++ b/src/spectator/dsl/expectations.cr @@ -1,6 +1,6 @@ -require "../assertion_failed" require "../block" require "../expectation" +require "../expectation_failed" require "../source" require "../value" @@ -10,7 +10,7 @@ module Spectator::DSL # Immediately fail the current test. # A reason can be specified with *message*. def fail(message = "Example failed", *, _file = __FILE__, _line = __LINE__) - raise AssertionFailed.new(Source.new(_file, _line), message) + raise ExpectationFailed.new(Source.new(_file, _line), message) end # Starts an expectation. diff --git a/src/spectator/expectation_failed.cr b/src/spectator/expectation_failed.cr new file mode 100644 index 0000000..67c6dea --- /dev/null +++ b/src/spectator/expectation_failed.cr @@ -0,0 +1,15 @@ +require "./expectation" + +module Spectator + # Exception that indicates an expectation from a test failed. + # When raised within a test, the test should abort. + class ExpectationFailed < Exception + # Expectation that failed. + getter expectation : Expectation + + # Creates the exception. + def initialize(@expectation : Expectation, message : String? = nil, cause : Exception? = nil) + super(message, cause) + end + end +end From b7ed4ec14c4ad01ff19919614e901e4166733510 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 21 Jan 2021 00:03:39 -0700 Subject: [PATCH 125/399] Nest spec types --- src/spectator/dsl/builder.cr | 8 +- src/spectator/dsl/examples.cr | 2 +- src/spectator/dsl/groups.cr | 2 +- src/spectator/example.cr | 4 +- src/spectator/example_group.cr | 14 +- src/spectator/example_iterator.cr | 8 +- src/spectator/spec.cr | 11 +- src/spectator/spec/builder.cr | 206 ++++++++++++++++++++++++++++++ src/spectator/spec/node.cr | 71 ++++++++++ src/spectator/spec/runner.cr | 14 ++ src/spectator/spec_builder.cr | 203 ----------------------------- src/spectator/spec_node.cr | 69 ---------- 12 files changed, 313 insertions(+), 299 deletions(-) create mode 100644 src/spectator/spec/builder.cr create mode 100644 src/spectator/spec/node.cr create mode 100644 src/spectator/spec/runner.cr delete mode 100644 src/spectator/spec_builder.cr delete mode 100644 src/spectator/spec_node.cr diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr index ebeeca5..df24b2c 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -1,5 +1,7 @@ -require "../spec" -require "../spec_builder" +require "../example_group_hook" +require "../example_hook" +require "../example_procsy_hook" +require "../spec/builder" module Spectator::DSL # Incrementally builds up a test spec from the DSL. @@ -8,7 +10,7 @@ module Spectator::DSL extend self # Underlying spec builder. - @@builder = SpecBuilder.new + @@builder = Spec::Builder.new # Defines a new example group and pushes it onto the group stack. # Examples and groups defined after calling this method will be nested under the new group. diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index c8c9d8c..1442720 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -49,7 +49,7 @@ module Spectator::DSL # Inserts the correct representation of a example's name. # If *what* is a string, then it is dropped in as-is. # For anything else, it is stringified. - # This is intended to be used to convert a description from the spec DSL to `SpecNode#name`. + # This is intended to be used to convert a description from the spec DSL to `Spec::Node#name`. private macro _spectator_example_name(what) {% if what.is_a?(StringLiteral) || what.is_a?(StringInterpolation) || diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 6bc8b47..531ee3d 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -32,7 +32,7 @@ module Spectator::DSL # If *what* appears to be a type name, it will be symbolized. # If it's a string, then it is dropped in as-is. # For anything else, it is stringified. - # This is intended to be used to convert a description from the spec DSL to `SpecNode#name`. + # This is intended to be used to convert a description from the spec DSL to `Spec::Node#name`. private macro _spectator_group_name(what) {% if (what.is_a?(Generic) || what.is_a?(Path) || diff --git a/src/spectator/example.cr b/src/spectator/example.cr index e26a5f4..f5601ae 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -4,11 +4,11 @@ require "./harness" require "./pending_result" require "./result" require "./source" -require "./spec_node" +require "./spec/node" module Spectator # Standard example that runs a test case. - class Example < SpecNode + class Example < Spec::Node # Currently running example. class_getter! current : Example diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index a21de28..c939460 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -1,15 +1,15 @@ require "./events" -require "./spec_node" require "./example_procsy_hook" +require "./spec/node" module Spectator # Collection of examples and sub-groups. - class ExampleGroup < SpecNode - include Enumerable(SpecNode) + class ExampleGroup < Spec::Node + include Enumerable(Spec::Node) include Events - include Iterable(SpecNode) + include Iterable(Spec::Node) - @nodes = [] of SpecNode + @nodes = [] of Spec::Node group_event before_all do |hooks| Log.trace { "Processing before_all hooks for #{self}" } @@ -65,7 +65,7 @@ module Spectator # Removes the specified *node* from the group. # The node will be unassigned from this group. - def delete(node : SpecNode) + def delete(node : Spec::Node) # Only remove from the group if it is associated with this group. return unless node.group == self @@ -92,7 +92,7 @@ module Spectator # Assigns the node to this group. # If the node already belongs to a group, # it will be removed from the previous group before adding it to this group. - def <<(node : SpecNode) + def <<(node : Spec::Node) # Remove from existing group if the node is part of one. if (previous = node.group?) previous.delete(node) diff --git a/src/spectator/example_iterator.cr b/src/spectator/example_iterator.cr index f8acbb3..abcfd33 100644 --- a/src/spectator/example_iterator.cr +++ b/src/spectator/example_iterator.cr @@ -1,6 +1,6 @@ require "./example" require "./example_group" -require "./spec_node" +require "./spec/node" module Spectator # Iterates through all examples in a group and its nested groups. @@ -9,12 +9,12 @@ module Spectator # Stack that contains the iterators for each group. # A stack is used to track where in the tree this iterator is. - @stack : Array(Iterator(SpecNode)) + @stack : Array(Iterator(Spec::Node)) # Creates a new iterator. # The *group* is the example group to iterate through. def initialize(@group : ExampleGroup) - iter = @group.each.as(Iterator(SpecNode)) + iter = @group.each.as(Iterator(Spec::Node)) @stack = [iter] end @@ -39,7 +39,7 @@ module Spectator # Restart the iterator at the beginning. def rewind # Same code as `#initialize`, but return self. - iter = @group.each.as(Iterator(SpecNode)) + iter = @group.each.as(Iterator(Spec::Node)) @stack = [iter] self end diff --git a/src/spectator/spec.cr b/src/spectator/spec.cr index 8cfb235..1f9064c 100644 --- a/src/spectator/spec.cr +++ b/src/spectator/spec.cr @@ -19,14 +19,7 @@ module Spectator examples = ExampleIterator.new(@root).to_a @config.shuffle!(examples) end - - private struct Runner - def initialize(@examples : Array(Example)) - end - - def run - @examples.each(&.run) - end - end end end + +require "./spec/*" diff --git a/src/spectator/spec/builder.cr b/src/spectator/spec/builder.cr new file mode 100644 index 0000000..1f13171 --- /dev/null +++ b/src/spectator/spec/builder.cr @@ -0,0 +1,206 @@ +require "../config" +require "../config_builder" +require "../example" +require "../example_context_method" +require "../example_group" +require "../spec" + +module Spectator + class Spec + # Progressively builds a test spec. + # + # A stack is used to track the current example group. + # Adding an example or group will nest it under the group at the top of the stack. + class Builder + Log = ::Spectator::Log.for(self) + + # Stack tracking the current group. + # The bottom of the stack (first element) is the root group. + # The root group should never be removed. + # The top of the stack (last element) is the current group. + # New examples should be added to the current group. + @group_stack : Deque(ExampleGroup) + + # Configuration for the spec. + @config : Config? + + # Creates a new spec builder. + # A root group is pushed onto the group stack. + def initialize + root_group = ExampleGroup.new + @group_stack = Deque(ExampleGroup).new + @group_stack.push(root_group) + end + + # Constructs the test spec. + # Returns the spec instance. + # + # Raises an error if there were not symmetrical calls to `#start_group` and `#end_group`. + # This would indicate a logical error somewhere in Spectator or an extension of it. + def build : Spec + raise "Mismatched start and end groups" unless root? + + Spec.new(root_group, config) + end + + # Defines a new example group and pushes it onto the group stack. + # Examples and groups defined after calling this method will be nested under the new group. + # The group will be finished and popped off the stack when `#end_example` is called. + # + # The *name* is the name or brief description of the group. + # This should be a symbol when describing a type - the type name is represented as a symbol. + # Otherwise, a string should be used. + # + # The *source* optionally defined where the group originates in source code. + # + # The newly created group is returned. + # It shouldn't be used outside of this class until a matching `#end_group` is called. + def start_group(name, source = nil) : ExampleGroup + Log.trace { "Start group: #{name.inspect} @ #{source}" } + ExampleGroup.new(name, source, current_group).tap do |group| + @group_stack << group + end + end + + # Completes a previously defined example group and pops it off the group stack. + # Be sure to call `#start_group` and `#end_group` symmetically. + # + # The completed group will be returned. + # At this point, it is safe to use the group. + # All of its examples and sub-groups have been populated. + def end_group : ExampleGroup + Log.trace { "End group: #{current_group}" } + raise "Can't pop root group" if root? + + @group_stack.pop + end + + # Defines a new example. + # The example is added to the group currently on the top of the stack. + # + # The *name* is the name or brief description of the example. + # This should be a string or nil. + # When nil, the example's name will be populated by the first expectation run inside of the test code. + # + # The *source* optionally defined where the example originates in source code. + # + # The *context* is an instance of the context the test code should run in. + # See `Context` for more information. + # + # A block must be provided. + # It will be yielded two arguments - the example created by this method, and the *context* argument. + # The return value of the block is ignored. + # It is expected that the test code runs when the block is called. + # + # The newly created example is returned. + def add_example(name, source, context, &block : Example -> _) : Example + Log.trace { "Add example: #{name} @ #{source}" } + Example.new(context, block, name, source, current_group) + # The example is added to the current group by `Example` initializer. + end + + # Attaches a hook to be invoked before any and all examples in the current group. + def before_all(hook) + Log.trace { "Add before_all hook #{hook}" } + current_group.add_before_all_hook(hook) + end + + # Defines a block of code to execute before any and all examples in the current group. + def before_all(&block) + Log.trace { "Add before_all hook" } + current_group.before_all(&block) + end + + # Attaches a hook to be invoked before every example in the current group. + # The current example is provided as a block argument. + def before_each(hook) + Log.trace { "Add before_each hook #{hook}" } + current_group.add_before_each_hook(hook) + end + + # Defines a block of code to execute before every example in the current group. + # The current example is provided as a block argument. + def before_each(&block : Example -> _) + Log.trace { "Add before_each hook block" } + current_group.before_each(&block) + end + + # Attaches a hook to be invoked after any and all examples in the current group. + def after_all(hook) + Log.trace { "Add after_all hook #{hook}" } + current_group.add_after_all_hook(hook) + end + + # Defines a block of code to execute after any and all examples in the current group. + def after_all(&block) + Log.trace { "Add after_all hook" } + current_group.after_all(&block) + end + + # Attaches a hook to be invoked after every example in the current group. + # The current example is provided as a block argument. + def after_each(hook) + Log.trace { "Add after_each hook #{hook}" } + current_group.add_after_each_hook(hook) + end + + # Defines a block of code to execute after every example in the current group. + # The current example is provided as a block argument. + def after_each(&block : Example -> _) + Log.trace { "Add after_each hook" } + current_group.after_each(&block) + end + + # Attaches a hook to be invoked around every example in the current group. + # The current example in procsy form is provided as a block argument. + def around_each(hook) + Log.trace { "Add around_each hook #{hook}" } + current_group.add_around_each_hook(hook) + end + + # Defines a block of code to execute around every example in the current group. + # The current example in procsy form is provided as a block argument. + def around_each(&block : Example -> _) + Log.trace { "Add around_each hook" } + current_group.around_each(&block) + end + + # Builds the configuration to use for the spec. + # A `ConfigBuilder` is yielded to the block provided to this method. + # That builder will be used to create the configuration. + def config + builder = ConfigBuilder.new + yield builder + @config = builder.build + end + + # Sets the configuration of the spec. + # This configuration controls how examples run. + def config=(config) + @config = config + end + + # Checks if the current group is the root group. + private def root? + @group_stack.size == 1 + end + + # Retrieves the root group. + private def root_group + @group_stack.first + end + + # Retrieves the current group, which is at the top of the stack. + # This is the group that new examples should be added to. + private def current_group + @group_stack.last + end + + # Retrieves the configuration. + # If one wasn't previously set, a default configuration is used. + private def config : Config + @config || ConfigBuilder.default + end + end + end +end diff --git a/src/spectator/spec/node.cr b/src/spectator/spec/node.cr new file mode 100644 index 0000000..a408d1b --- /dev/null +++ b/src/spectator/spec/node.cr @@ -0,0 +1,71 @@ +require "../label" +require "../source" + +module Spectator + class Spec + # A single item in a test spec. + # This is commonly an `Example` or `ExampleGroup`, + # but can be anything that should be iterated over when running the spec. + abstract class Node + # Location of the node in source code. + getter! source : Source + + # User-provided name or description of the node. + # This does not include the group name or descriptions. + # Use `#to_s` to get the full name. + # + # This value will be nil if no name was provided. + # In this case, and the node is a runnable example, + # the name should be set to the description + # of the first matcher that runs in the test case. + # + # If this value is a `Symbol`, the user specified a type for the name. + getter! name : Label + + # Updates the name of the node. + protected def name=(@name : String) + end + + # Group the node belongs to. + getter! group : ExampleGroup + + # Assigns the node to the specified *group*. + # This is an internal method and should only be called from `ExampleGroup`. + # `ExampleGroup` manages the association of nodes to groups. + protected setter group : ExampleGroup? + + # Creates the node. + # The *name* describes the purpose of the node. + # It can be a `Symbol` to describe a type. + # The *source* tracks where the node exists in source code. + # The node will be assigned to *group* if it is provided. + def initialize(@name : Label = nil, @source : Source? = nil, group : ExampleGroup? = nil) + # Ensure group is linked. + group << self if group + end + + # Indicates whether the node has completed. + abstract def finished? : Bool + + # Constructs the full name or description of the node. + # This prepends names of groups this node is part of. + def to_s(io) + name = @name + + # Prefix with group's full name if the node belongs to a group. + if (group = @group) + group.to_s(io) + + # Add padding between the node names + # only if the names don't appear to be symbolic. + # Skip blank group names (like the root group). + io << ' ' unless !group.name? || # ameba:disable Style/NegatedConditionsInUnless + (group.name?.is_a?(Symbol) && name.is_a?(String) && + (name.starts_with?('#') || name.starts_with?('.'))) + end + + name.to_s(io) + end + end + end +end diff --git a/src/spectator/spec/runner.cr b/src/spectator/spec/runner.cr new file mode 100644 index 0000000..3447578 --- /dev/null +++ b/src/spectator/spec/runner.cr @@ -0,0 +1,14 @@ +require "../example" + +module Spectator + class Spec + private struct Runner + def initialize(@examples : Array(Example)) + end + + def run + @examples.each(&.run) + end + end + end +end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr deleted file mode 100644 index 8afbb6e..0000000 --- a/src/spectator/spec_builder.cr +++ /dev/null @@ -1,203 +0,0 @@ -require "./config" -require "./config_builder" -require "./example" -require "./example_context_method" -require "./example_group" - -module Spectator - # Progressively builds a test spec. - # - # A stack is used to track the current example group. - # Adding an example or group will nest it under the group at the top of the stack. - class SpecBuilder - Log = ::Spectator::Log.for(self) - - # Stack tracking the current group. - # The bottom of the stack (first element) is the root group. - # The root group should never be removed. - # The top of the stack (last element) is the current group. - # New examples should be added to the current group. - @group_stack : Deque(ExampleGroup) - - # Configuration for the spec. - @config : Config? - - # Creates a new spec builder. - # A root group is pushed onto the group stack. - def initialize - root_group = ExampleGroup.new - @group_stack = Deque(ExampleGroup).new - @group_stack.push(root_group) - end - - # Constructs the test spec. - # Returns the spec instance. - # - # Raises an error if there were not symmetrical calls to `#start_group` and `#end_group`. - # This would indicate a logical error somewhere in Spectator or an extension of it. - def build : Spec - raise "Mismatched start and end groups" unless root? - - Spec.new(root_group, config) - end - - # Defines a new example group and pushes it onto the group stack. - # Examples and groups defined after calling this method will be nested under the new group. - # The group will be finished and popped off the stack when `#end_example` is called. - # - # The *name* is the name or brief description of the group. - # This should be a symbol when describing a type - the type name is represented as a symbol. - # Otherwise, a string should be used. - # - # The *source* optionally defined where the group originates in source code. - # - # The newly created group is returned. - # It shouldn't be used outside of this class until a matching `#end_group` is called. - def start_group(name, source = nil) : ExampleGroup - Log.trace { "Start group: #{name.inspect} @ #{source}" } - ExampleGroup.new(name, source, current_group).tap do |group| - @group_stack << group - end - end - - # Completes a previously defined example group and pops it off the group stack. - # Be sure to call `#start_group` and `#end_group` symmetically. - # - # The completed group will be returned. - # At this point, it is safe to use the group. - # All of its examples and sub-groups have been populated. - def end_group : ExampleGroup - Log.trace { "End group: #{current_group}" } - raise "Can't pop root group" if root? - - @group_stack.pop - end - - # Defines a new example. - # The example is added to the group currently on the top of the stack. - # - # The *name* is the name or brief description of the example. - # This should be a string or nil. - # When nil, the example's name will be populated by the first expectation run inside of the test code. - # - # The *source* optionally defined where the example originates in source code. - # - # The *context* is an instance of the context the test code should run in. - # See `Context` for more information. - # - # A block must be provided. - # It will be yielded two arguments - the example created by this method, and the *context* argument. - # The return value of the block is ignored. - # It is expected that the test code runs when the block is called. - # - # The newly created example is returned. - def add_example(name, source, context, &block : Example -> _) : Example - Log.trace { "Add example: #{name} @ #{source}" } - Example.new(context, block, name, source, current_group) - # The example is added to the current group by `Example` initializer. - end - - # Attaches a hook to be invoked before any and all examples in the current group. - def before_all(hook) - Log.trace { "Add before_all hook #{hook}" } - current_group.add_before_all_hook(hook) - end - - # Defines a block of code to execute before any and all examples in the current group. - def before_all(&block) - Log.trace { "Add before_all hook" } - current_group.before_all(&block) - end - - # Attaches a hook to be invoked before every example in the current group. - # The current example is provided as a block argument. - def before_each(hook) - Log.trace { "Add before_each hook #{hook}" } - current_group.add_before_each_hook(hook) - end - - # Defines a block of code to execute before every example in the current group. - # The current example is provided as a block argument. - def before_each(&block : Example -> _) - Log.trace { "Add before_each hook block" } - current_group.before_each(&block) - end - - # Attaches a hook to be invoked after any and all examples in the current group. - def after_all(hook) - Log.trace { "Add after_all hook #{hook}" } - current_group.add_after_all_hook(hook) - end - - # Defines a block of code to execute after any and all examples in the current group. - def after_all(&block) - Log.trace { "Add after_all hook" } - current_group.after_all(&block) - end - - # Attaches a hook to be invoked after every example in the current group. - # The current example is provided as a block argument. - def after_each(hook) - Log.trace { "Add after_each hook #{hook}" } - current_group.add_after_each_hook(hook) - end - - # Defines a block of code to execute after every example in the current group. - # The current example is provided as a block argument. - def after_each(&block : Example -> _) - Log.trace { "Add after_each hook" } - current_group.after_each(&block) - end - - # Attaches a hook to be invoked around every example in the current group. - # The current example in procsy form is provided as a block argument. - def around_each(hook) - Log.trace { "Add around_each hook #{hook}" } - current_group.add_around_each_hook(hook) - end - - # Defines a block of code to execute around every example in the current group. - # The current example in procsy form is provided as a block argument. - def around_each(&block : Example -> _) - Log.trace { "Add around_each hook" } - current_group.around_each(&block) - end - - # Builds the configuration to use for the spec. - # A `ConfigBuilder` is yielded to the block provided to this method. - # That builder will be used to create the configuration. - def config - builder = ConfigBuilder.new - yield builder - @config = builder.build - end - - # Sets the configuration of the spec. - # This configuration controls how examples run. - def config=(config) - @config = config - end - - # Checks if the current group is the root group. - private def root? - @group_stack.size == 1 - end - - # Retrieves the root group. - private def root_group - @group_stack.first - end - - # Retrieves the current group, which is at the top of the stack. - # This is the group that new examples should be added to. - private def current_group - @group_stack.last - end - - # Retrieves the configuration. - # If one wasn't previously set, a default configuration is used. - private def config : Config - @config || ConfigBuilder.default - end - end -end diff --git a/src/spectator/spec_node.cr b/src/spectator/spec_node.cr deleted file mode 100644 index 7a00173..0000000 --- a/src/spectator/spec_node.cr +++ /dev/null @@ -1,69 +0,0 @@ -require "./label" -require "./source" - -module Spectator - # A single item in a test spec. - # This is commonly an `Example` or `ExampleGroup`, - # but can be anything that should be iterated over when running the spec. - abstract class SpecNode - # Location of the node in source code. - getter! source : Source - - # User-provided name or description of the node. - # This does not include the group name or descriptions. - # Use `#to_s` to get the full name. - # - # This value will be nil if no name was provided. - # In this case, and the node is a runnable example, - # the name should be set to the description - # of the first matcher that runs in the test case. - # - # If this value is a `Symbol`, the user specified a type for the name. - getter! name : Label - - # Updates the name of the node. - protected def name=(@name : String) - end - - # Group the node belongs to. - getter! group : ExampleGroup - - # Assigns the node to the specified *group*. - # This is an internal method and should only be called from `ExampleGroup`. - # `ExampleGroup` manages the association of nodes to groups. - protected setter group : ExampleGroup? - - # Creates the node. - # The *name* describes the purpose of the node. - # It can be a `Symbol` to describe a type. - # The *source* tracks where the node exists in source code. - # The node will be assigned to *group* if it is provided. - def initialize(@name : Label = nil, @source : Source? = nil, group : ExampleGroup? = nil) - # Ensure group is linked. - group << self if group - end - - # Indicates whether the node has completed. - abstract def finished? : Bool - - # Constructs the full name or description of the node. - # This prepends names of groups this node is part of. - def to_s(io) - name = @name - - # Prefix with group's full name if the node belongs to a group. - if (group = @group) - group.to_s(io) - - # Add padding between the node names - # only if the names don't appear to be symbolic. - # Skip blank group names (like the root group). - io << ' ' unless !group.name? || # ameba:disable Style/NegatedConditionsInUnless - (group.name?.is_a?(Symbol) && name.is_a?(String) && - (name.starts_with?('#') || name.starts_with?('.'))) - end - - name.to_s(io) - end - end -end From a8840351d5f482345a48732a4b2ba6668545912c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 21 Jan 2021 00:03:57 -0700 Subject: [PATCH 126/399] More work hooking up expectations --- src/spectator/expectation.cr | 36 ++++++++++++++++++++++++++++++++++++ src/spectator/harness.cr | 17 ++++++++++++----- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/spectator/expectation.cr b/src/spectator/expectation.cr index 031e41d..a4306f2 100644 --- a/src/spectator/expectation.cr +++ b/src/spectator/expectation.cr @@ -11,6 +11,42 @@ module Spectator # for instance using the *should* syntax or dynamically created expectations. getter source : Source? + # Indicates whether the expectation was met. + def satisfied? + @match_data.matched? + end + + # Indicates whether the expectation was not met. + def failed? + !satisfied? + end + + # If nil, then the match was successful. + def failure_message? + @match_data.as?(Matchers::FailedMatchData).try(&.failure_message) + end + + # Description of why the match failed. + def failure_message + failure_message?.not_nil! + end + + # Additional information about the match, useful for debug. + # If nil, then the match was successful. + def values? + @match_data.as?(Matchers::FailedMatchData).try(&.values) + end + + # Additional information about the match, useful for debug. + def values + values?.not_nil! + end + + def description + @match_data.description + end + + # Creates the expectation. # The *match_data* comes from the result of calling `Matcher#match`. # The *source* is the location of the expectation in source code, if available. diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 2b42c8f..6cfd8dc 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -1,4 +1,5 @@ require "./error_result" +require "./expectation" require "./pass_result" require "./result" @@ -29,6 +30,8 @@ module Spectator # Instead, methods the test calls can access it. # For instance, an expectation reporting a result. class Harness + Log = ::Spectator::Log.for(self) + # Retrieves the harness for the current running example. class_getter! current : self @@ -56,8 +59,9 @@ module Spectator translate(*outcome) end - def report(expectation) - # TODO + def report(expectation : Expectation) : Nil + Log.debug { "Reporting expectation #{expectation}" } + raise ExpectationFailed.new(expectation) if expectation.failed? end # Stores a block of code to be executed later. @@ -84,10 +88,13 @@ module Spectator # Takes the *elapsed* time and a possible *error* from the test. # Returns a type of `Result`. private def translate(elapsed, error) : Result - if error - ErrorResult.new(elapsed, error) - else + case error + when nil PassResult.new(elapsed) + when ExpectationFailed + FailResult.new(elapsed, error) + else + ErrorResult.new(elapsed, error) end end From 76378c9daee9195b664bb6df12899d45f5a39c6e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 21 Jan 2021 21:05:12 -0700 Subject: [PATCH 127/399] Whitespace --- src/spectator/expectation.cr | 1 - 1 file changed, 1 deletion(-) diff --git a/src/spectator/expectation.cr b/src/spectator/expectation.cr index a4306f2..29af037 100644 --- a/src/spectator/expectation.cr +++ b/src/spectator/expectation.cr @@ -46,7 +46,6 @@ module Spectator @match_data.description end - # Creates the expectation. # The *match_data* comes from the result of calling `Matcher#match`. # The *source* is the location of the expectation in source code, if available. From 5cccf5b4cbe0a69a4cd01036c9ac131adb46e858 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 22 Jan 2021 23:00:17 -0700 Subject: [PATCH 128/399] Support example as block argument to let and subject Surprisingly, RSpec supports this. https://relishapp.com/rspec/rspec-core/v/3-10/docs/metadata/current-example --- src/spectator/dsl/values.cr | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/spectator/dsl/values.cr b/src/spectator/dsl/values.cr index 210a9c1..6f0435d 100644 --- a/src/spectator/dsl/values.cr +++ b/src/spectator/dsl/values.cr @@ -10,11 +10,15 @@ module Spectator::DSL macro let(name, &block) {% raise "Block required for 'let'" unless block %} {% raise "Cannot use 'let' inside of a test block" if @def %} + {% raise "Block argument count for 'let' must be 0..1" if block.args.size > 1 %} @%value = ::Spectator::LazyWrapper.new def {{name.id}} - @%value.get {{block}} + {% if block.args.size > 0 %}{{block.args.first}} = ::Spectator::Example.current{% end %} + @%value.get do + {{block.body}} + end end end @@ -25,6 +29,7 @@ module Spectator::DSL macro let!(name, &block) {% raise "Block required for 'let!'" unless block %} {% raise "Cannot use 'let!' inside of a test block" if @def %} + {% raise "Block argument count for 'let!' must be 0..1" if block.args.size > 1 %} let({{name}}) {{block}} before_each { {{name.id}} } @@ -37,6 +42,7 @@ module Spectator::DSL macro subject(&block) {% raise "Block required for 'subject'" unless block %} {% raise "Cannot use 'subject' inside of a test block" if @def %} + {% raise "Block argument count for 'subject' must be 0..1" if block.args.size > 1 %} let(subject) {{block}} end @@ -63,6 +69,7 @@ module Spectator::DSL macro subject!(&block) {% raise "Block required for 'subject!'" unless block %} {% raise "Cannot use 'subject!' inside of a test block" if @def %} + {% raise "Block argument count for 'subject!' must be 0..1" if block.args.size > 1 %} let!(subject) {{block}} end From 57b262ccd67bd6e030840dd9e152783b1913048a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 23 Jan 2021 19:22:58 -0700 Subject: [PATCH 129/399] Reference 0.10.0 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0edf63f..7fea4f5 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Add this to your application's `shard.yml`: development_dependencies: spectator: gitlab: arctic-fox/spectator - version: ~> 0.9.31 + version: ~> 0.10.0 ``` Usage From 8252d333fae01db27d9a6d7edfdee774693f965b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 23 Jan 2021 19:42:14 -0700 Subject: [PATCH 130/399] Update changelog with current known changes --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92be77e..884ff2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Added +- Hooks are yielded the current example as a block argument. +- The `let` and `subject` blocks are yielded the current example as a block argument. +- Add internal logging that uses Crystal's `Log` utility. Provide the `LOG_LEVEL` environment variable to enable. +- Support dynamic creation of examples. +- Capture and log information for hooks. + +### Changed +- Simplify and reduce defined types and generics. Should speed up compilation times. +- `around_each` hooks wrap `before_all` and `after_all` hooks. [#12](https://github.com/icy-arctic-fox/spectator/issues/12) +- The "should" syntax no longer reports the source as inside Spectator. +- Short-hand "should" syntax must be included by using `require "spectator/should"` - `it { should eq("foo") }` +- Overhaul example creation and handling. +- Overhaul storage of test values. +- Cleanup and simplify DSL implementation. +- Better error messages and detection when DSL methods are used when they shouldn't (i.e. `describe` inside `it`). +- Other minor internal improvements and cleanup. + ## [0.9.31] - 2021-01-08 ### Fixed - Fix misaligned line numbers when referencing examples and groups. From 466a388558adf3ef0a1a9a2759e850ae74b76996 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 00:40:59 -0700 Subject: [PATCH 131/399] Initial code for example metadata --- src/spectator/dsl/examples.cr | 18 ++++++++++++++++-- src/spectator/dsl/groups.cr | 15 ++++++++++++++- src/spectator/spec/builder.cr | 4 ++-- src/spectator/test_context.cr | 6 ++++++ 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 1442720..c8d3d59 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -21,11 +21,24 @@ module Spectator::DSL # # The example will be marked as pending if the block is omitted. # A block or name must be provided. - macro {{name.id}}(what = nil, &block) + macro {{name.id}}(what = nil, *tags, **metadata, &block) \{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %} \{% raise "A description or block must be provided. Cannot use '{{name.id}}' alone." unless what || block %} \{% raise "Block argument count '{{name.id}}' hook must be 0..1" if block.args.size > 1 %} + def self.\%metadata + \{% if tags.empty? && metadata.empty? %} + _spectator_metadata + \{% else %} + _spectator_metadata.merge( + \{% for tag in tags %} + \{{tag.id.stringify}}: true, + \{% end %} + \{{metadata.double_splat}} + ) + \{% end %} + end + def \%test(\{{block.args.splat}}) : Nil \{{block.body}} end @@ -33,7 +46,8 @@ module Spectator::DSL ::Spectator::DSL::Builder.add_example( _spectator_example_name(\{{what}}), ::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}}), - \{{@type.name}}.new.as(::Spectator::Context) + \{{@type.name}}.new.as(::Spectator::Context), + \{{@type.name}}.\%metadata ) do |example| example.with_context(\{{@type.name}}) do \{% if block.args.empty? %} diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 531ee3d..0fec1fe 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -10,12 +10,25 @@ module Spectator::DSL # The *what* argument is a name or description of the group. # # TODO: Handle string interpolation in example and group names. - macro {{name.id}}(what, &block) + macro {{name.id}}(what, *tags, **metadata, &block) \{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %} class Group\%group < \{{@type.id}} _spectator_group_subject(\{{what}}) + def self._spectator_metadata + \{% if tags.empty? && metadata.empty? %} + super + \{% else %} + super.merge( + \{% for tag in tags %} + \{{tag.id.stringify}}: true, + \{% end %} + \{{metadata.double_splat}} + ) + \{% end %} + end + ::Spectator::DSL::Builder.start_group( _spectator_group_name(\{{what}}), ::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}}) diff --git a/src/spectator/spec/builder.cr b/src/spectator/spec/builder.cr index 1f13171..42f4e11 100644 --- a/src/spectator/spec/builder.cr +++ b/src/spectator/spec/builder.cr @@ -93,8 +93,8 @@ module Spectator # It is expected that the test code runs when the block is called. # # The newly created example is returned. - def add_example(name, source, context, &block : Example -> _) : Example - Log.trace { "Add example: #{name} @ #{source}" } + def add_example(name, source, context, metadata, &block : Example -> _) : Example + Log.trace { "Add example: #{name} @ #{source}; metadata: #{metadata}" } Example.new(context, block, name, source, current_group) # The example is added to the current group by `Example` initializer. end diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr index c32119f..caa77af 100644 --- a/src/spectator/test_context.cr +++ b/src/spectator/test_context.cr @@ -27,4 +27,10 @@ class SpectatorTestContext < SpectatorContext private def subject @subject.get { _spectator_implicit_subject } end + + # Initial metadata for tests. + # This method should be overridden by example groups and examples. + def self._spectator_metadata + NamedTuple.new + end end From a56b1e0eb1f2cad4c073d29594ef528c14a7fa99 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 01:16:26 -0700 Subject: [PATCH 132/399] Somewhat functional metadata unwrap --- src/spectator/dsl/examples.cr | 2 +- src/spectator/example.cr | 13 +++++++++++-- src/spectator/metadata_example.cr | 12 ++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 src/spectator/metadata_example.cr diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index c8d3d59..7c68053 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -53,7 +53,7 @@ module Spectator::DSL \{% if block.args.empty? %} \%test \{% else %} - \%test(example) + \%test(example.unwrap_metadata(typeof(\{{@type.name}}.\%metadata))) \{% end %} end end diff --git a/src/spectator/example.cr b/src/spectator/example.cr index f5601ae..7ada1e9 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -1,6 +1,7 @@ require "./example_context_delegate" require "./example_group" require "./harness" +require "./metadata_example" require "./pending_result" require "./result" require "./source" @@ -26,8 +27,9 @@ module Spectator # The *source* tracks where the example exists in source code. # The example will be assigned to *group* if it is provided. def initialize(@context : Context, @entrypoint : self ->, - name : String? = nil, source : Source? = nil, group : ExampleGroup? = nil) + name : String? = nil, source : Source? = nil, group : ExampleGroup? = nil, metadata = NamedTuple.new) super(name, source, group) + @metadata = Wrapper.new(metadata) end # Creates a dynamic example. @@ -37,9 +39,11 @@ module Spectator # It can be a `Symbol` to describe a type. # The *source* tracks where the example exists in source code. # The example will be assigned to *group* if it is provided. - def initialize(name : String? = nil, source : Source? = nil, group : ExampleGroup? = nil, &block : self ->) + def initialize(name : String? = nil, source : Source? = nil, group : ExampleGroup? = nil, + metadata = NamedTuple.new, &block : self ->) @context = NullContext.new @entrypoint = block + @metadata = Wrapper.new(metadata) end # Executes the test case. @@ -93,6 +97,11 @@ module Spectator with context yield end + protected def unwrap_metadata(klass) + metadata = @metadata.get(klass) + MetadataExample.new(self, metadata) + end + # Casts the example's test context to a specific type. # This is an advanced method intended for internal usage only. # diff --git a/src/spectator/metadata_example.cr b/src/spectator/metadata_example.cr new file mode 100644 index 0000000..6365d82 --- /dev/null +++ b/src/spectator/metadata_example.cr @@ -0,0 +1,12 @@ +require "./example" + +module Spectator + class MetadataExample(Metadata) + getter metadata : Metadata + + def initialize(@example : Example, @metadata : Metadata) + end + + forward_missing_to @example + end +end From 11cb16fb3ab0bb296694a71365725a1d52e70b18 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 10:14:09 -0700 Subject: [PATCH 133/399] Add missing call to super --- src/spectator/example.cr | 1 + 1 file changed, 1 insertion(+) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 7ada1e9..1c3e654 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -41,6 +41,7 @@ module Spectator # The example will be assigned to *group* if it is provided. def initialize(name : String? = nil, source : Source? = nil, group : ExampleGroup? = nil, metadata = NamedTuple.new, &block : self ->) + super(name, source, group) @context = NullContext.new @entrypoint = block @metadata = Wrapper.new(metadata) From 8cf498c9e904cc85e8351783dda3e86501e8de76 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 11:13:30 -0700 Subject: [PATCH 134/399] Switch to using tags instead of metadata --- src/spectator/dsl/examples.cr | 24 +++++++++++++++--------- src/spectator/example.cr | 18 ++++++++---------- src/spectator/metadata_example.cr | 12 ------------ src/spectator/spec/builder.cr | 4 ++-- src/spectator/test_context.cr | 6 +++--- 5 files changed, 28 insertions(+), 36 deletions(-) delete mode 100644 src/spectator/metadata_example.cr diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 7c68053..e41bea7 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -26,16 +26,22 @@ module Spectator::DSL \{% raise "A description or block must be provided. Cannot use '{{name.id}}' alone." unless what || block %} \{% raise "Block argument count '{{name.id}}' hook must be 0..1" if block.args.size > 1 %} - def self.\%metadata + def self.\%tags \{% if tags.empty? && metadata.empty? %} - _spectator_metadata + _spectator_tags \{% else %} - _spectator_metadata.merge( - \{% for tag in tags %} - \{{tag.id.stringify}}: true, + _spectator_tags.concat({\{{tags.map(&.id.stringify).splat}}}).tap do |tags| + \{% for k, v in metadata %} + cond = begin + \{{v}} + end + if cond + tags.add(\{{k.id.stringify}}) + else + tags.remove(\{{k.id.stringify}}) + end \{% end %} - \{{metadata.double_splat}} - ) + end \{% end %} end @@ -47,13 +53,13 @@ module Spectator::DSL _spectator_example_name(\{{what}}), ::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}}), \{{@type.name}}.new.as(::Spectator::Context), - \{{@type.name}}.\%metadata + \{{@type.name}}.\%tags ) do |example| example.with_context(\{{@type.name}}) do \{% if block.args.empty? %} \%test \{% else %} - \%test(example.unwrap_metadata(typeof(\{{@type.name}}.\%metadata))) + \%test(example) \{% end %} end end diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 1c3e654..d5390ab 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -1,7 +1,6 @@ require "./example_context_delegate" require "./example_group" require "./harness" -require "./metadata_example" require "./pending_result" require "./result" require "./source" @@ -10,6 +9,9 @@ require "./spec/node" module Spectator # Standard example that runs a test case. class Example < Spec::Node + # User-defined keywords used for filtering and behavior modification. + alias Tags = Set(String) + # Currently running example. class_getter! current : Example @@ -19,6 +21,9 @@ module Spectator # Retrieves the result of the last time the example ran. getter result : Result = PendingResult.new + # User-defined keywords used for filtering and behavior modification. + getter tags : Set(String) + # Creates the example. # An instance to run the test code in is given by *context*. # The *entrypoint* defines the test code (typically inside *context*). @@ -27,9 +32,8 @@ module Spectator # The *source* tracks where the example exists in source code. # The example will be assigned to *group* if it is provided. def initialize(@context : Context, @entrypoint : self ->, - name : String? = nil, source : Source? = nil, group : ExampleGroup? = nil, metadata = NamedTuple.new) + name : String? = nil, source : Source? = nil, group : ExampleGroup? = nil, @tags = Tags.new) super(name, source, group) - @metadata = Wrapper.new(metadata) end # Creates a dynamic example. @@ -40,11 +44,10 @@ module Spectator # The *source* tracks where the example exists in source code. # The example will be assigned to *group* if it is provided. def initialize(name : String? = nil, source : Source? = nil, group : ExampleGroup? = nil, - metadata = NamedTuple.new, &block : self ->) + @tags = Tags.new, &block : self ->) super(name, source, group) @context = NullContext.new @entrypoint = block - @metadata = Wrapper.new(metadata) end # Executes the test case. @@ -98,11 +101,6 @@ module Spectator with context yield end - protected def unwrap_metadata(klass) - metadata = @metadata.get(klass) - MetadataExample.new(self, metadata) - end - # Casts the example's test context to a specific type. # This is an advanced method intended for internal usage only. # diff --git a/src/spectator/metadata_example.cr b/src/spectator/metadata_example.cr deleted file mode 100644 index 6365d82..0000000 --- a/src/spectator/metadata_example.cr +++ /dev/null @@ -1,12 +0,0 @@ -require "./example" - -module Spectator - class MetadataExample(Metadata) - getter metadata : Metadata - - def initialize(@example : Example, @metadata : Metadata) - end - - forward_missing_to @example - end -end diff --git a/src/spectator/spec/builder.cr b/src/spectator/spec/builder.cr index 42f4e11..3836a61 100644 --- a/src/spectator/spec/builder.cr +++ b/src/spectator/spec/builder.cr @@ -93,8 +93,8 @@ module Spectator # It is expected that the test code runs when the block is called. # # The newly created example is returned. - def add_example(name, source, context, metadata, &block : Example -> _) : Example - Log.trace { "Add example: #{name} @ #{source}; metadata: #{metadata}" } + def add_example(name, source, context, tags, &block : Example -> _) : Example + Log.trace { "Add example: #{name} @ #{source}; tags: #{tags}" } Example.new(context, block, name, source, current_group) # The example is added to the current group by `Example` initializer. end diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr index caa77af..60bf451 100644 --- a/src/spectator/test_context.cr +++ b/src/spectator/test_context.cr @@ -28,9 +28,9 @@ class SpectatorTestContext < SpectatorContext @subject.get { _spectator_implicit_subject } end - # Initial metadata for tests. + # Initial tags for tests. # This method should be overridden by example groups and examples. - def self._spectator_metadata - NamedTuple.new + def self._spectator_tags + ::Spectator::Example::Tags.new end end From 71a497b1482fb509ccbf37b84baf6a4f375a90c7 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 11:20:20 -0700 Subject: [PATCH 135/399] Move tags to node level --- src/spectator/example.cr | 17 +++++++---------- src/spectator/spec/node.cr | 10 +++++++++- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index d5390ab..6f7b2d2 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -9,9 +9,6 @@ require "./spec/node" module Spectator # Standard example that runs a test case. class Example < Spec::Node - # User-defined keywords used for filtering and behavior modification. - alias Tags = Set(String) - # Currently running example. class_getter! current : Example @@ -21,9 +18,6 @@ module Spectator # Retrieves the result of the last time the example ran. getter result : Result = PendingResult.new - # User-defined keywords used for filtering and behavior modification. - getter tags : Set(String) - # Creates the example. # An instance to run the test code in is given by *context*. # The *entrypoint* defines the test code (typically inside *context*). @@ -31,9 +25,11 @@ module Spectator # It can be a `Symbol` to describe a type. # The *source* tracks where the example exists in source code. # The example will be assigned to *group* if it is provided. + # A set of *tags* can be used for filtering and modifying example behavior. def initialize(@context : Context, @entrypoint : self ->, - name : String? = nil, source : Source? = nil, group : ExampleGroup? = nil, @tags = Tags.new) - super(name, source, group) + name : String? = nil, source : Source? = nil, + group : ExampleGroup? = nil, tags = Spec::Node::Tags.new) + super(name, source, group, tags) end # Creates a dynamic example. @@ -43,9 +39,10 @@ module Spectator # It can be a `Symbol` to describe a type. # The *source* tracks where the example exists in source code. # The example will be assigned to *group* if it is provided. + # A set of *tags* can be used for filtering and modifying example behavior. def initialize(name : String? = nil, source : Source? = nil, group : ExampleGroup? = nil, - @tags = Tags.new, &block : self ->) - super(name, source, group) + tags = Spec::Node::Tags.new, &block : self ->) + super(name, source, group, tags) @context = NullContext.new @entrypoint = block end diff --git a/src/spectator/spec/node.cr b/src/spectator/spec/node.cr index a408d1b..f59e02b 100644 --- a/src/spectator/spec/node.cr +++ b/src/spectator/spec/node.cr @@ -7,6 +7,9 @@ module Spectator # This is commonly an `Example` or `ExampleGroup`, # but can be anything that should be iterated over when running the spec. abstract class Node + # User-defined keywords used for filtering and behavior modification. + alias Tags = Set(String) + # Location of the node in source code. getter! source : Source @@ -29,6 +32,9 @@ module Spectator # Group the node belongs to. getter! group : ExampleGroup + # User-defined keywords used for filtering and behavior modification. + getter tags : Tags + # Assigns the node to the specified *group*. # This is an internal method and should only be called from `ExampleGroup`. # `ExampleGroup` manages the association of nodes to groups. @@ -39,7 +45,9 @@ module Spectator # It can be a `Symbol` to describe a type. # The *source* tracks where the node exists in source code. # The node will be assigned to *group* if it is provided. - def initialize(@name : Label = nil, @source : Source? = nil, group : ExampleGroup? = nil) + # A set of *tags* can be used for filtering and modifying example behavior. + def initialize(@name : Label = nil, @source : Source? = nil, + group : ExampleGroup? = nil, @tags : Tags = Tags.new) # Ensure group is linked. group << self if group end From e77d6f0a4f629fd6e7c5f8de82472c6586d8f6bd Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 11:21:49 -0700 Subject: [PATCH 136/399] Change groups from metadata to tags --- src/spectator/dsl/groups.cr | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 0fec1fe..5be4500 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -16,16 +16,22 @@ module Spectator::DSL class Group\%group < \{{@type.id}} _spectator_group_subject(\{{what}}) - def self._spectator_metadata + def self._spectator_tags \{% if tags.empty? && metadata.empty? %} super \{% else %} - super.merge( - \{% for tag in tags %} - \{{tag.id.stringify}}: true, + super.concat({\{{tags.map(&.id.stringify).splat}}}).tap do |tags| + \{% for k, v in metadata %} + cond = begin + \{{v}} + end + if cond + tags.add(\{{k.id.stringify}}) + else + tags.remove(\{{k.id.stringify}}) + end \{% end %} - \{{metadata.double_splat}} - ) + end \{% end %} end From 467f0d3ad4d07384087639ebcbbefc82c43e051e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 11:34:01 -0700 Subject: [PATCH 137/399] Pass along tags --- src/spectator/dsl/groups.cr | 3 ++- src/spectator/spec/builder.cr | 16 +++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 5be4500..d75c1b3 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -37,7 +37,8 @@ module Spectator::DSL ::Spectator::DSL::Builder.start_group( _spectator_group_name(\{{what}}), - ::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}}) + ::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}}), + _spectator_tags ) \{{block.body}} diff --git a/src/spectator/spec/builder.cr b/src/spectator/spec/builder.cr index 3836a61..f111928 100644 --- a/src/spectator/spec/builder.cr +++ b/src/spectator/spec/builder.cr @@ -53,11 +53,14 @@ module Spectator # # The *source* optionally defined where the group originates in source code. # + # A set of *tags* can be used for filtering and modifying example behavior. + # For instance, adding a "pending" tag will mark tests as pending and skip execution. + # # The newly created group is returned. # It shouldn't be used outside of this class until a matching `#end_group` is called. - def start_group(name, source = nil) : ExampleGroup - Log.trace { "Start group: #{name.inspect} @ #{source}" } - ExampleGroup.new(name, source, current_group).tap do |group| + def start_group(name, source = nil, tags = Spec::Node::Tags.new) : ExampleGroup + Log.trace { "Start group: #{name.inspect} @ #{source}; tags: #{tags}" } + ExampleGroup.new(name, source, current_group, tags).tap do |group| @group_stack << group end end @@ -87,15 +90,18 @@ module Spectator # The *context* is an instance of the context the test code should run in. # See `Context` for more information. # + # A set of *tags* can be used for filtering and modifying example behavior. + # For instance, adding a "pending" tag will mark the test as pending and skip execution. + # # A block must be provided. # It will be yielded two arguments - the example created by this method, and the *context* argument. # The return value of the block is ignored. # It is expected that the test code runs when the block is called. # # The newly created example is returned. - def add_example(name, source, context, tags, &block : Example -> _) : Example + def add_example(name, source, context, tags = Spec::Node::Tags.new, &block : Example -> _) : Example Log.trace { "Add example: #{name} @ #{source}; tags: #{tags}" } - Example.new(context, block, name, source, current_group) + Example.new(context, block, name, source, current_group, tags) # The example is added to the current group by `Example` initializer. end From 5166cd7778d1c302cfb5af0b5f5dfa9530340407 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 12:01:49 -0700 Subject: [PATCH 138/399] Fix case with no tags and some metadata Simplify tag set construction code. --- src/spectator/dsl/examples.cr | 27 ++++++++++++------------- src/spectator/dsl/groups.cr | 37 ++++++++++++++++++----------------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index e41bea7..f48b8a4 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -27,22 +27,21 @@ module Spectator::DSL \{% raise "Block argument count '{{name.id}}' hook must be 0..1" if block.args.size > 1 %} def self.\%tags - \{% if tags.empty? && metadata.empty? %} - _spectator_tags - \{% else %} - _spectator_tags.concat({\{{tags.map(&.id.stringify).splat}}}).tap do |tags| - \{% for k, v in metadata %} - cond = begin - \{{v}} - end - if cond - tags.add(\{{k.id.stringify}}) - else - tags.remove(\{{k.id.stringify}}) - end - \{% end %} + tags = _spectator_tags + \{% if !tags.empty? %} + tags.concat({ \{{tags.map(&.id.stringify).splat}} }) + \{% end %} + \{% for k, v in metadata %} + cond = begin + \{{v}} + end + if cond + tags.add(\{{k.id.stringify}}) + else + tags.delete(\{{k.id.stringify}}) end \{% end %} + tags end def \%test(\{{block.args.splat}}) : Nil diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index d75c1b3..78cc7bf 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -16,24 +16,25 @@ module Spectator::DSL class Group\%group < \{{@type.id}} _spectator_group_subject(\{{what}}) - def self._spectator_tags - \{% if tags.empty? && metadata.empty? %} - super - \{% else %} - super.concat({\{{tags.map(&.id.stringify).splat}}}).tap do |tags| - \{% for k, v in metadata %} - cond = begin - \{{v}} - end - if cond - tags.add(\{{k.id.stringify}}) - else - tags.remove(\{{k.id.stringify}}) - end - \{% end %} - end - \{% end %} - end + \{% if !tags.empty? || !metadata.empty? %} + def self._spectator_tags + tags = super + \{% if !tags.empty? %} + tags.concat({ \{{tags.map(&.id.stringify).splat}} }) + \{% end %} + \{% for k, v in metadata %} + cond = begin + \{{v}} + end + if cond + tags.add(\{{k.id.stringify}}) + else + tags.delete(\{{k.id.stringify}}) + end + \{% end %} + tags + end + \{% end %} ::Spectator::DSL::Builder.start_group( _spectator_group_name(\{{what}}), From db877da984eff6729b5a2b003a33f9e73508b0ec Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 12:03:38 -0700 Subject: [PATCH 139/399] Change tags to symbols --- src/spectator/dsl/examples.cr | 6 +++--- src/spectator/dsl/groups.cr | 6 +++--- src/spectator/spec/node.cr | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index f48b8a4..9b28b20 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -29,16 +29,16 @@ module Spectator::DSL def self.\%tags tags = _spectator_tags \{% if !tags.empty? %} - tags.concat({ \{{tags.map(&.id.stringify).splat}} }) + tags.concat({ \{{tags.map(&.id.symbolize).splat}} }) \{% end %} \{% for k, v in metadata %} cond = begin \{{v}} end if cond - tags.add(\{{k.id.stringify}}) + tags.add(\{{k.id.symbolize}}) else - tags.delete(\{{k.id.stringify}}) + tags.delete(\{{k.id.symbolize}}) end \{% end %} tags diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 78cc7bf..ad35a30 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -20,16 +20,16 @@ module Spectator::DSL def self._spectator_tags tags = super \{% if !tags.empty? %} - tags.concat({ \{{tags.map(&.id.stringify).splat}} }) + tags.concat({ \{{tags.map(&.id.symbolize).splat}} }) \{% end %} \{% for k, v in metadata %} cond = begin \{{v}} end if cond - tags.add(\{{k.id.stringify}}) + tags.add(\{{k.id.symbolize}}) else - tags.delete(\{{k.id.stringify}}) + tags.delete(\{{k.id.symbolize}}) end \{% end %} tags diff --git a/src/spectator/spec/node.cr b/src/spectator/spec/node.cr index f59e02b..ed4bc86 100644 --- a/src/spectator/spec/node.cr +++ b/src/spectator/spec/node.cr @@ -8,7 +8,7 @@ module Spectator # but can be anything that should be iterated over when running the spec. abstract class Node # User-defined keywords used for filtering and behavior modification. - alias Tags = Set(String) + alias Tags = Set(Symbol) # Location of the node in source code. getter! source : Source From c5246e1cd31711137d3eea36b76e196d788d8a34 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 12:07:23 -0700 Subject: [PATCH 140/399] Promote Tags to the Spectator namespace --- src/spectator/example.cr | 5 +++-- src/spectator/spec/builder.cr | 5 +++-- src/spectator/spec/node.cr | 4 +--- src/spectator/tags.cr | 4 ++++ src/spectator/test_context.cr | 4 +++- 5 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 src/spectator/tags.cr diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 6f7b2d2..dcc8f07 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -5,6 +5,7 @@ require "./pending_result" require "./result" require "./source" require "./spec/node" +require "./tags" module Spectator # Standard example that runs a test case. @@ -28,7 +29,7 @@ module Spectator # A set of *tags* can be used for filtering and modifying example behavior. def initialize(@context : Context, @entrypoint : self ->, name : String? = nil, source : Source? = nil, - group : ExampleGroup? = nil, tags = Spec::Node::Tags.new) + group : ExampleGroup? = nil, tags = Tags.new) super(name, source, group, tags) end @@ -41,7 +42,7 @@ module Spectator # The example will be assigned to *group* if it is provided. # A set of *tags* can be used for filtering and modifying example behavior. def initialize(name : String? = nil, source : Source? = nil, group : ExampleGroup? = nil, - tags = Spec::Node::Tags.new, &block : self ->) + tags = Tags.new, &block : self ->) super(name, source, group, tags) @context = NullContext.new @entrypoint = block diff --git a/src/spectator/spec/builder.cr b/src/spectator/spec/builder.cr index f111928..f9a4ced 100644 --- a/src/spectator/spec/builder.cr +++ b/src/spectator/spec/builder.cr @@ -4,6 +4,7 @@ require "../example" require "../example_context_method" require "../example_group" require "../spec" +require "../tags" module Spectator class Spec @@ -58,7 +59,7 @@ module Spectator # # The newly created group is returned. # It shouldn't be used outside of this class until a matching `#end_group` is called. - def start_group(name, source = nil, tags = Spec::Node::Tags.new) : ExampleGroup + def start_group(name, source = nil, tags = Tags.new) : ExampleGroup Log.trace { "Start group: #{name.inspect} @ #{source}; tags: #{tags}" } ExampleGroup.new(name, source, current_group, tags).tap do |group| @group_stack << group @@ -99,7 +100,7 @@ module Spectator # It is expected that the test code runs when the block is called. # # The newly created example is returned. - def add_example(name, source, context, tags = Spec::Node::Tags.new, &block : Example -> _) : Example + def add_example(name, source, context, tags = Tags.new, &block : Example -> _) : Example Log.trace { "Add example: #{name} @ #{source}; tags: #{tags}" } Example.new(context, block, name, source, current_group, tags) # The example is added to the current group by `Example` initializer. diff --git a/src/spectator/spec/node.cr b/src/spectator/spec/node.cr index ed4bc86..6459e62 100644 --- a/src/spectator/spec/node.cr +++ b/src/spectator/spec/node.cr @@ -1,5 +1,6 @@ require "../label" require "../source" +require "../tags" module Spectator class Spec @@ -7,9 +8,6 @@ module Spectator # This is commonly an `Example` or `ExampleGroup`, # but can be anything that should be iterated over when running the spec. abstract class Node - # User-defined keywords used for filtering and behavior modification. - alias Tags = Set(Symbol) - # Location of the node in source code. getter! source : Source diff --git a/src/spectator/tags.cr b/src/spectator/tags.cr new file mode 100644 index 0000000..e7c1065 --- /dev/null +++ b/src/spectator/tags.cr @@ -0,0 +1,4 @@ +module Spectator + # User-defined keywords used for filtering and behavior modification. + alias Tags = Set(Symbol) +end diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr index 60bf451..8186daa 100644 --- a/src/spectator/test_context.cr +++ b/src/spectator/test_context.cr @@ -1,5 +1,7 @@ require "./context" require "./dsl" +require "./lazy_wrapper" +require "./tags" # Class used as the base for all specs using the DSL. # It adds methods and macros necessary to use the DSL from the spec. @@ -31,6 +33,6 @@ class SpectatorTestContext < SpectatorContext # Initial tags for tests. # This method should be overridden by example groups and examples. def self._spectator_tags - ::Spectator::Example::Tags.new + ::Spectator::Tags.new end end From fdc2a71dd57cab9612bb495889df59064fc3d38c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 12:07:45 -0700 Subject: [PATCH 141/399] Formatting --- src/spectator/spec/node.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/spec/node.cr b/src/spectator/spec/node.cr index 6459e62..bbd3214 100644 --- a/src/spectator/spec/node.cr +++ b/src/spectator/spec/node.cr @@ -45,7 +45,7 @@ module Spectator # The node will be assigned to *group* if it is provided. # A set of *tags* can be used for filtering and modifying example behavior. def initialize(@name : Label = nil, @source : Source? = nil, - group : ExampleGroup? = nil, @tags : Tags = Tags.new) + group : ExampleGroup? = nil, @tags : Tags = Tags.new) # Ensure group is linked. group << self if group end From a5fcb963785cdc1e4ec86d08fcd0855a445de394 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 12:10:09 -0700 Subject: [PATCH 142/399] Support for tags --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 884ff2f..fcad249 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add internal logging that uses Crystal's `Log` utility. Provide the `LOG_LEVEL` environment variable to enable. - Support dynamic creation of examples. - Capture and log information for hooks. +- Tags can be added to examples and example groups. ### Changed - Simplify and reduce defined types and generics. Should speed up compilation times. From de779fdf613d1b26af11d7fd63cd3444e86d53dc Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 12:26:29 -0700 Subject: [PATCH 143/399] Remove unecessary type name --- src/spectator/dsl/examples.cr | 4 ++-- src/spectator/dsl/hooks.cr | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 9b28b20..8063b61 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -51,8 +51,8 @@ module Spectator::DSL ::Spectator::DSL::Builder.add_example( _spectator_example_name(\{{what}}), ::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}}), - \{{@type.name}}.new.as(::Spectator::Context), - \{{@type.name}}.\%tags + new.as(::Spectator::Context), + \%tags ) do |example| example.with_context(\{{@type.name}}) do \{% if block.args.empty? %} diff --git a/src/spectator/dsl/hooks.cr b/src/spectator/dsl/hooks.cr index 33b3428..e1194ba 100644 --- a/src/spectator/dsl/hooks.cr +++ b/src/spectator/dsl/hooks.cr @@ -17,7 +17,7 @@ module Spectator::DSL ::Spectator::DSL::Builder.{{type.id}}( ::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}}) - ) { \{{@type.name}}.\%hook } + ) { \%hook } end end From 803a09464df140123d05e652e01ca4b6aa9d281c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 12:32:13 -0700 Subject: [PATCH 144/399] Initial work to pass along pre-set tags Define pending examples and groups using this method. --- src/spectator/dsl/examples.cr | 23 ++++++++++++-- src/spectator/dsl/groups.cr | 57 +++++++++++++++++++++++------------ 2 files changed, 58 insertions(+), 22 deletions(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 8063b61..d789a44 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -8,7 +8,7 @@ module Spectator::DSL # Defines a macro to generate code for an example. # The *name* is the name given to the macro. # TODO: Mark example as pending if block is omitted. - macro define_example(name) + macro define_example(name, *tags, **metadata) # Defines an example. # # If a block is given, it is treated as the code to test. @@ -28,6 +28,19 @@ module Spectator::DSL def self.\%tags tags = _spectator_tags + {% if !tags.empty? %} + tags.concat({ {{tags.map(&.id.symbolize).splat}} }) + {% end %} + {% for k, v in metadata %} + cond = begin + {{v}} + end + if cond + tags.add({{k.id.symbolize}}) + else + tags.delete({{k.id.symbolize}}) + end + {% end %} \{% if !tags.empty? %} tags.concat({ \{{tags.map(&.id.symbolize).splat}} }) \{% end %} @@ -85,6 +98,12 @@ module Spectator::DSL define_example :specify - # TODO: pending, skip, and xit + define_example :xexample, :pending + + define_example :xspecify, :pending + + define_example :xit, :pending + + define_example :skip, :pending end end diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index ad35a30..d6dd9c2 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -5,7 +5,7 @@ module Spectator::DSL # DSL methods and macros for creating example groups. # This module should be included as a mix-in. module Groups - macro define_example_group(name) + macro define_example_group(name, *tags, **metadata) # Defines a new example group. # The *what* argument is a name or description of the group. # @@ -16,25 +16,36 @@ module Spectator::DSL class Group\%group < \{{@type.id}} _spectator_group_subject(\{{what}}) - \{% if !tags.empty? || !metadata.empty? %} - def self._spectator_tags - tags = super - \{% if !tags.empty? %} - tags.concat({ \{{tags.map(&.id.symbolize).splat}} }) - \{% end %} - \{% for k, v in metadata %} - cond = begin - \{{v}} - end - if cond - tags.add(\{{k.id.symbolize}}) - else - tags.delete(\{{k.id.symbolize}}) - end - \{% end %} - tags - end - \{% end %} + def self._spectator_tags + tags = super + {% if !tags.empty? %} + tags.concat({ {{tags.map(&.id.symbolize).splat}} }) + {% end %} + {% for k, v in metadata %} + cond = begin + {{v}} + end + if cond + tags.add({{k.id.symbolize}}) + else + tags.delete({{k.id.symbolize}}) + end + {% end %} + \{% if !tags.empty? %} + tags.concat({ \{{tags.map(&.id.symbolize).splat}} }) + \{% end %} + \{% for k, v in metadata %} + cond = begin + \{{v}} + end + if cond + tags.add(\{{k.id.symbolize}}) + else + tags.delete(\{{k.id.symbolize}}) + end + \{% end %} + tags + end ::Spectator::DSL::Builder.start_group( _spectator_group_name(\{{what}}), @@ -107,6 +118,12 @@ module Spectator::DSL define_example_group :context + define_example_group :xexample_group, :pending + + define_example_group :xdescribe, :pending + + define_example_group :xcontext, :pending + # TODO: sample, random_sample, and given end end From e093ec788e9e627cfba85b5c0abe9ef1ea0c3f0d Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 14:08:42 -0700 Subject: [PATCH 145/399] Clean up tags method generation by moving it to another macro --- src/spectator/dsl/examples.cr | 55 ++++++++++++++++------------------- src/spectator/dsl/groups.cr | 55 ++++++++++++++++------------------- 2 files changed, 50 insertions(+), 60 deletions(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index d789a44..b711502 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -26,36 +26,8 @@ module Spectator::DSL \{% raise "A description or block must be provided. Cannot use '{{name.id}}' alone." unless what || block %} \{% raise "Block argument count '{{name.id}}' hook must be 0..1" if block.args.size > 1 %} - def self.\%tags - tags = _spectator_tags - {% if !tags.empty? %} - tags.concat({ {{tags.map(&.id.symbolize).splat}} }) - {% end %} - {% for k, v in metadata %} - cond = begin - {{v}} - end - if cond - tags.add({{k.id.symbolize}}) - else - tags.delete({{k.id.symbolize}}) - end - {% end %} - \{% if !tags.empty? %} - tags.concat({ \{{tags.map(&.id.symbolize).splat}} }) - \{% end %} - \{% for k, v in metadata %} - cond = begin - \{{v}} - end - if cond - tags.add(\{{k.id.symbolize}}) - else - tags.delete(\{{k.id.symbolize}}) - end - \{% end %} - tags - end + _spectator_tags_method(%tags, :_spectator_tags, {{tags.splat(",")}} {{metadata.double_splat}}) + _spectator_tags_method(\%tags, %tags, \{{tags.splat(",")}} \{{metadata.double_splat}}) def \%test(\{{block.args.splat}}) : Nil \{{block.body}} @@ -92,6 +64,29 @@ module Spectator::DSL {% end %} end + # Defines a class method named *name* that combines tags + # returned by *source* with *tags* and *metadata*. + # Any falsey items from *metadata* are removed. + private macro _spectator_tags_method(name, source, *tags, **metadata) + def self.{{name.id}} + tags = {{source.id}} + {% unless tags.empty? %} + tags.concat({ {{tags.map(&.id.symbolize).splat}} }) + {% end %} + {% for k, v in metadata %} + cond = begin + {{v}} + end + if cond + tags.add({{k.id.symbolize}}) + else + tags.delete({{k.id.symbolize}}) + end + {% end %} + tags + end + end + define_example :example define_example :it diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index d6dd9c2..a570cb9 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -16,36 +16,8 @@ module Spectator::DSL class Group\%group < \{{@type.id}} _spectator_group_subject(\{{what}}) - def self._spectator_tags - tags = super - {% if !tags.empty? %} - tags.concat({ {{tags.map(&.id.symbolize).splat}} }) - {% end %} - {% for k, v in metadata %} - cond = begin - {{v}} - end - if cond - tags.add({{k.id.symbolize}}) - else - tags.delete({{k.id.symbolize}}) - end - {% end %} - \{% if !tags.empty? %} - tags.concat({ \{{tags.map(&.id.symbolize).splat}} }) - \{% end %} - \{% for k, v in metadata %} - cond = begin - \{{v}} - end - if cond - tags.add(\{{k.id.symbolize}}) - else - tags.delete(\{{k.id.symbolize}}) - end - \{% end %} - tags - end + _spectator_tags_method(:_spectator_tags, :super, {{tags.splat(", ")}} {{metadata.double_splat}}) + _spectator_tags_method(:_spectator_tags, :previous_def, \{{tags.splat(", ")}} \{{metadata.double_splat}}) ::Spectator::DSL::Builder.start_group( _spectator_group_name(\{{what}}), @@ -112,6 +84,29 @@ module Spectator::DSL {% end %} end + # Defines a class method named *name* that combines tags + # returned by *source* with *tags* and *metadata*. + # Any falsey items from *metadata* are removed. + private macro _spectator_tags_method(name, source, *tags, **metadata) + def self.{{name.id}} + tags = {{source.id}} + {% unless tags.empty? %} + tags.concat({ {{tags.map(&.id.symbolize).splat}} }) + {% end %} + {% for k, v in metadata %} + cond = begin + {{v}} + end + if cond + tags.add({{k.id.symbolize}}) + else + tags.delete({{k.id.symbolize}}) + end + {% end %} + tags + end + end + define_example_group :example_group define_example_group :describe From bd942bb644831a05e47baeb261adcce59767f406 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 14:13:27 -0700 Subject: [PATCH 146/399] Shorten tag method names Plain `tags` is safe to use as a class method on the group. --- src/spectator/dsl/examples.cr | 20 ++++++++++---------- src/spectator/dsl/groups.cr | 20 ++++++++++---------- src/spectator/test_context.cr | 2 +- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index b711502..4798af0 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -26,8 +26,8 @@ module Spectator::DSL \{% raise "A description or block must be provided. Cannot use '{{name.id}}' alone." unless what || block %} \{% raise "Block argument count '{{name.id}}' hook must be 0..1" if block.args.size > 1 %} - _spectator_tags_method(%tags, :_spectator_tags, {{tags.splat(",")}} {{metadata.double_splat}}) - _spectator_tags_method(\%tags, %tags, \{{tags.splat(",")}} \{{metadata.double_splat}}) + _spectator_tags(%tags, :tags, {{tags.splat(",")}} {{metadata.double_splat}}) + _spectator_tags(\%tags, %tags, \{{tags.splat(",")}} \{{metadata.double_splat}}) def \%test(\{{block.args.splat}}) : Nil \{{block.body}} @@ -67,23 +67,23 @@ module Spectator::DSL # Defines a class method named *name* that combines tags # returned by *source* with *tags* and *metadata*. # Any falsey items from *metadata* are removed. - private macro _spectator_tags_method(name, source, *tags, **metadata) + private macro _spectator_tags(name, source, *tags, **metadata) def self.{{name.id}} - tags = {{source.id}} + %tags = {{source.id}} {% unless tags.empty? %} - tags.concat({ {{tags.map(&.id.symbolize).splat}} }) + %tags.concat({ {{tags.map(&.id.symbolize).splat}} }) {% end %} {% for k, v in metadata %} - cond = begin + %cond = begin {{v}} end - if cond - tags.add({{k.id.symbolize}}) + if %cond + %tags.add({{k.id.symbolize}}) else - tags.delete({{k.id.symbolize}}) + %tags.delete({{k.id.symbolize}}) end {% end %} - tags + %tags end end diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index a570cb9..6f0aef6 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -16,13 +16,13 @@ module Spectator::DSL class Group\%group < \{{@type.id}} _spectator_group_subject(\{{what}}) - _spectator_tags_method(:_spectator_tags, :super, {{tags.splat(", ")}} {{metadata.double_splat}}) - _spectator_tags_method(:_spectator_tags, :previous_def, \{{tags.splat(", ")}} \{{metadata.double_splat}}) + _spectator_tags_method(:tags, :super, {{tags.splat(", ")}} {{metadata.double_splat}}) + _spectator_tags_method(:tags, :previous_def, \{{tags.splat(", ")}} \{{metadata.double_splat}}) ::Spectator::DSL::Builder.start_group( _spectator_group_name(\{{what}}), ::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}}), - _spectator_tags + tags ) \{{block.body}} @@ -89,21 +89,21 @@ module Spectator::DSL # Any falsey items from *metadata* are removed. private macro _spectator_tags_method(name, source, *tags, **metadata) def self.{{name.id}} - tags = {{source.id}} + %tags = {{source.id}} {% unless tags.empty? %} - tags.concat({ {{tags.map(&.id.symbolize).splat}} }) + %tags.concat({ {{tags.map(&.id.symbolize).splat}} }) {% end %} {% for k, v in metadata %} - cond = begin + %cond = begin {{v}} end - if cond - tags.add({{k.id.symbolize}}) + if %cond + %tags.add({{k.id.symbolize}}) else - tags.delete({{k.id.symbolize}}) + %tags.delete({{k.id.symbolize}}) end {% end %} - tags + %tags end end diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr index 8186daa..a4a3156 100644 --- a/src/spectator/test_context.cr +++ b/src/spectator/test_context.cr @@ -32,7 +32,7 @@ class SpectatorTestContext < SpectatorContext # Initial tags for tests. # This method should be overridden by example groups and examples. - def self._spectator_tags + def self.tags ::Spectator::Tags.new end end From ba967218faa063d98d598be4d6a0a404ef23c7f5 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 15:34:17 -0700 Subject: [PATCH 147/399] Move tags code to a common location --- src/spectator/dsl.cr | 1 + src/spectator/dsl/examples.cr | 26 +++----------------------- src/spectator/dsl/groups.cr | 30 +++++------------------------- src/spectator/dsl/tags.cr | 26 ++++++++++++++++++++++++++ 4 files changed, 35 insertions(+), 48 deletions(-) create mode 100644 src/spectator/dsl/tags.cr diff --git a/src/spectator/dsl.cr b/src/spectator/dsl.cr index e7b4b79..81fc816 100644 --- a/src/spectator/dsl.cr +++ b/src/spectator/dsl.cr @@ -5,6 +5,7 @@ require "./dsl/expectations" require "./dsl/groups" require "./dsl/hooks" require "./dsl/matchers" +require "./dsl/tags" require "./dsl/top" require "./dsl/values" diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 4798af0..a309901 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -1,10 +1,13 @@ require "../context" require "../source" require "./builder" +require "./tags" module Spectator::DSL # DSL methods for defining examples and test code. module Examples + include Tags + # Defines a macro to generate code for an example. # The *name* is the name given to the macro. # TODO: Mark example as pending if block is omitted. @@ -64,29 +67,6 @@ module Spectator::DSL {% end %} end - # Defines a class method named *name* that combines tags - # returned by *source* with *tags* and *metadata*. - # Any falsey items from *metadata* are removed. - private macro _spectator_tags(name, source, *tags, **metadata) - def self.{{name.id}} - %tags = {{source.id}} - {% unless tags.empty? %} - %tags.concat({ {{tags.map(&.id.symbolize).splat}} }) - {% end %} - {% for k, v in metadata %} - %cond = begin - {{v}} - end - if %cond - %tags.add({{k.id.symbolize}}) - else - %tags.delete({{k.id.symbolize}}) - end - {% end %} - %tags - end - end - define_example :example define_example :it diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 6f0aef6..20f8a4e 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -1,10 +1,13 @@ require "../source" require "./builder" +require "./tags" module Spectator::DSL # DSL methods and macros for creating example groups. # This module should be included as a mix-in. module Groups + include Tags + macro define_example_group(name, *tags, **metadata) # Defines a new example group. # The *what* argument is a name or description of the group. @@ -16,8 +19,8 @@ module Spectator::DSL class Group\%group < \{{@type.id}} _spectator_group_subject(\{{what}}) - _spectator_tags_method(:tags, :super, {{tags.splat(", ")}} {{metadata.double_splat}}) - _spectator_tags_method(:tags, :previous_def, \{{tags.splat(", ")}} \{{metadata.double_splat}}) + _spectator_tags(:tags, :super, {{tags.splat(", ")}} {{metadata.double_splat}}) + _spectator_tags(:tags, :previous_def, \{{tags.splat(", ")}} \{{metadata.double_splat}}) ::Spectator::DSL::Builder.start_group( _spectator_group_name(\{{what}}), @@ -84,29 +87,6 @@ module Spectator::DSL {% end %} end - # Defines a class method named *name* that combines tags - # returned by *source* with *tags* and *metadata*. - # Any falsey items from *metadata* are removed. - private macro _spectator_tags_method(name, source, *tags, **metadata) - def self.{{name.id}} - %tags = {{source.id}} - {% unless tags.empty? %} - %tags.concat({ {{tags.map(&.id.symbolize).splat}} }) - {% end %} - {% for k, v in metadata %} - %cond = begin - {{v}} - end - if %cond - %tags.add({{k.id.symbolize}}) - else - %tags.delete({{k.id.symbolize}}) - end - {% end %} - %tags - end - end - define_example_group :example_group define_example_group :describe diff --git a/src/spectator/dsl/tags.cr b/src/spectator/dsl/tags.cr new file mode 100644 index 0000000..a73c396 --- /dev/null +++ b/src/spectator/dsl/tags.cr @@ -0,0 +1,26 @@ +module Spectator::DSL + module Tags + # Defines a class method named *name* that combines tags + # returned by *source* with *tags* and *metadata*. + # Any falsey items from *metadata* are removed. + private macro _spectator_tags(name, source, *tags, **metadata) + def self.{{name.id}} + %tags = {{source.id}} + {% unless tags.empty? %} + %tags.concat({ {{tags.map(&.id.symbolize).splat}} }) + {% end %} + {% for k, v in metadata %} + %cond = begin + {{v}} + end + if %cond + %tags.add({{k.id.symbolize}}) + else + %tags.delete({{k.id.symbolize}}) + end + {% end %} + %tags + end + end + end +end From 20680f37cb1d8d50f3b580fe1168f5d7f9513d35 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 15:39:29 -0700 Subject: [PATCH 148/399] Add docs --- src/spectator/dsl/examples.cr | 9 +++++++++ src/spectator/dsl/groups.cr | 16 ++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index a309901..b466b02 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -10,6 +10,11 @@ module Spectator::DSL # Defines a macro to generate code for an example. # The *name* is the name given to the macro. + # + # Default tags can be provided with *tags* and *metadata*. + # The tags are merged with parent groups. + # Any items with falsey values from *metadata* remove the corresponding tag. + # # TODO: Mark example as pending if block is omitted. macro define_example(name, *tags, **metadata) # Defines an example. @@ -24,6 +29,10 @@ module Spectator::DSL # # The example will be marked as pending if the block is omitted. # A block or name must be provided. + # + # Tags can be specified by adding symbols (keywords) after the first argument. + # Key-value pairs can also be specified. + # Any falsey items will remove a previously defined tag. macro {{name.id}}(what = nil, *tags, **metadata, &block) \{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %} \{% raise "A description or block must be provided. Cannot use '{{name.id}}' alone." unless what || block %} diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 20f8a4e..0bb8bfd 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -8,10 +8,26 @@ module Spectator::DSL module Groups include Tags + # Defines a macro to generate code for an example group. + # The *name* is the name given to the macro. + # + # Default tags can be provided with *tags* and *metadata*. + # The tags are merged with parent groups. + # Any items with falsey values from *metadata* remove the corresponding tag. macro define_example_group(name, *tags, **metadata) # Defines a new example group. # The *what* argument is a name or description of the group. # + # The first argument names the example (test). + # Typically, this specifies what is being tested. + # This argument is also used as the subject. + # When it is a type name, it becomes an explicit, which overrides any previous subjects. + # Otherwise it becomes an implicit subject, which doesn't override explicitly defined subjects. + # + # Tags can be specified by adding symbols (keywords) after the first argument. + # Key-value pairs can also be specified. + # Any falsey items will remove a previously defined tag. + # # TODO: Handle string interpolation in example and group names. macro {{name.id}}(what, *tags, **metadata, &block) \{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %} From 2301e44bd16c6f9ec3cc13789515626b66c056f7 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 16:05:32 -0700 Subject: [PATCH 149/399] Add pending? method --- src/spectator/spec/node.cr | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/spectator/spec/node.cr b/src/spectator/spec/node.cr index bbd3214..c499f6f 100644 --- a/src/spectator/spec/node.cr +++ b/src/spectator/spec/node.cr @@ -53,6 +53,12 @@ module Spectator # Indicates whether the node has completed. abstract def finished? : Bool + # Checks if the node has been marked as pending. + # Pending items should be skipped during execution. + def pending? + tags.includes?(:pending) + end + # Constructs the full name or description of the node. # This prepends names of groups this node is part of. def to_s(io) From 3bc567da4c52179eab83edadcf98d224210231ed Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 16:36:15 -0700 Subject: [PATCH 150/399] Skip examples marked pending --- src/spectator/example.cr | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index dcc8f07..eee98e2 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -55,6 +55,11 @@ module Spectator Log.debug { "Running example #{self}" } Log.warn { "Example #{self} already ran" } if @finished + if pending? + Log.debug { "Skipping example #{self} - marked pending" } + return @result = PendingResult.new + end + previous_example = @@current @@current = self From d43d309a4702d8680655b0e03cd2127d65c83684 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 16:39:41 -0700 Subject: [PATCH 151/399] Note about tag inheritence --- src/spectator/example.cr | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index eee98e2..e012ddf 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -27,6 +27,7 @@ module Spectator # The *source* tracks where the example exists in source code. # The example will be assigned to *group* if it is provided. # A set of *tags* can be used for filtering and modifying example behavior. + # Note: The tags will not be merged with the parent tags. def initialize(@context : Context, @entrypoint : self ->, name : String? = nil, source : Source? = nil, group : ExampleGroup? = nil, tags = Tags.new) @@ -41,6 +42,7 @@ module Spectator # The *source* tracks where the example exists in source code. # The example will be assigned to *group* if it is provided. # A set of *tags* can be used for filtering and modifying example behavior. + # Note: The tags will not be merged with the parent tags. def initialize(name : String? = nil, source : Source? = nil, group : ExampleGroup? = nil, tags = Tags.new, &block : self ->) super(name, source, group, tags) From 73985772b7c863dbe7e0af8b94b29fd6449aac5b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 16:43:13 -0700 Subject: [PATCH 152/399] DSL fully added back in --- src/spectator/dsl.cr | 11 +- src/spectator/dsl/mocks.cr | 294 +++++++++++++++++----------------- src/spectator/test_context.cr | 1 + 3 files changed, 150 insertions(+), 156 deletions(-) diff --git a/src/spectator/dsl.cr b/src/spectator/dsl.cr index 81fc816..6a98e85 100644 --- a/src/spectator/dsl.cr +++ b/src/spectator/dsl.cr @@ -1,13 +1,4 @@ -# require "./dsl/*" -require "./dsl/builder" -require "./dsl/examples" -require "./dsl/expectations" -require "./dsl/groups" -require "./dsl/hooks" -require "./dsl/matchers" -require "./dsl/tags" -require "./dsl/top" -require "./dsl/values" +require "./dsl/*" module Spectator # Namespace containing methods representing the spec domain specific language. diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 7ffcfc8..d66c2de 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -1,173 +1,175 @@ require "../mocks" module Spectator::DSL - macro double(name = "Anonymous", **stubs, &block) - {% if name.is_a?(StringLiteral) || name.is_a?(StringInterpolation) %} - anonymous_double({{name}}, {{stubs.double_splat}}) - {% else %} - {% - safe_name = name.id.symbolize.gsub(/\W/, "_").id - type_name = "Double#{safe_name}".id - %} - - {% if block.is_a?(Nop) %} - create_double({{type_name}}, {{name}}, {{stubs.double_splat}}) + module Mocks + macro double(name = "Anonymous", **stubs, &block) + {% if name.is_a?(StringLiteral) || name.is_a?(StringInterpolation) %} + anonymous_double({{name}}, {{stubs.double_splat}}) {% else %} - define_double({{type_name}}, {{name}}, {{stubs.double_splat}}) {{block}} - {% end %} - {% end %} - end + {% + safe_name = name.id.symbolize.gsub(/\W/, "_").id + type_name = "Double#{safe_name}".id + %} - macro create_double(type_name, name, **stubs) - {% type_name.resolve? || raise("Could not find a double labeled #{name}") %} - - {{type_name}}.new.tap do |%double| - {% for name, value in stubs %} - allow(%double).to receive({{name.id}}).and_return({{value}}) + {% if block.is_a?(Nop) %} + create_double({{type_name}}, {{name}}, {{stubs.double_splat}}) + {% else %} + define_double({{type_name}}, {{name}}, {{stubs.double_splat}}) {{block}} + {% end %} {% end %} end - end - macro define_double(type_name, name, **stubs, &block) - {% begin %} - {% if (name.is_a?(Path) || name.is_a?(Generic)) && (resolved = name.resolve?) %} - verify_double({{name}}) - class {{type_name}} < ::Spectator::Mocks::VerifyingDouble(::{{resolved.id}}) - {% else %} - class {{type_name}} < ::Spectator::Mocks::Double - def initialize(null = false) - super({{name.id.stringify}}, null) - end - {% end %} + macro create_double(type_name, name, **stubs) + {% type_name.resolve? || raise("Could not find a double labeled #{name}") %} - def as_null_object - {{type_name}}.new(true) + {{type_name}}.new.tap do |%double| + {% for name, value in stubs %} + allow(%double).to receive({{name.id}}).and_return({{value}}) + {% end %} end - - # TODO: Do something with **stubs? - - {{block.body}} end - {% end %} - end - def anonymous_double(name = "Anonymous", **stubs) - Mocks::AnonymousDouble.new(name, stubs) - end + macro define_double(type_name, name, **stubs, &block) + {% begin %} + {% if (name.is_a?(Path) || name.is_a?(Generic)) && (resolved = name.resolve?) %} + verify_double({{name}}) + class {{type_name}} < ::Spectator::Mocks::VerifyingDouble(::{{resolved.id}}) + {% else %} + class {{type_name}} < ::Spectator::Mocks::Double + def initialize(null = false) + super({{name.id.stringify}}, null) + end + {% end %} - macro null_double(name, **stubs, &block) - {% if name.is_a?(StringLiteral) || name.is_a?(StringInterpolation) %} - anonymous_null_double({{name}}, {{stubs.double_splat}}) - {% else %} - {% - safe_name = name.id.symbolize.gsub(/\W/, "_").id - type_name = "Double#{safe_name}".id - %} + def as_null_object + {{type_name}}.new(true) + end - {% if block.is_a?(Nop) %} - create_null_double({{type_name}}, {{name}}, {{stubs.double_splat}}) - {% else %} - define_null_double({{type_name}}, {{name}}, {{stubs.double_splat}}) {{block}} - {% end %} - {% end %} - end - - macro create_null_double(type_name, name, **stubs) - {% type_name.resolve? || raise("Could not find a double labeled #{name}") %} - - {{type_name}}.new(true).tap do |%double| - {% for name, value in stubs %} - allow(%double).to receive({{name.id}}).and_return({{value}}) - {% end %} - end - end - - macro define_null_double(type_name, name, **stubs, &block) - class {{type_name}} < ::Spectator::Mocks::Double - def initialize(null = true) - super({{name.id.stringify}}, null) - end - - def as_null_object - {{type_name}}.new(true) - end - - # TODO: Do something with **stubs? - - {{block.body}} - end - end - - def anonymous_null_double(name = "Anonymous", **stubs) - AnonymousNullDouble.new(name, stubs) - end - - macro mock(name, &block) - {% resolved = name.resolve - type = if resolved < Reference - :class - elsif resolved < Value - :struct - else - :module - end %} - {% begin %} - {{type.id}} ::{{resolved.id}} - include ::Spectator::Mocks::Stubs + # TODO: Do something with **stubs? {{block.body}} end - {% end %} - end + {% end %} + end - macro verify_double(name, &block) - {% resolved = name.resolve - type = if resolved < Reference - :class - elsif resolved < Value - :struct - else - :module - end %} - {% begin %} - {{type.id}} ::{{resolved.id}} - include ::Spectator::Mocks::Reflection + def anonymous_double(name = "Anonymous", **stubs) + Mocks::AnonymousDouble.new(name, stubs) + end - macro finished - _spectator_reflect - end + macro null_double(name, **stubs, &block) + {% if name.is_a?(StringLiteral) || name.is_a?(StringInterpolation) %} + anonymous_null_double({{name}}, {{stubs.double_splat}}) + {% else %} + {% + safe_name = name.id.symbolize.gsub(/\W/, "_").id + type_name = "Double#{safe_name}".id + %} + + {% if block.is_a?(Nop) %} + create_null_double({{type_name}}, {{name}}, {{stubs.double_splat}}) + {% else %} + define_null_double({{type_name}}, {{name}}, {{stubs.double_splat}}) {{block}} + {% end %} + {% end %} + end + + macro create_null_double(type_name, name, **stubs) + {% type_name.resolve? || raise("Could not find a double labeled #{name}") %} + + {{type_name}}.new(true).tap do |%double| + {% for name, value in stubs %} + allow(%double).to receive({{name.id}}).and_return({{value}}) + {% end %} end - {% end %} - end + end - def allow(thing) - Mocks::Allow.new(thing) - end + macro define_null_double(type_name, name, **stubs, &block) + class {{type_name}} < ::Spectator::Mocks::Double + def initialize(null = true) + super({{name.id.stringify}}, null) + end - def allow_any_instance_of(type : T.class) forall T - Mocks::AllowAnyInstance(T).new - end + def as_null_object + {{type_name}}.new(true) + end - macro expect_any_instance_of(type, _source_file = __FILE__, _source_line = __LINE__) - %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - ::Spectator::Mocks::ExpectAnyInstance({{type}}).new(%source) - end + # TODO: Do something with **stubs? - macro receive(method_name, _source_file = __FILE__, _source_line = __LINE__, &block) - %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - {% if block.is_a?(Nop) %} - ::Spectator::Mocks::NilMethodStub.new({{method_name.id.symbolize}}, %source) - {% else %} - ::Spectator::Mocks::ProcMethodStub.create({{method_name.id.symbolize}}, %source) { {{block.body}} } - {% end %} - end + {{block.body}} + end + end - macro receive_messages(_source_file = __FILE__, _source_line = __LINE__, **stubs) - %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - %stubs = [] of ::Spectator::Mocks::MethodStub - {% for name, value in stubs %} - %stubs << ::Spectator::Mocks::ValueMethodStub.new({{name.id.symbolize}}, %source, {{value}}) - {% end %} - %stubs + def anonymous_null_double(name = "Anonymous", **stubs) + AnonymousNullDouble.new(name, stubs) + end + + macro mock(name, &block) + {% resolved = name.resolve + type = if resolved < Reference + :class + elsif resolved < Value + :struct + else + :module + end %} + {% begin %} + {{type.id}} ::{{resolved.id}} + include ::Spectator::Mocks::Stubs + + {{block.body}} + end + {% end %} + end + + macro verify_double(name, &block) + {% resolved = name.resolve + type = if resolved < Reference + :class + elsif resolved < Value + :struct + else + :module + end %} + {% begin %} + {{type.id}} ::{{resolved.id}} + include ::Spectator::Mocks::Reflection + + macro finished + _spectator_reflect + end + end + {% end %} + end + + def allow(thing) + Mocks::Allow.new(thing) + end + + def allow_any_instance_of(type : T.class) forall T + Mocks::AllowAnyInstance(T).new + end + + macro expect_any_instance_of(type, _source_file = __FILE__, _source_line = __LINE__) + %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) + ::Spectator::Mocks::ExpectAnyInstance({{type}}).new(%source) + end + + macro receive(method_name, _source_file = __FILE__, _source_line = __LINE__, &block) + %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) + {% if block.is_a?(Nop) %} + ::Spectator::Mocks::NilMethodStub.new({{method_name.id.symbolize}}, %source) + {% else %} + ::Spectator::Mocks::ProcMethodStub.create({{method_name.id.symbolize}}, %source) { {{block.body}} } + {% end %} + end + + macro receive_messages(_source_file = __FILE__, _source_line = __LINE__, **stubs) + %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) + %stubs = [] of ::Spectator::Mocks::MethodStub + {% for name, value in stubs %} + %stubs << ::Spectator::Mocks::ValueMethodStub.new({{name.id.symbolize}}, %source, {{value}}) + {% end %} + %stubs + end end end diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr index a4a3156..d56d817 100644 --- a/src/spectator/test_context.cr +++ b/src/spectator/test_context.cr @@ -13,6 +13,7 @@ class SpectatorTestContext < SpectatorContext include ::Spectator::DSL::Groups include ::Spectator::DSL::Hooks include ::Spectator::DSL::Matchers + include ::Spectator::DSL::Mocks include ::Spectator::DSL::Values @subject = ::Spectator::LazyWrapper.new From 3b5086c74bc58d1318ee0606cd2ad89cd2418641 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 17:27:02 -0700 Subject: [PATCH 153/399] Re-add mocks to harness --- src/spectator/harness.cr | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 6cfd8dc..44bc321 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -1,5 +1,6 @@ require "./error_result" require "./expectation" +require "./mocks" require "./pass_result" require "./result" @@ -35,6 +36,8 @@ module Spectator # Retrieves the harness for the current running example. class_getter! current : self + getter mocks = Mocks::Registry.new + # Wraps an example with a harness and runs test code. # A block provided to this method is considered to be the test code. # The value of `.current` is set to the harness for the duration of the test. From b07dca697e3a8e1a80eeb9d101bfa83322ba1ac7 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 17:27:17 -0700 Subject: [PATCH 154/399] Remove seemingly unused context stubs code --- src/spectator/mocks/registry.cr | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/spectator/mocks/registry.cr b/src/spectator/mocks/registry.cr index 37af7ca..ab09bd8 100644 --- a/src/spectator/mocks/registry.cr +++ b/src/spectator/mocks/registry.cr @@ -11,23 +11,6 @@ module Spectator::Mocks @all_instances = {} of String => Entry @entries = {} of Key => Entry - def initialize(context : TestContext) - current_context = context - while current_context - current_context.stubs.each do |k, v| - stubs = if @all_instances.has_key?(k) - @all_instances[k].stubs - else - entry = Entry.new - @all_instances[k] = entry - entry.stubs - end - stubs.concat(v) - end - current_context = current_context.parent? - end - end - def reset : Nil @entries.clear end From d3ad0963cdb3fdd56acc2a5fb4e9b8ff58c51f48 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 17:31:50 -0700 Subject: [PATCH 155/399] Minor fixes --- spec/spec_helper.cr | 3 ++- src/spectator/dsl/matchers.cr | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 736ac91..c9176e6 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -1,11 +1,12 @@ require "../src/spectator" +require "../src/spectator/should" require "./helpers/**" macro it_fails(description = nil, &block) it {{description}} do expect do {{block.body}} - end.to raise_error(Spectator::ExampleFailed) + end.to raise_error(Spectator::ExpectationFailed) end end diff --git a/src/spectator/dsl/matchers.cr b/src/spectator/dsl/matchers.cr index 8d51193..c5ceeab 100644 --- a/src/spectator/dsl/matchers.cr +++ b/src/spectator/dsl/matchers.cr @@ -718,13 +718,13 @@ module Spectator::DSL # expect { subject << :foo }.to change(&.size).by(1) # ``` macro change(&expression) - {% if block.args.size == 1 && block.args[0] =~ /^__arg\d+$/ && block.body.is_a?(Call) && block.body.id =~ /^__arg\d+\./ %} - {% method_name = block.body.id.split('.')[1..-1].join('.') %} + {% if expression.args.size == 1 && expression.args[0] =~ /^__arg\d+$/ && expression.body.is_a?(Call) && expression.body.id =~ /^__arg\d+\./ %} + {% method_name = expression.body.id.split('.')[1..-1].join('.') %} %block = ::Spectator::Block.new({{"#" + method_name}}) do subject.{{method_name.id}} end - {% elsif block.args.empty? %} - %block = ::Spectator::Block.new({{"`" + block.body.stringify + "`"}}) {{block}} + {% elsif expression.args.empty? %} + %block = ::Spectator::Block.new({{"`" + expression.body.stringify + "`"}}) {{expression}} {% else %} {% raise "Unexpected block arguments in 'expect' call" %} {% end %} From b6335ab4bc50641b9aad535b84c9ffd0d1334243 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 17:40:03 -0700 Subject: [PATCH 156/399] Update includes list --- src/spectator/includes.cr | 47 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/spectator/includes.cr b/src/spectator/includes.cr index 5fa8f02..665b8e9 100644 --- a/src/spectator/includes.cr +++ b/src/spectator/includes.cr @@ -4,8 +4,55 @@ # Including all files with a wildcard would accidentally enable should-syntax. # Unfortunately, that leads to the existence of this file to include everything but that file. +require "./abstract_expression" +require "./anything" +require "./block" require "./command_line_arguments_config_source" +require "./composite_example_filter" require "./config_builder" require "./config" +require "./config_source" +require "./context" +require "./context_delegate" +require "./context_method" require "./dsl" +require "./error_result" +require "./events" +require "./example_context_delegate" +require "./example_context_method" +require "./example" +require "./example_filter" +require "./example_group" +require "./example_group_hook" +require "./example_hook" +require "./example_iterator" +require "./example_procsy_hook" +require "./expectation" +require "./expectation_failed" +require "./expression" +require "./fail_result" +require "./formatting" +require "./harness" +require "./label" +require "./lazy" +require "./lazy_wrapper" +require "./line_example_filter" +require "./matchers" +require "./mocks" +require "./name_example_filter" +require "./null_context" +require "./null_example_filter" +require "./pass_result" +require "./pending_result" +require "./profile" +require "./report" +require "./result" +require "./runner" +require "./source" +require "./source_example_filter" +require "./spec" +require "./tags" require "./test_context" +require "./test_suite" +require "./value" +require "./wrapper" From e8b284fc60cd98e1210fa643af4d97db90821a33 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 17:40:33 -0700 Subject: [PATCH 157/399] Fix namepsaces crystal spec compiles again! --- src/spectator/dsl/mocks.cr | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index d66c2de..049a9f1 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -53,7 +53,7 @@ module Spectator::DSL end def anonymous_double(name = "Anonymous", **stubs) - Mocks::AnonymousDouble.new(name, stubs) + ::Spectator::Mocks::AnonymousDouble.new(name, stubs) end macro null_double(name, **stubs, &block) @@ -100,7 +100,7 @@ module Spectator::DSL end def anonymous_null_double(name = "Anonymous", **stubs) - AnonymousNullDouble.new(name, stubs) + ::Spectator::Mocks::AnonymousNullDouble.new(name, stubs) end macro mock(name, &block) @@ -142,11 +142,11 @@ module Spectator::DSL end def allow(thing) - Mocks::Allow.new(thing) + ::Spectator::Mocks::Allow.new(thing) end def allow_any_instance_of(type : T.class) forall T - Mocks::AllowAnyInstance(T).new + ::Spectator::Mocks::AllowAnyInstance(T).new end macro expect_any_instance_of(type, _source_file = __FILE__, _source_line = __LINE__) From 17a3f27029c411576621cde1e902015eec969250 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 18:57:29 -0700 Subject: [PATCH 158/399] Address Ameba errors --- src/spectator/abstract_expression.cr | 2 +- src/spectator/lazy.cr | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/spectator/abstract_expression.cr b/src/spectator/abstract_expression.cr index 3bb20f2..dc4ba13 100644 --- a/src/spectator/abstract_expression.cr +++ b/src/spectator/abstract_expression.cr @@ -47,7 +47,7 @@ module Spectator # This consists of the label (if one is available) and the value. def inspect(io) if (label = @label) - io << @label + io << label io << ':' io << ' ' end diff --git a/src/spectator/lazy.cr b/src/spectator/lazy.cr index 25aaed5..7a477b3 100644 --- a/src/spectator/lazy.cr +++ b/src/spectator/lazy.cr @@ -8,8 +8,8 @@ module Spectator # The block should return the value to store. # Subsequent calls will return the same value and not yield. def get(&block : -> T) - if (value = @value) - value.get + if (existing = @value) + existing.get else yield.tap do |value| @value = Value.new(value) From f5713efc624e97ed8279ef1ea622e754a0da4493 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 19:00:05 -0700 Subject: [PATCH 159/399] Split Ameba and formatting --- .gitlab-ci.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 630d59b..caf6bad 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,11 +16,15 @@ spec: - shards - crystal spec --error-on-warnings +format: + script: + - shards + - crystal tool format --check + style: script: - shards - bin/ameba - - crystal tool format --check nightly: image: "crystallang/crystal:nightly" From 9d139dfeedf5eeb41aa3b7c3cdd17e183b02451f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 19:42:46 -0700 Subject: [PATCH 160/399] Hack together result output --- src/spectator.cr | 2 +- src/spectator/error_result.cr | 10 +++ src/spectator/example.cr | 12 ++- src/spectator/fail_result.cr | 14 +++- src/spectator/formatting/color.cr | 2 +- .../formatting/document_formatter.cr | 6 +- src/spectator/formatting/dots_formatter.cr | 6 +- .../formatting/error_junit_test_case.cr | 2 +- src/spectator/formatting/failure_block.cr | 8 +- src/spectator/formatting/failure_command.cr | 2 +- .../formatting/failure_junit_test_case.cr | 4 +- src/spectator/formatting/junit_test_suite.cr | 10 +-- src/spectator/formatting/stats_counter.cr | 2 +- .../formatting/successful_junit_test_case.cr | 2 +- src/spectator/formatting/tap_test_line.cr | 2 +- src/spectator/harness.cr | 7 +- src/spectator/includes.cr | 1 - src/spectator/pass_result.cr | 10 +++ src/spectator/pending_result.cr | 12 ++- src/spectator/profile.cr | 4 +- src/spectator/report.cr | 14 ++-- src/spectator/result.cr | 8 +- src/spectator/runner.cr | 81 ------------------- src/spectator/spec.cr | 14 +--- src/spectator/spec/runner.cr | 71 +++++++++++++++- 25 files changed, 168 insertions(+), 138 deletions(-) delete mode 100644 src/spectator/runner.cr diff --git a/src/spectator.cr b/src/spectator.cr index a27367c..dd53ce3 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -67,7 +67,7 @@ module Spectator # Build the spec and run it. DSL::Builder.config = config spec = DSL::Builder.build - spec.run + spec.run(config.example_filter) true rescue ex # Catch all unhandled exceptions here. diff --git a/src/spectator/error_result.cr b/src/spectator/error_result.cr index cb47a4f..00cdeae 100644 --- a/src/spectator/error_result.cr +++ b/src/spectator/error_result.cr @@ -10,9 +10,19 @@ module Spectator visitor.error end + # Calls the `error` method on *visitor*. + def accept(visitor) + visitor.error(yield self) + end + # One-word description of the result. def to_s(io) io << "error" end + + # TODO + def to_json(builder) + builder.string("ERROR") + end end end diff --git a/src/spectator/example.cr b/src/spectator/example.cr index e012ddf..0efede2 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -17,7 +17,10 @@ module Spectator getter? finished : Bool = false # 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. # An instance to run the test code in is given by *context*. @@ -59,7 +62,7 @@ module Spectator if pending? Log.debug { "Skipping example #{self} - marked pending" } - return @result = PendingResult.new + return @result = PendingResult.new(self) end previous_example = @@current @@ -146,6 +149,11 @@ module Spectator io << result end + # TODO + def to_json(builder) + builder.string("EXAMPLE") + end + # Wraps an example to behave like a `Proc`. # This is typically used for an *around_each* hook. # Invoking `#call` or `#run` will run the example. diff --git a/src/spectator/fail_result.cr b/src/spectator/fail_result.cr index 710698a..ead0e79 100644 --- a/src/spectator/fail_result.cr +++ b/src/spectator/fail_result.cr @@ -11,8 +11,8 @@ module Spectator # Creates a failure result. # The *elapsed* argument is the length of time it took to run the example. # The *error* is the exception raised that caused the failure. - def initialize(elapsed, @error) - super(elapsed) + def initialize(example, elapsed, @error, expectations = [] of Expectation) + super(example, elapsed, expectations) end # Calls the `failure` method on *visitor*. @@ -20,9 +20,19 @@ module Spectator visitor.failure end + # Calls the `failure` method on *visitor*. + def accept(visitor) + visitor.failure(yield self) + end + # One-word description of the result. def to_s(io) io << "fail" end + + # TODO + def to_json(builder) + builder.string("FAIL") + end end end diff --git a/src/spectator/formatting/color.cr b/src/spectator/formatting/color.cr index ed02ddc..889f878 100644 --- a/src/spectator/formatting/color.cr +++ b/src/spectator/formatting/color.cr @@ -15,7 +15,7 @@ module Spectator::Formatting } # Colorizes some text with the success color. - def success(text) + def pass(text) text.colorize(COLORS[:success]) end diff --git a/src/spectator/formatting/document_formatter.cr b/src/spectator/formatting/document_formatter.cr index 910c254..487bf53 100644 --- a/src/spectator/formatting/document_formatter.cr +++ b/src/spectator/formatting/document_formatter.cr @@ -29,7 +29,7 @@ module Spectator::Formatting # Produces a single character output based on a result. def end_example(result) @previous_hierarchy.size.times { @io.print INDENT } - @io.puts result.call(Color) { result.example.description } + @io.puts result.accept(Color) { result.example.name } end # Produces a list of groups making up the hierarchy for an example. @@ -38,7 +38,7 @@ module Spectator::Formatting group = example.group while group.is_a?(ExampleGroup) hierarchy << group - group = group.parent + group = group.group? end hierarchy.reverse end @@ -57,7 +57,7 @@ module Spectator::Formatting private def print_sub_hierarchy(index, sub_hierarchy) sub_hierarchy.each do |group| index.times { @io.print INDENT } - @io.puts group.description + @io.puts group.name index += 1 end end diff --git a/src/spectator/formatting/dots_formatter.cr b/src/spectator/formatting/dots_formatter.cr index 392b26e..1fd35c6 100644 --- a/src/spectator/formatting/dots_formatter.cr +++ b/src/spectator/formatting/dots_formatter.cr @@ -21,7 +21,7 @@ module Spectator::Formatting # Produces a single character output based on a result. def end_example(result) - @io.print result.call(Character) + @io.print result.accept(Character) end # Interface for `Result` to pick a character for output. @@ -37,8 +37,8 @@ module Spectator::Formatting } # Character output for a successful example. - def success - Color.success(CHARACTERS[:success]) + def pass + Color.pass(CHARACTERS[:success]) end # Character output for a failed example. diff --git a/src/spectator/formatting/error_junit_test_case.cr b/src/spectator/formatting/error_junit_test_case.cr index f832355..f1a12ce 100644 --- a/src/spectator/formatting/error_junit_test_case.cr +++ b/src/spectator/formatting/error_junit_test_case.cr @@ -7,7 +7,7 @@ module Spectator::Formatting private getter result # Creates the JUnit test case. - def initialize(@result : ErroredResult) + def initialize(@result : ErrorResult) end # Adds the exception to the XML block. diff --git a/src/spectator/formatting/failure_block.cr b/src/spectator/formatting/failure_block.cr index 5ee5aaa..ff24022 100644 --- a/src/spectator/formatting/failure_block.cr +++ b/src/spectator/formatting/failure_block.cr @@ -16,7 +16,7 @@ module Spectator::Formatting # Creates the failure block. # The *index* uniquely identifies the failure in the output. # The *result* is the outcome of the failed example. - def initialize(@index : Int32, @result : FailedResult) + def initialize(@index : Int32, @result : FailResult) end # Creates the block of text describing the failure. @@ -47,12 +47,12 @@ module Spectator::Formatting # then an error stacktrace if an error occurred. private def content(indent) unsatisfied_expectations(indent) - error_stacktrace(indent) if @result.is_a?(ErroredResult) + error_stacktrace(indent) if @result.is_a?(ErrorResult) end # Produces a list of unsatisfied expectations and their values. 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 indent.increase do @@ -66,7 +66,7 @@ module Spectator::Formatting private def matcher_values(indent, expectation) MatchDataValues.new(expectation.values).each do |pair| colored_pair = if expectation.satisfied? - Color.success(pair) + Color.pass(pair) else Color.failure(pair) end diff --git a/src/spectator/formatting/failure_command.cr b/src/spectator/formatting/failure_command.cr index 5d4187c..1d4247c 100644 --- a/src/spectator/formatting/failure_command.cr +++ b/src/spectator/formatting/failure_command.cr @@ -13,7 +13,7 @@ module Spectator::Formatting # Colorizes the command instance based on the result. def self.color(result) - result.call(Color) { new(result.example) } + result.accept(Color) { new(result.example) } end end end diff --git a/src/spectator/formatting/failure_junit_test_case.cr b/src/spectator/formatting/failure_junit_test_case.cr index 99b0265..99181ea 100644 --- a/src/spectator/formatting/failure_junit_test_case.cr +++ b/src/spectator/formatting/failure_junit_test_case.cr @@ -7,7 +7,7 @@ module Spectator::Formatting private getter result # Creates the JUnit test case. - def initialize(@result : FailedResult) + def initialize(@result : FailResult) end # Status string specific to the result type. @@ -18,7 +18,7 @@ module Spectator::Formatting # Adds the failed expectations to the XML block. private def content(xml) super - @result.expectations.each_unsatisfied do |expectation| + @result.expectations.reject(&.satisfied?).each do |expectation| xml.element("failure", message: expectation.failure_message) do expectation_values(expectation.values, xml) end diff --git a/src/spectator/formatting/junit_test_suite.cr b/src/spectator/formatting/junit_test_suite.cr index 9abca41..f7abc4b 100644 --- a/src/spectator/formatting/junit_test_suite.cr +++ b/src/spectator/formatting/junit_test_suite.cr @@ -25,7 +25,7 @@ module Spectator::Formatting # Adds the test case elements to the XML. private def add_test_cases(xml) @report.each do |result| - test_case = result.call(JUnitTestCaseSelector) { |r| r } + test_case = result.accept(JUnitTestCaseSelector) { |r| r } test_case.to_xml(xml) end end @@ -50,18 +50,18 @@ module Spectator::Formatting extend self # Creates a successful JUnit test case. - def success(result) - SuccessfulJUnitTestCase.new(result.as(SuccessfulResult)) + def pass(result) + SuccessfulJUnitTestCase.new(result.as(PassResult)) end # Creates a failure JUnit test case. def failure(result) - FailureJUnitTestCase.new(result.as(FailedResult)) + FailureJUnitTestCase.new(result.as(FailResult)) end # Creates an error JUnit test case. def error(result) - ErrorJUnitTestCase.new(result.as(ErroredResult)) + ErrorJUnitTestCase.new(result.as(ErrorResult)) end # Creates a skipped JUnit test case. diff --git a/src/spectator/formatting/stats_counter.cr b/src/spectator/formatting/stats_counter.cr index 714c63a..fa0e697 100644 --- a/src/spectator/formatting/stats_counter.cr +++ b/src/spectator/formatting/stats_counter.cr @@ -20,7 +20,7 @@ module Spectator::Formatting elsif @pending > 0 Color.pending(self) else - Color.success(self) + Color.pass(self) end end diff --git a/src/spectator/formatting/successful_junit_test_case.cr b/src/spectator/formatting/successful_junit_test_case.cr index 108e966..803ea6e 100644 --- a/src/spectator/formatting/successful_junit_test_case.cr +++ b/src/spectator/formatting/successful_junit_test_case.cr @@ -5,7 +5,7 @@ module Spectator::Formatting private getter result # Creates the JUnit test case. - def initialize(@result : SuccessfulResult) + def initialize(@result : PassResult) end # Status string specific to the result type. diff --git a/src/spectator/formatting/tap_test_line.cr b/src/spectator/formatting/tap_test_line.cr index 8c0e823..089031b 100644 --- a/src/spectator/formatting/tap_test_line.cr +++ b/src/spectator/formatting/tap_test_line.cr @@ -17,7 +17,7 @@ module Spectator::Formatting # The text "ok" or "not ok" depending on the result. private def status - @result.is_a?(FailedResult) ? "not ok" : "ok" + @result.is_a?(FailResult) ? "not ok" : "ok" end # The example that was tested. diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 44bc321..2d19e85 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -91,13 +91,14 @@ module Spectator # Takes the *elapsed* time and a possible *error* from the test. # Returns a type of `Result`. private def translate(elapsed, error) : Result + example = Example.current # TODO: Remove this. case error when nil - PassResult.new(elapsed) + PassResult.new(example, elapsed) when ExpectationFailed - FailResult.new(elapsed, error) + FailResult.new(example, elapsed, error) else - ErrorResult.new(elapsed, error) + ErrorResult.new(example, elapsed, error) end end diff --git a/src/spectator/includes.cr b/src/spectator/includes.cr index 665b8e9..9f73ade 100644 --- a/src/spectator/includes.cr +++ b/src/spectator/includes.cr @@ -47,7 +47,6 @@ require "./pending_result" require "./profile" require "./report" require "./result" -require "./runner" require "./source" require "./source_example_filter" require "./spec" diff --git a/src/spectator/pass_result.cr b/src/spectator/pass_result.cr index 193e3c2..4094b5b 100644 --- a/src/spectator/pass_result.cr +++ b/src/spectator/pass_result.cr @@ -8,9 +8,19 @@ module Spectator visitor.pass end + # Calls the `pass` method on *visitor*. + def accept(visitor) + visitor.pass(yield self) + end + # One-word description of the result. def to_s(io) io << "pass" end + + # TODO + def to_json(builder) + builder.string("PASS") + end end end diff --git a/src/spectator/pending_result.cr b/src/spectator/pending_result.cr index 5781edc..3800a9c 100644 --- a/src/spectator/pending_result.cr +++ b/src/spectator/pending_result.cr @@ -7,7 +7,7 @@ module Spectator class PendingResult < Result # Creates the result. # *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 end @@ -16,9 +16,19 @@ module Spectator visitor.pending end + # Calls the `pending` method on the *visitor*. + def accept(visitor) + visitor.pending(yield self) + end + # One-word description of the result. def to_s(io) io << "pending" end + + # TODO + def to_json(builder) + builder.string("PENDING") + end end end diff --git a/src/spectator/profile.cr b/src/spectator/profile.cr index ff6f426..0067f7a 100644 --- a/src/spectator/profile.cr +++ b/src/spectator/profile.cr @@ -8,7 +8,7 @@ module Spectator # Creates the profiling information. # 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 # Number of results in the profile. @@ -33,7 +33,7 @@ module Spectator # Produces the profile from a report. def self.generate(report, size = 10) - results = report.compact_map(&.as?(FinishedResult)) + results = report.to_a sorted_results = results.sort_by(&.elapsed) slowest = sorted_results.last(size).reverse self.new(slowest, report.example_runtime) diff --git a/src/spectator/report.cr b/src/spectator/report.cr index 9ce1ebd..e295b5b 100644 --- a/src/spectator/report.cr +++ b/src/spectator/report.cr @@ -37,12 +37,12 @@ module Spectator def initialize(@results : Array(Result), @runtime, @remaining_count = 0, @fail_blank = false, @random_seed = nil) @results.each do |result| case result - when SuccessfulResult + when PassResult @successful_count += 1 - when ErroredResult + when ErrorResult @error_count += 1 @failed_count += 1 - when FailedResult + when FailResult @failed_count += 1 when PendingResult @pending_count += 1 @@ -58,7 +58,7 @@ module Spectator # The *results* are from running the examples in the test suite. # The runtime is calculated from the *results*. def initialize(results : Array(Result)) - runtime = results.each.compact_map(&.as?(FinishedResult)).sum(&.elapsed) + runtime = results.sum(&.elapsed) initialize(results, runtime) end @@ -92,19 +92,19 @@ module Spectator # Returns a set of results for all failed examples. def failures - @results.each.compact_map(&.as?(FailedResult)) + @results.each.compact_map(&.as?(FailResult)) end # Returns a set of results for all errored examples. def errors - @results.each.compact_map(&.as?(ErroredResult)) + @results.each.compact_map(&.as?(ErrorResult)) end # Length of time it took to run just example code. # This does not include hooks, # but it does include pre- and post-conditions. def example_runtime - @results.each.compact_map(&.as?(FinishedResult)).sum(&.elapsed) + @results.sum(&.elapsed) end # Length of time spent in framework processes and hooks. diff --git a/src/spectator/result.cr b/src/spectator/result.cr index 056c979..441be07 100644 --- a/src/spectator/result.cr +++ b/src/spectator/result.cr @@ -2,15 +2,19 @@ module Spectator # Base class that represents the outcome of running an example. # Sub-classes contain additional information specific to the type of result. abstract class Result + # Example that generated the result. + # TODO: Remove this. + getter example : Example + # Length of time it took to run the example. getter elapsed : Time::Span # The assertions checked in the example. - # getter assertions : Enumerable(Assertion) # TODO: Implement Assertion type. + getter expectations : Enumerable(Expectation) # Creates the result. # *elapsed* is the length of time it took to run the example. - def initialize(@elapsed) + def initialize(@example, @elapsed, @expectations = [] of Expectation) end # Calls the corresponding method for the type of result. diff --git a/src/spectator/runner.cr b/src/spectator/runner.cr deleted file mode 100644 index d5c50f9..0000000 --- a/src/spectator/runner.cr +++ /dev/null @@ -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 diff --git a/src/spectator/spec.cr b/src/spectator/spec.cr index 1f9064c..a034332 100644 --- a/src/spectator/spec.cr +++ b/src/spectator/spec.cr @@ -1,7 +1,7 @@ require "./config" require "./example" require "./example_group" -require "./example_iterator" +require "./test_suite" module Spectator # Contains examples to be tested. @@ -9,15 +9,9 @@ module Spectator def initialize(@root : ExampleGroup, @config : Config) end - def run - Runner.new(examples).run - end - - # 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) + def run(filter : ExampleFilter) + suite = TestSuite.new(@root, filter) + Runner.new(suite, @config).run end end end diff --git a/src/spectator/spec/runner.cr b/src/spectator/spec/runner.cr index 3447578..99ce35b 100644 --- a/src/spectator/spec/runner.cr +++ b/src/spectator/spec/runner.cr @@ -3,11 +3,76 @@ require "../example" module Spectator class Spec private struct Runner - def initialize(@examples : Array(Example)) + def initialize(@suite : TestSuite, @config : Config) end - def run - @examples.each(&.run) + # 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?(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 From bda554739c686414bc664a9df08eda3b79783b61 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 20:07:36 -0700 Subject: [PATCH 161/399] Re-add JSON output for some types --- src/spectator/error_result.cr | 25 ++++++++++++++++++++++--- src/spectator/example.cr | 7 ++++--- src/spectator/expectation.cr | 23 +++++++++++++++++++++++ src/spectator/fail_result.cr | 7 ++++--- src/spectator/pass_result.cr | 5 ----- src/spectator/pending_result.cr | 5 ----- src/spectator/result.cr | 16 ++++++++++++++++ 7 files changed, 69 insertions(+), 19 deletions(-) diff --git a/src/spectator/error_result.cr b/src/spectator/error_result.cr index 00cdeae..1f19062 100644 --- a/src/spectator/error_result.cr +++ b/src/spectator/error_result.cr @@ -20,9 +20,28 @@ module Spectator io << "error" end - # TODO - def to_json(builder) - builder.string("ERROR") + # 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 end end end diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 0efede2..3fc713b 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -149,9 +149,10 @@ module Spectator io << result end - # TODO - def to_json(builder) - builder.string("EXAMPLE") + # Creates the JSON representation of the example, + # which is just its name. + def to_json(json : ::JSON::Builder) + json.string(to_s) end # Wraps an example to behave like a `Proc`. diff --git a/src/spectator/expectation.cr b/src/spectator/expectation.cr index 29af037..6b38771 100644 --- a/src/spectator/expectation.cr +++ b/src/spectator/expectation.cr @@ -52,6 +52,29 @@ module Spectator def initialize(@match_data : Matchers::MatchData, @source : Source? = nil) end + # Creates the JSON representation of the expectation. + def to_json(json : ::JSON::Builder) + json.object do + json.field("source") { @source.to_json(json) } + json.field("satisfied", satisfied?) + if (failed = @match_data.as?(Matchers::FailedMatchData)) + failed_to_json(failed, json) + end + end + end + + # Adds failure information to a JSON structure. + private def failed_to_json(failed : Matchers::FailedMatchData, json : ::JSON::Builder) + json.field("failure", failed.failure_message) + json.field("values") do + json.object do + failed.values.each do |pair| + json.field(pair.first, pair.last) + end + end + end + end + # Stores part of an expectation. # This covers the actual value (or block) being inspected and its source. # This is the type returned by an `expect` block in the DSL. diff --git a/src/spectator/fail_result.cr b/src/spectator/fail_result.cr index ead0e79..dfe8937 100644 --- a/src/spectator/fail_result.cr +++ b/src/spectator/fail_result.cr @@ -30,9 +30,10 @@ module Spectator io << "fail" end - # TODO - def to_json(builder) - builder.string("FAIL") + # Adds all of the JSON fields for finished results and failed results. + private def add_json_fields(json : ::JSON::Builder) + super + json.field("error", error.message) end end end diff --git a/src/spectator/pass_result.cr b/src/spectator/pass_result.cr index 4094b5b..e059057 100644 --- a/src/spectator/pass_result.cr +++ b/src/spectator/pass_result.cr @@ -17,10 +17,5 @@ module Spectator def to_s(io) io << "pass" end - - # TODO - def to_json(builder) - builder.string("PASS") - end end end diff --git a/src/spectator/pending_result.cr b/src/spectator/pending_result.cr index 3800a9c..4ff64e6 100644 --- a/src/spectator/pending_result.cr +++ b/src/spectator/pending_result.cr @@ -25,10 +25,5 @@ module Spectator def to_s(io) io << "pending" end - - # TODO - def to_json(builder) - builder.string("PENDING") - end end end diff --git a/src/spectator/result.cr b/src/spectator/result.cr index 441be07..d85bf39 100644 --- a/src/spectator/result.cr +++ b/src/spectator/result.cr @@ -20,5 +20,21 @@ module Spectator # Calls the corresponding method for the type of result. # This is the visitor design pattern. abstract def accept(visitor) + + # Creates a JSON object from the result information. + def to_json(json : ::JSON::Builder) + json.object do + add_json_fields(json) + end + end + + # Adds the common fields for a result to a JSON builder. + private def add_json_fields(json : ::JSON::Builder) + json.field("name", example) + json.field("location", example.source) + json.field("result", to_s) + json.field("time", elapsed.total_seconds) + json.field("expectations", expectations) + end end end From 9743f37e5c2915bd76285516fc0ea0a7165ed336 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 20:07:45 -0700 Subject: [PATCH 162/399] Capture reported expectations --- src/spectator/harness.cr | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 2d19e85..e0db3c7 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -53,6 +53,7 @@ module Spectator end @deferred = Deque(->).new + @expectations = [] of Expectation # Runs test code and produces a result based on the outcome. # The test code should be called from within the block given to this method. @@ -64,6 +65,7 @@ module Spectator def report(expectation : Expectation) : Nil Log.debug { "Reporting expectation #{expectation}" } + @expectations << expectation raise ExpectationFailed.new(expectation) if expectation.failed? end @@ -94,11 +96,11 @@ module Spectator example = Example.current # TODO: Remove this. case error when nil - PassResult.new(example, elapsed) + PassResult.new(example, elapsed, @expectations) when ExpectationFailed - FailResult.new(example, elapsed, error) + FailResult.new(example, elapsed, error, @expectations) else - ErrorResult.new(example, elapsed, error) + ErrorResult.new(example, elapsed, error, @expectations) end end From 228696c8b0022d7b589c51d71abde82df575c998 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 20:11:09 -0700 Subject: [PATCH 163/399] Fix exit code when tests fail --- src/spectator.cr | 1 - 1 file changed, 1 deletion(-) diff --git a/src/spectator.cr b/src/spectator.cr index dd53ce3..3dff022 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -68,7 +68,6 @@ module Spectator DSL::Builder.config = config spec = DSL::Builder.build spec.run(config.example_filter) - true rescue ex # Catch all unhandled exceptions here. # Examples are already wrapped, so any exceptions they throw are caught. From 2c33e96dd4edbec1179ef35ad1f05a8d490a6b5f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 20:14:25 -0700 Subject: [PATCH 164/399] Fix parsing passing spec --- spec/helpers/result.cr | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/helpers/result.cr b/spec/helpers/result.cr index aeabf3d..1f99b2e 100644 --- a/spec/helpers/result.cr +++ b/spec/helpers/result.cr @@ -57,10 +57,10 @@ module Spectator::SpecHelpers # Converts a result string, such as "fail" to an enum value. private def self.parse_outcome_string(string) case string - when /success/i then Outcome::Success - when /fail/i then Outcome::Failure - when /error/i then Outcome::Error - else Outcome::Unknown + when /pass/i then Outcome::Success + when /fail/i then Outcome::Failure + when /error/i then Outcome::Error + else Outcome::Unknown end end end From 62d478f7c204a761712718a9c222197f2b3857af Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 20:20:30 -0700 Subject: [PATCH 165/399] Prevent overly verbose output of internals --- src/spectator/context.cr | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/spectator/context.cr b/src/spectator/context.cr index c0a12db..2860ae3 100644 --- a/src/spectator/context.cr +++ b/src/spectator/context.cr @@ -4,6 +4,15 @@ # This type is intentionally outside the `Spectator` module. # The reason for this is to prevent name collision when using the DSL to define a spec. abstract class SpectatorContext + def to_s(io) + io << "Context" + end + + def inspect(io) + io << "Context<" + io << self.class + io << '>' + end end module Spectator From 948e29a8b7590c35bca66a070eada8b662abebe5 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 20:27:36 -0700 Subject: [PATCH 166/399] Fix short-hand should syntax --- src/spectator/should.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/should.cr b/src/spectator/should.cr index 3be851f..7ebcf84 100644 --- a/src/spectator/should.cr +++ b/src/spectator/should.cr @@ -65,7 +65,7 @@ struct Proc(*T, R) end end -module Spectator::DSL::Assertions +module Spectator::DSL::Expectations macro should(matcher) expect(subject).to({{matcher}}) end From 078058ad058cc73a4da948c14396f5b873e445f8 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 20:38:09 -0700 Subject: [PATCH 167/399] Fix various issues with subjects All tests in crystal spec pass! --- src/spectator/dsl/groups.cr | 13 +++++++------ src/spectator/dsl/values.cr | 20 ++++++++++++++------ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 0bb8bfd..3dc353b 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -1,6 +1,7 @@ require "../source" require "./builder" require "./tags" +require "./values" module Spectator::DSL # DSL methods and macros for creating example groups. @@ -89,13 +90,13 @@ module Spectator::DSL {{described_type}} end - private def _spectator_implicit_subject - {% if described_type < Reference || described_type < Value %} - described_class.new - {% else %} + {% if described_type < Reference || described_type < Value %} + subject { described_class.new } + {% else %} + private def subject described_class - {% end %} - end + end + {% end %} {% else %} private def _spectator_implicit_subject {{what}} diff --git a/src/spectator/dsl/values.cr b/src/spectator/dsl/values.cr index 6f0435d..b079f2e 100644 --- a/src/spectator/dsl/values.cr +++ b/src/spectator/dsl/values.cr @@ -53,11 +53,15 @@ module Spectator::DSL # The block is evaluated only the first time the subject is referenced # and the return value is saved for subsequent calls. macro subject(name, &block) - subject {{block}} + {% raise "Block required for 'subject'" unless block %} + {% raise "Cannot use 'subject' inside of a test block" if @def %} + {% raise "Block argument count for 'subject' must be 0..1" if block.args.size > 1 %} + + let({{name.id}}) {{block}} {% if name.id != :subject.id %} - def {{name.id}} - subject + def subject + {{name.id}} end {% end %} end @@ -80,11 +84,15 @@ module Spectator::DSL # The block is evaluated once before the example runs # and the return value is saved for subsequent calls. macro subject!(name, &block) - subject! {{block}} + {% raise "Block required for 'subject!'" unless block %} + {% raise "Cannot use 'subject!' inside of a test block" if @def %} + {% raise "Block argument count for 'subject!' must be 0..1" if block.args.size > 1 %} + + let!({{name.id}}) {{block}} {% if name.id != :subject.id %} - def {{name.id}} - subject + def subject + {{name.id}} end {% end %} end From 82e13f5434504fc339ee59efd3c736662baf6df3 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 20:53:09 -0700 Subject: [PATCH 168/399] Sloppily handle deferred operations --- src/spectator/harness.cr | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index e0db3c7..aa09a81 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -58,9 +58,9 @@ module Spectator # Runs test code and produces a result based on the outcome. # The test code should be called from within the block given to this method. def run : Result - outcome = capture { yield } - run_deferred # TODO: Handle errors in deferred blocks. - translate(*outcome) + elapsed, error = capture { yield } + elapsed2, error2 = run_deferred + translate(elapsed + elapsed2, error || error2) end def report(expectation : Expectation) : Nil @@ -105,9 +105,15 @@ module Spectator end # Runs all deferred blocks. - private def run_deferred : Nil - @deferred.each(&.call) + private def run_deferred + error = nil.as(Exception?) + elapsed = Time.measure do + @deferred.each(&.call) + rescue ex + error = ex + end @deferred.clear + {elapsed, error} end end end From e44505d4c18b0f101b9b95667960856222275c98 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 21:00:01 -0700 Subject: [PATCH 169/399] Set nameless example to matcher description --- src/spectator/harness.cr | 1 + 1 file changed, 1 insertion(+) diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index aa09a81..784df86 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -66,6 +66,7 @@ module Spectator def report(expectation : Expectation) : Nil Log.debug { "Reporting expectation #{expectation}" } @expectations << expectation + Example.current.name = expectation.description unless Example.current.name? raise ExpectationFailed.new(expectation) if expectation.failed? end From 292dfcbe29720852ab4c89c45d1a3c740deb5ce8 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 21:00:28 -0700 Subject: [PATCH 170/399] Ignore nameless groups in document hierarchy --- src/spectator/formatting/document_formatter.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/formatting/document_formatter.cr b/src/spectator/formatting/document_formatter.cr index 487bf53..16efdf8 100644 --- a/src/spectator/formatting/document_formatter.cr +++ b/src/spectator/formatting/document_formatter.cr @@ -37,7 +37,7 @@ module Spectator::Formatting hierarchy = [] of ExampleGroup group = example.group while group.is_a?(ExampleGroup) - hierarchy << group + hierarchy << group if group.name? group = group.group? end hierarchy.reverse From 0332b6eb3ba2a30c604fdb3f565fc697bbe8404a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 21:07:42 -0700 Subject: [PATCH 171/399] Log when deferred operations run --- src/spectator/harness.cr | 1 + 1 file changed, 1 insertion(+) diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 784df86..59e8975 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -107,6 +107,7 @@ module Spectator # Runs all deferred blocks. private def run_deferred + Log.debug { "Running deferred operations" } error = nil.as(Exception?) elapsed = Time.measure do @deferred.each(&.call) From 0f44403053e80580b3e7c450ac103b79ee56fbcd Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 21:11:51 -0700 Subject: [PATCH 172/399] Fix namespaces preventing structs from being mocked --- src/spectator/mocks/registry.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/mocks/registry.cr b/src/spectator/mocks/registry.cr index ab09bd8..545a70c 100644 --- a/src/spectator/mocks/registry.cr +++ b/src/spectator/mocks/registry.cr @@ -79,11 +79,11 @@ module Spectator::Mocks end end - private def unique_key(reference : Reference) + private def unique_key(reference : ::Reference) {reference.class.name, reference.object_id} end - private def unique_key(value : Value) + private def unique_key(value : ::Value) {value.class.name, value.hash} end end From ff49c674c653612888182af4e4dcbda220983d4f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 21:15:20 -0700 Subject: [PATCH 173/399] Fix around_each hook ordering when at the same level --- src/spectator/example_group.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index c939460..c5f00c6 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -138,7 +138,7 @@ module Spectator # Wrap each hook with the next. outer = procsy - @around_hooks.each do |hook| + @around_hooks.reverse_each do |hook| outer = hook.wrap(outer) end From 3ae83f8b3d8044b1ac37a23e73b8521d60de2188 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 21:29:11 -0700 Subject: [PATCH 174/399] Note about removing one-liner braceless syntax --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcad249..b5a9446 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Better error messages and detection when DSL methods are used when they shouldn't (i.e. `describe` inside `it`). - Other minor internal improvements and cleanup. +### Removed +- Removed one-liner it syntax without braces (block). + ## [0.9.31] - 2021-01-08 ### Fixed - Fix misaligned line numbers when referencing examples and groups. From e275711f2b9a67bc22639e848276360fbd46bb81 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 23:48:59 -0700 Subject: [PATCH 175/399] Formatting --- src/spectator/dsl/values.cr | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/spectator/dsl/values.cr b/src/spectator/dsl/values.cr index b079f2e..a0d448b 100644 --- a/src/spectator/dsl/values.cr +++ b/src/spectator/dsl/values.cr @@ -15,7 +15,9 @@ module Spectator::DSL @%value = ::Spectator::LazyWrapper.new def {{name.id}} - {% if block.args.size > 0 %}{{block.args.first}} = ::Spectator::Example.current{% end %} + {% if block.args.size > 0 %} + {{block.args.first}} = ::Spectator::Example.current + {% end %} @%value.get do {{block.body}} end From a20b7cad804d6212464f93ae64bf8942075b9a37 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 30 Jan 2021 23:49:20 -0700 Subject: [PATCH 176/399] Workaround for case where wrapper might store a type --- src/spectator/lazy_wrapper.cr | 2 +- src/spectator/wrapper.cr | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/spectator/lazy_wrapper.cr b/src/spectator/lazy_wrapper.cr index ea177f9..3939c95 100644 --- a/src/spectator/lazy_wrapper.cr +++ b/src/spectator/lazy_wrapper.cr @@ -13,7 +13,7 @@ module Spectator # Subsequent calls will return the same value and not yield. def get(&block : -> T) : T forall T wrapper = @lazy.get { Wrapper.new(yield) } - wrapper.get(T) + wrapper.get { yield } end end end diff --git a/src/spectator/wrapper.cr b/src/spectator/wrapper.cr index efee1b6..b5edeb3 100644 --- a/src/spectator/wrapper.cr +++ b/src/spectator/wrapper.cr @@ -17,6 +17,16 @@ module Spectator value.get end + # Retrieves the previously wrapped value. + # Alternate form of `#get` that accepts a block. + # The block must return the same type as the wrapped value, otherwise an error will be raised. + # This method gets around the issue where the value might be a type (i.e. `Int32.class`). + # The block will never be executed, it is only used for type information. + def get(& : -> T) : T forall T + value = @value.as(Value(T)) + value.get + end + # Base type that generic types inherit from. # This provides a common base type, # since Crystal doesn't support storing an `Object` (yet). From 0363c43dfff45cbe60a0dfe83218cae742459b0e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 31 Jan 2021 00:38:17 -0700 Subject: [PATCH 177/399] Fix described_class Must be a macro, otherwise the return type becomes a union of all nested described_class methods, which can cause weird errors. --- src/spectator/dsl/groups.cr | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 3dc353b..001f4fb 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -86,17 +86,17 @@ module Spectator::DSL what.is_a?(TypeNode) || what.is_a?(Union)) && (described_type = what.resolve?).is_a?(TypeNode) %} - private def described_class - {{described_type}} + private macro described_class + {{what}} end - {% if described_type < Reference || described_type < Value %} - subject { described_class.new } - {% else %} - private def subject + subject do + {% if described_type.class? || described_type.struct? %} + described_class.new + {% else %} described_class - end - {% end %} + {% end %} + end {% else %} private def _spectator_implicit_subject {{what}} From bbd9acda33c2be7517074b35a3f7dac479105ed2 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 9 Feb 2021 18:31:41 -0700 Subject: [PATCH 178/399] Capture test results from JUnit output --- .gitlab-ci.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index caf6bad..14d3678 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,7 +14,13 @@ before_script: spec: script: - shards - - crystal spec --error-on-warnings + - crystal spec --error-on-warnings --junit_output=. + artifacts: + when: always + paths: + - output.xml + reports: + junit: output.xml format: script: @@ -31,8 +37,14 @@ nightly: allow_failure: true script: - shards --ignore-crystal-version - - crystal spec --error-on-warnings + - crystal spec --error-on-warnings --junit_output=. - crystal tool format --check + artifacts: + when: always + paths: + - output.xml + reports: + junit: output.xml pages: stage: deploy From d06aaa9d14676b22d7185dbc78dff24b939e7e38 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 9 Feb 2021 19:08:58 -0700 Subject: [PATCH 179/399] Only run spec that was changed --- .guardian.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.guardian.yml b/.guardian.yml index ad67b38..f283d96 100644 --- a/.guardian.yml +++ b/.guardian.yml @@ -1,5 +1,11 @@ -files: ./**/*.cr +files: ./src/**/*.cr run: time crystal spec --error-trace --- +files: ./src/**/*.cr +run: bin/ameba %file% +--- +files: ./spec/**/*.cr +run: time crystal spec --error-trace %file% +--- files: ./shard.yml run: shards From 4e3cb5d25ffe48c1381107eae6568d3d1951fe84 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 9 Feb 2021 19:09:58 -0700 Subject: [PATCH 180/399] Ignore JUnit output --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index e29dae7..c4166ba 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ # Libraries don't need dependency lock # Dependencies will be locked in application that uses them /shard.lock + +# Ignore JUnit output +output.xml From a20f2d4f983a9c78c41d3aac258b624136462e62 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 9 Feb 2021 19:10:11 -0700 Subject: [PATCH 181/399] Test and improve "Anything" --- spec/spectator/anything_spec.cr | 49 +++++++++++++++++++++++++++++++++ src/spectator/anything.cr | 21 ++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 spec/spectator/anything_spec.cr diff --git a/spec/spectator/anything_spec.cr b/spec/spectator/anything_spec.cr new file mode 100644 index 0000000..11672a5 --- /dev/null +++ b/spec/spectator/anything_spec.cr @@ -0,0 +1,49 @@ +require "../spec_helper" + +Spectator.describe Spectator::Anything do + it "equals everything" do + expect(true).to eq(subject) + expect(false).to eq(subject) + expect(nil).to eq(subject) + expect(42).to eq(subject) + expect(42.as(Int32 | String)).to eq(subject) + expect(["foo", "bar"]).to eq(subject) + end + + it "matches everything" do + expect(true).to match(subject) + expect(false).to match(subject) + expect(nil).to match(subject) + expect(42).to match(subject) + expect(42.as(Int32 | String)).to match(subject) + expect(["foo", "bar"]).to match(subject) + end + + context "nested in a container" do + it "equals everything" do + expect(["foo", "bar"]).to eq(["foo", subject]) + expect({"foo", "bar"}).to eq({"foo", subject}) + expect({foo: "bar"}).to eq({foo: subject}) + expect({"foo" => "bar"}).to eq({"foo" => subject}) + end + + it "matches everything" do + expect(["foo", "bar"]).to match(["foo", subject]) + expect({"foo", "bar"}).to match({"foo", subject}) + expect({foo: "bar"}).to match({foo: subject}) + expect({"foo" => "bar"}).to match({"foo" => subject}) + end + end + + describe "#to_s" do + subject { super.to_s } + + it { is_expected.to contain("anything") } + end + + describe "#inspect" do + subject { super.inspect } + + it { is_expected.to contain("anything") } + end +end diff --git a/src/spectator/anything.cr b/src/spectator/anything.cr index 511a024..6dafc60 100644 --- a/src/spectator/anything.cr +++ b/src/spectator/anything.cr @@ -1,15 +1,36 @@ module Spectator + # Type dedicated to matching everything. + # This is intended to be used as a value to compare against when the value doesn't matter. + # Can be used like so: + # ``` + # anything = Spectator::Anything.new + # array = ["foo", anything] + # expect(["foo", "bar"]).to eq(array) + # ``` struct Anything + # Always returns true. def ==(other) true end + # Always returns true. def ===(other) true end + # Always returns true. def =~(other) true end + + # Displays "anything". + def to_s(io) + io << "anything" + end + + # Displays "". + def inspect(io) + io << "" + end end end From b2aaac9a4698f5f8232b3371b432626c53ec8802 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 9 Feb 2021 19:08:58 -0700 Subject: [PATCH 182/399] Only run spec that was changed --- .guardian.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.guardian.yml b/.guardian.yml index ad67b38..f283d96 100644 --- a/.guardian.yml +++ b/.guardian.yml @@ -1,5 +1,11 @@ -files: ./**/*.cr +files: ./src/**/*.cr run: time crystal spec --error-trace --- +files: ./src/**/*.cr +run: bin/ameba %file% +--- +files: ./spec/**/*.cr +run: time crystal spec --error-trace %file% +--- files: ./shard.yml run: shards From 6f81011ba1fcb812fe0117405799c13b023426b2 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 9 Feb 2021 19:09:58 -0700 Subject: [PATCH 183/399] Ignore JUnit output --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index e29dae7..c4166ba 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ # Libraries don't need dependency lock # Dependencies will be locked in application that uses them /shard.lock + +# Ignore JUnit output +output.xml From ae26377b3d18fe5c09c169260bbd34e75c800fa6 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 9 Feb 2021 19:10:11 -0700 Subject: [PATCH 184/399] Test and improve "Anything" --- spec/spectator/anything_spec.cr | 49 +++++++++++++++++++++++++++++++++ src/spectator/anything.cr | 21 ++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 spec/spectator/anything_spec.cr diff --git a/spec/spectator/anything_spec.cr b/spec/spectator/anything_spec.cr new file mode 100644 index 0000000..11672a5 --- /dev/null +++ b/spec/spectator/anything_spec.cr @@ -0,0 +1,49 @@ +require "../spec_helper" + +Spectator.describe Spectator::Anything do + it "equals everything" do + expect(true).to eq(subject) + expect(false).to eq(subject) + expect(nil).to eq(subject) + expect(42).to eq(subject) + expect(42.as(Int32 | String)).to eq(subject) + expect(["foo", "bar"]).to eq(subject) + end + + it "matches everything" do + expect(true).to match(subject) + expect(false).to match(subject) + expect(nil).to match(subject) + expect(42).to match(subject) + expect(42.as(Int32 | String)).to match(subject) + expect(["foo", "bar"]).to match(subject) + end + + context "nested in a container" do + it "equals everything" do + expect(["foo", "bar"]).to eq(["foo", subject]) + expect({"foo", "bar"}).to eq({"foo", subject}) + expect({foo: "bar"}).to eq({foo: subject}) + expect({"foo" => "bar"}).to eq({"foo" => subject}) + end + + it "matches everything" do + expect(["foo", "bar"]).to match(["foo", subject]) + expect({"foo", "bar"}).to match({"foo", subject}) + expect({foo: "bar"}).to match({foo: subject}) + expect({"foo" => "bar"}).to match({"foo" => subject}) + end + end + + describe "#to_s" do + subject { super.to_s } + + it { is_expected.to contain("anything") } + end + + describe "#inspect" do + subject { super.inspect } + + it { is_expected.to contain("anything") } + end +end diff --git a/src/spectator/anything.cr b/src/spectator/anything.cr index 511a024..6dafc60 100644 --- a/src/spectator/anything.cr +++ b/src/spectator/anything.cr @@ -1,15 +1,36 @@ module Spectator + # Type dedicated to matching everything. + # This is intended to be used as a value to compare against when the value doesn't matter. + # Can be used like so: + # ``` + # anything = Spectator::Anything.new + # array = ["foo", anything] + # expect(["foo", "bar"]).to eq(array) + # ``` struct Anything + # Always returns true. def ==(other) true end + # Always returns true. def ===(other) true end + # Always returns true. def =~(other) true end + + # Displays "anything". + def to_s(io) + io << "anything" + end + + # Displays "". + def inspect(io) + io << "" + end end end From 7a5f7adfc297fc3fc311025fda7b12a5f6d8aaf1 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 9 Feb 2021 21:18:20 -0700 Subject: [PATCH 185/399] Change Anything to only use case equality --- spec/spectator/anything_spec.cr | 25 ------------------------- src/spectator/anything.cr | 13 +------------ 2 files changed, 1 insertion(+), 37 deletions(-) diff --git a/spec/spectator/anything_spec.cr b/spec/spectator/anything_spec.cr index 11672a5..3140f7e 100644 --- a/spec/spectator/anything_spec.cr +++ b/spec/spectator/anything_spec.cr @@ -1,15 +1,6 @@ require "../spec_helper" Spectator.describe Spectator::Anything do - it "equals everything" do - expect(true).to eq(subject) - expect(false).to eq(subject) - expect(nil).to eq(subject) - expect(42).to eq(subject) - expect(42.as(Int32 | String)).to eq(subject) - expect(["foo", "bar"]).to eq(subject) - end - it "matches everything" do expect(true).to match(subject) expect(false).to match(subject) @@ -19,22 +10,6 @@ Spectator.describe Spectator::Anything do expect(["foo", "bar"]).to match(subject) end - context "nested in a container" do - it "equals everything" do - expect(["foo", "bar"]).to eq(["foo", subject]) - expect({"foo", "bar"}).to eq({"foo", subject}) - expect({foo: "bar"}).to eq({foo: subject}) - expect({"foo" => "bar"}).to eq({"foo" => subject}) - end - - it "matches everything" do - expect(["foo", "bar"]).to match(["foo", subject]) - expect({"foo", "bar"}).to match({"foo", subject}) - expect({foo: "bar"}).to match({foo: subject}) - expect({"foo" => "bar"}).to match({"foo" => subject}) - end - end - describe "#to_s" do subject { super.to_s } diff --git a/src/spectator/anything.cr b/src/spectator/anything.cr index 6dafc60..e4d7b34 100644 --- a/src/spectator/anything.cr +++ b/src/spectator/anything.cr @@ -4,25 +4,14 @@ module Spectator # Can be used like so: # ``` # anything = Spectator::Anything.new - # array = ["foo", anything] - # expect(["foo", "bar"]).to eq(array) + # expect("foo").to match(anything) # ``` struct Anything - # Always returns true. - def ==(other) - true - end - # Always returns true. def ===(other) true end - # Always returns true. - def =~(other) - true - end - # Displays "anything". def to_s(io) io << "anything" From 8232da116788ade3c06e83f1b90a9bf7b12e2433 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 9 Feb 2021 22:40:15 -0700 Subject: [PATCH 186/399] Mark DSL generated methods as private --- src/spectator/dsl/examples.cr | 2 +- src/spectator/dsl/hooks.cr | 4 ++-- src/spectator/dsl/tags.cr | 2 +- src/spectator/dsl/values.cr | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index b466b02..af25638 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -41,7 +41,7 @@ module Spectator::DSL _spectator_tags(%tags, :tags, {{tags.splat(",")}} {{metadata.double_splat}}) _spectator_tags(\%tags, %tags, \{{tags.splat(",")}} \{{metadata.double_splat}}) - def \%test(\{{block.args.splat}}) : Nil + private def \%test(\{{block.args.splat}}) : Nil \{{block.body}} end diff --git a/src/spectator/dsl/hooks.cr b/src/spectator/dsl/hooks.cr index e1194ba..75dd06c 100644 --- a/src/spectator/dsl/hooks.cr +++ b/src/spectator/dsl/hooks.cr @@ -11,7 +11,7 @@ module Spectator::DSL \{% raise "Missing block for '{{type.id}}' hook" unless block %} \{% raise "Cannot use '{{type.id}}' inside of a test block" if @def %} - def self.\%hook : Nil + private def self.\%hook : Nil \{{block.body}} end @@ -29,7 +29,7 @@ module Spectator::DSL \{% raise "Block argument count '{{type.id}}' hook must be 0..1" if block.args.size > 1 %} \{% raise "Cannot use '{{type.id}}' inside of a test block" if @def %} - def \%hook(\{{block.args.splat}}) : Nil + private def \%hook(\{{block.args.splat}}) : Nil \{{block.body}} end diff --git a/src/spectator/dsl/tags.cr b/src/spectator/dsl/tags.cr index a73c396..f2dc268 100644 --- a/src/spectator/dsl/tags.cr +++ b/src/spectator/dsl/tags.cr @@ -4,7 +4,7 @@ module Spectator::DSL # returned by *source* with *tags* and *metadata*. # Any falsey items from *metadata* are removed. private macro _spectator_tags(name, source, *tags, **metadata) - def self.{{name.id}} + private def self.{{name.id}} %tags = {{source.id}} {% unless tags.empty? %} %tags.concat({ {{tags.map(&.id.symbolize).splat}} }) diff --git a/src/spectator/dsl/values.cr b/src/spectator/dsl/values.cr index a0d448b..fdff02f 100644 --- a/src/spectator/dsl/values.cr +++ b/src/spectator/dsl/values.cr @@ -14,7 +14,7 @@ module Spectator::DSL @%value = ::Spectator::LazyWrapper.new - def {{name.id}} + private def {{name.id}} {% if block.args.size > 0 %} {{block.args.first}} = ::Spectator::Example.current {% end %} @@ -62,7 +62,7 @@ module Spectator::DSL let({{name.id}}) {{block}} {% if name.id != :subject.id %} - def subject + private def subject {{name.id}} end {% end %} @@ -93,7 +93,7 @@ module Spectator::DSL let!({{name.id}}) {{block}} {% if name.id != :subject.id %} - def subject + private def subject {{name.id}} end {% end %} From 8b5fd099bb7463c2f919ad8b69190ae4ea7f4429 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 9 Feb 2021 22:50:09 -0700 Subject: [PATCH 187/399] More docs --- src/spectator/lazy_wrapper.cr | 19 ++++++++++++++++++- src/spectator/wrapper.cr | 12 ++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/spectator/lazy_wrapper.cr b/src/spectator/lazy_wrapper.cr index 3939c95..9bc7125 100644 --- a/src/spectator/lazy_wrapper.cr +++ b/src/spectator/lazy_wrapper.cr @@ -4,6 +4,23 @@ require "./wrapper" module Spectator # Lazily stores a value of any type. # Combines `Lazy` and `Wrapper`. + # + # Contains no value until the first call to `#get` is made. + # Any type can be stored in this wrapper. + # However, the type must always be known when retrieving it via `#get`. + # The type is inferred from the block, and all blocks must return the same type. + # Because of this, it is recommended to only have `#get` called in one location. + # + # This type is expected to be used like so: + # ``` + # @wrapper : LazyWrapper + # + # # ... + # + # def lazy_load + # @wrapper.get { some_expensive_operation } + # end + # ``` struct LazyWrapper @lazy = Lazy(Wrapper).new @@ -11,7 +28,7 @@ module Spectator # On the first invocation of this method, it will yield. # The block should return the value to store. # Subsequent calls will return the same value and not yield. - def get(&block : -> T) : T forall T + def get(& : -> T) : T forall T wrapper = @lazy.get { Wrapper.new(yield) } wrapper.get { yield } end diff --git a/src/spectator/wrapper.cr b/src/spectator/wrapper.cr index b5edeb3..21bd0ec 100644 --- a/src/spectator/wrapper.cr +++ b/src/spectator/wrapper.cr @@ -2,6 +2,12 @@ module Spectator # Typeless wrapper for a value. # Stores any value or reference type. # However, the type must be known when retrieving the value. + # + # This type is expected to be used like so: + # ``` + # wrapper = Wrapper.new("wrapped") + # value = wrapper.get(String) + # ``` struct Wrapper @value : TypelessValue @@ -22,6 +28,12 @@ module Spectator # The block must return the same type as the wrapped value, otherwise an error will be raised. # This method gets around the issue where the value might be a type (i.e. `Int32.class`). # The block will never be executed, it is only used for type information. + # + # ``` + # wrapper = Wrapper.new(Int32) + # # type = wrapper.get(Int32.class) # Does not work! + # type = wrapper.get { Int32 } # Returns Int32 + # ``` def get(& : -> T) : T forall T value = @value.as(Value(T)) value.get From 35946dc99364959e2aa9fb9d714d8a6238284dba Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 9 Feb 2021 22:50:21 -0700 Subject: [PATCH 188/399] Test value types --- spec/spectator/block_spec.cr | 34 ++++++++++++++++++++++++++++ spec/spectator/lazy_wrapper_spec.cr | 28 +++++++++++++++++++++++ spec/spectator/value_spec.cr | 35 +++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 spec/spectator/block_spec.cr create mode 100644 spec/spectator/lazy_wrapper_spec.cr create mode 100644 spec/spectator/value_spec.cr diff --git a/spec/spectator/block_spec.cr b/spec/spectator/block_spec.cr new file mode 100644 index 0000000..a459b02 --- /dev/null +++ b/spec/spectator/block_spec.cr @@ -0,0 +1,34 @@ +require "../spec_helper" + +Spectator.describe Spectator::Block do + describe "#value" do + it "calls the block" do + called = false + block = described_class.new { called = true } + expect { block.value }.to change { called }.to(true) + end + + it "can be called multiple times (doesn't cache the value)" do + count = 0 + block = described_class.new { count += 1 } + block.value # Call once, count should be 1. + expect { block.value }.to change { count }.from(1).to(2) + end + end + + describe "#to_s" do + let(block) do + described_class.new("Test Label") { 42 } + end + + subject { block.to_s } + + it "contains the label" do + is_expected.to contain("Test Label") + end + + it "contains the value" do + is_expected.to contain("42") + end + end +end diff --git a/spec/spectator/lazy_wrapper_spec.cr b/spec/spectator/lazy_wrapper_spec.cr new file mode 100644 index 0000000..94782b4 --- /dev/null +++ b/spec/spectator/lazy_wrapper_spec.cr @@ -0,0 +1,28 @@ +require "../spec_helper" + +Spectator.describe Spectator::LazyWrapper do + it "returns the value of the block" do + expect { subject.get { 42 } }.to eq(42) + end + + it "caches the value" do + wrapper = described_class.new + count = 0 + expect { wrapper.get { count += 1 } }.to change { count }.from(0).to(1) + expect { wrapper.get { count += 1 } }.to_not change { count } + end + + # This type of nesting is used when `super` is called in a subject block. + # ``` + # subject { super.to_s } + # ``` + it "works with nested wrappers" do + outer = described_class.new + inner = described_class.new + value = outer.get do + inner.get { 42 }.to_s + end + expect(value).to eq("42") + expect(value).to be_a(String) + end +end diff --git a/spec/spectator/value_spec.cr b/spec/spectator/value_spec.cr new file mode 100644 index 0000000..ab7652e --- /dev/null +++ b/spec/spectator/value_spec.cr @@ -0,0 +1,35 @@ +require "../spec_helper" + +Spectator.describe Spectator::Value do + subject { described_class.new(42, "Test Label") } + + it "stores the value" do + expect(&.value).to eq(42) + end + + # TODO: Fix issue with compile-time type of `subject` being a union. + # describe "#to_s" do + # subject { super.to_s } + # + # it "contains the label" do + # is_expected.to contain("Test Label") + # end + # + # it "contains the value" do + # is_expected.to contain("42") + # end + # end + # + # describe "#inspect" do + # let(value) { described_class.new([42], "Test Label") } + # subject { value.inspect } + # + # it "contains the label" do + # is_expected.to contain("Test Label") + # end + # + # it "contains the value" do + # is_expected.to contain("[42]") + # end + # end +end From 86a85c0946587e177cd195d2897ebc98016dfbec Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 9 Feb 2021 23:27:00 -0700 Subject: [PATCH 189/399] Missed a method that should be private --- src/spectator/test_context.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr index d56d817..0f83c60 100644 --- a/src/spectator/test_context.cr +++ b/src/spectator/test_context.cr @@ -33,7 +33,7 @@ class SpectatorTestContext < SpectatorContext # Initial tags for tests. # This method should be overridden by example groups and examples. - def self.tags + private def self.tags ::Spectator::Tags.new end end From 3cd569e639862a519d86cb5d22450b6b78849781 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 10 Feb 2021 16:58:17 -0700 Subject: [PATCH 190/399] Prevent using reserved keywords in let and subject --- CHANGELOG.md | 1 + src/spectator/dsl.cr | 3 +++ src/spectator/dsl/values.cr | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e6215e..b224148 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Overhaul storage of test values. - Cleanup and simplify DSL implementation. - Better error messages and detection when DSL methods are used when they shouldn't (i.e. `describe` inside `it`). +- Prevent usage of reserved keywords in DSL (such as `initialize`). - Other minor internal improvements and cleanup. ### Removed diff --git a/src/spectator/dsl.cr b/src/spectator/dsl.cr index 6a98e85..51d6f38 100644 --- a/src/spectator/dsl.cr +++ b/src/spectator/dsl.cr @@ -7,5 +7,8 @@ module Spectator # This also helps keep error traces small. # Documentation only useful for debugging is included in generated code. module DSL + # Keywords that cannot be used in specs using the DSL. + # These are either problematic or reserved for internal use. + RESERVED_KEYWORDS = %i[initialize] end end diff --git a/src/spectator/dsl/values.cr b/src/spectator/dsl/values.cr index fdff02f..09b9b7a 100644 --- a/src/spectator/dsl/values.cr +++ b/src/spectator/dsl/values.cr @@ -11,6 +11,7 @@ module Spectator::DSL {% raise "Block required for 'let'" unless block %} {% raise "Cannot use 'let' inside of a test block" if @def %} {% raise "Block argument count for 'let' must be 0..1" if block.args.size > 1 %} + {% raise "Cannot use '#{name.id}' for 'let'" if ::Spectator::DSL::RESERVED_KEYWORDS.includes?(name.id.symbolize) %} @%value = ::Spectator::LazyWrapper.new @@ -32,6 +33,7 @@ module Spectator::DSL {% raise "Block required for 'let!'" unless block %} {% raise "Cannot use 'let!' inside of a test block" if @def %} {% raise "Block argument count for 'let!' must be 0..1" if block.args.size > 1 %} + {% raise "Cannot use '#{name.id}' for 'let!'" if ::Spectator::DSL::RESERVED_KEYWORDS.includes?(name.id.symbolize) %} let({{name}}) {{block}} before_each { {{name.id}} } @@ -58,6 +60,7 @@ module Spectator::DSL {% raise "Block required for 'subject'" unless block %} {% raise "Cannot use 'subject' inside of a test block" if @def %} {% raise "Block argument count for 'subject' must be 0..1" if block.args.size > 1 %} + {% raise "Cannot use '#{name.id}' for 'subject'" if ::Spectator::DSL::RESERVED_KEYWORDS.includes?(name.id.symbolize) %} let({{name.id}}) {{block}} @@ -89,6 +92,7 @@ module Spectator::DSL {% raise "Block required for 'subject!'" unless block %} {% raise "Cannot use 'subject!' inside of a test block" if @def %} {% raise "Block argument count for 'subject!' must be 0..1" if block.args.size > 1 %} + {% raise "Cannot use '#{name.id}' for 'subject!'" if ::Spectator::DSL::RESERVED_KEYWORDS.includes?(name.id.symbolize) %} let!({{name.id}}) {{block}} From 3083f82132a3ad216c84fbda2c8a0b2d4e93e501 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 10 Feb 2021 17:07:49 -0700 Subject: [PATCH 191/399] Change DSL::Values to DSL::Memoize --- src/spectator/dsl/groups.cr | 2 +- src/spectator/dsl/{values.cr => memoize.cr} | 3 ++- src/spectator/test_context.cr | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) rename src/spectator/dsl/{values.cr => memoize.cr} (98%) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 001f4fb..bf91b75 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -1,7 +1,7 @@ require "../source" require "./builder" require "./tags" -require "./values" +require "./memoize" module Spectator::DSL # DSL methods and macros for creating example groups. diff --git a/src/spectator/dsl/values.cr b/src/spectator/dsl/memoize.cr similarity index 98% rename from src/spectator/dsl/values.cr rename to src/spectator/dsl/memoize.cr index 09b9b7a..0e9ab22 100644 --- a/src/spectator/dsl/values.cr +++ b/src/spectator/dsl/memoize.cr @@ -2,7 +2,8 @@ require "../lazy_wrapper" module Spectator::DSL # DSL methods for defining test values (subjects). - module Values + # These values are stored and reused throughout the test. + module Memoize # Defines a memoized getter. # The *name* is the name of the getter method. # The block is evaluated only on the first time the getter is used diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr index 0f83c60..5559473 100644 --- a/src/spectator/test_context.cr +++ b/src/spectator/test_context.cr @@ -13,8 +13,8 @@ class SpectatorTestContext < SpectatorContext include ::Spectator::DSL::Groups include ::Spectator::DSL::Hooks include ::Spectator::DSL::Matchers + include ::Spectator::DSL::Memoize include ::Spectator::DSL::Mocks - include ::Spectator::DSL::Values @subject = ::Spectator::LazyWrapper.new From 08451df643e7489b935a35b9e76f7c46feeee17a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 12 Feb 2021 18:33:50 -0700 Subject: [PATCH 192/399] Add matcher to check compiled type of values --- CHANGELOG.md | 1 + src/spectator/dsl/matchers.cr | 17 +++++- .../matchers/compiled_type_matcher.cr | 59 +++++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 src/spectator/matchers/compiled_type_matcher.cr diff --git a/CHANGELOG.md b/CHANGELOG.md index b224148..1f7cae1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support dynamic creation of examples. - Capture and log information for hooks. - Tags can be added to examples and example groups. +- Add matcher to check compiled type of values. ### Changed - Simplify and reduce defined types and generics. Should speed up compilation times. diff --git a/src/spectator/dsl/matchers.cr b/src/spectator/dsl/matchers.cr index c5ceeab..a8fabb1 100644 --- a/src/spectator/dsl/matchers.cr +++ b/src/spectator/dsl/matchers.cr @@ -123,7 +123,7 @@ module Spectator::DSL end # Indicates that some value should be of a specified type. - # The value's runtime class is checked. + # The value's runtime type is checked. # A type name or type union should be used for *expected*. # # Examples: @@ -135,7 +135,7 @@ module Spectator::DSL end # Indicates that some value should be of a specified type. - # The value's runtime class is checked. + # The value's runtime type is checked. # A type name or type union should be used for *expected*. # This method is identical to `#be_an_instance_of`, # and exists just to improve grammar. @@ -148,6 +148,19 @@ module Spectator::DSL be_instance_of({{expected}}) end + # Indicates that some value should be of a specified type at compile time. + # The value's compile time type is checked. + # This can test is a variable or value returned by a method is inferred to the expected type. + # + # Examples: + # ``` + # value = 42 || "foobar" + # expect(value).to compile_as(Int32 | String) + # ``` + macro compile_as(expected) + ::Spectator::Matchers::CompiledTypeMatcher({{expected}}).new + end + # Indicates that some value should respond to a method call. # One or more method names can be provided. # diff --git a/src/spectator/matchers/compiled_type_matcher.cr b/src/spectator/matchers/compiled_type_matcher.cr new file mode 100644 index 0000000..2b60e3b --- /dev/null +++ b/src/spectator/matchers/compiled_type_matcher.cr @@ -0,0 +1,59 @@ +require "./matcher" + +module Spectator::Matchers + # Matcher that tests a value is of a specified type at compile time. + # The values are compared with the `typeof` method. + # This can be used to inspect the inferred type of methods and variables. + struct CompiledTypeMatcher(Expected) < StandardMatcher + # Short text about the matcher's purpose. + # This explains what condition satisfies the matcher. + # The description is used when the one-liner syntax is used. + def description : String + "compiles as #{Expected}" + end + + # Checks whether the matcher is satisifed with the expression given to it. + private def match?(actual : Expression(T)) : Bool forall T + Expected == typeof(actual.value) + end + + # Message displayed when the matcher isn't satisifed. + # + # This is only called when `#match?` returns false. + # + # The message should typically only contain the test expression labels. + # Actual values should be returned by `#values`. + private def failure_message(actual) : String + "#{actual.label} does not compile as #{Expected}" + end + + # Message displayed when the matcher isn't satisifed and is negated. + # This is essentially what would satisfy the matcher if it wasn't negated. + # + # This is only called when `#does_not_match?` returns false. + # + # The message should typically only contain the test expression labels. + # Actual values should be returned by `#values`. + private def failure_message_when_negated(actual) : String + "#{actual.label} compiles as #{Expected}" + end + + # Additional information about the match failure. + # The return value is a NamedTuple with Strings for each value. + private def values(actual) + { + expected: Expected.to_s, + actual: typeof(actual.value).inspect, + } + end + + # Additional information about the match failure when negated. + # The return value is a NamedTuple with Strings for each value. + private def negated_values(actual) + { + expected: "Not #{Expected}", + actual: typeof(actual.value).inspect, + } + end + end +end From 74b78b7ca85af6e9953bbfc2a4dc1d3b460ccb4b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 12 Feb 2021 22:46:22 -0700 Subject: [PATCH 193/399] Rename Source to Location --- .../command_line_arguments_config_source.cr | 8 ++++---- src/spectator/dsl/builder.cr | 20 +++++++++---------- src/spectator/dsl/examples.cr | 4 ++-- src/spectator/dsl/expectations.cr | 12 +++++------ src/spectator/dsl/groups.cr | 4 ++-- src/spectator/dsl/hooks.cr | 6 +++--- src/spectator/dsl/mocks.cr | 14 ++++++------- src/spectator/example.cr | 20 +++++++++---------- src/spectator/example_group_hook.cr | 18 ++++++++--------- src/spectator/example_hook.cr | 18 ++++++++--------- src/spectator/example_procsy_hook.cr | 18 ++++++++--------- src/spectator/expectation.cr | 20 +++++++++---------- src/spectator/formatting/failure_block.cr | 8 ++++---- src/spectator/formatting/failure_command.cr | 2 +- src/spectator/formatting/json_formatter.cr | 2 +- src/spectator/formatting/junit_formatter.cr | 2 +- src/spectator/formatting/junit_test_case.cr | 2 +- src/spectator/formatting/location_timing.cr | 16 +++++++++++++++ src/spectator/formatting/profile_block.cr | 2 +- src/spectator/formatting/source_timing.cr | 16 --------------- src/spectator/formatting/tap_formatter.cr | 2 +- src/spectator/includes.cr | 4 ++-- src/spectator/line_example_filter.cr | 2 +- src/spectator/{source.cr => location.cr} | 12 +++++------ ...e_filter.cr => location_example_filter.cr} | 8 ++++---- src/spectator/mocks/exception_method_stub.cr | 4 ++-- src/spectator/mocks/expect_any_instance.cr | 4 ++-- src/spectator/mocks/generic_method_stub.cr | 6 +++--- src/spectator/mocks/method_stub.cr | 6 +++--- .../mocks/multi_value_method_stub.cr | 4 ++-- src/spectator/mocks/nil_method_stub.cr | 16 +++++++-------- src/spectator/mocks/proc_method_stub.cr | 8 ++++---- src/spectator/mocks/reflection.cr | 4 ++-- src/spectator/mocks/type_registry.cr | 4 ++-- src/spectator/mocks/value_method_stub.cr | 4 ++-- src/spectator/result.cr | 2 +- src/spectator/spec/builder.cr | 16 +++++++-------- src/spectator/spec/node.cr | 8 ++++---- 38 files changed, 163 insertions(+), 163 deletions(-) create mode 100644 src/spectator/formatting/location_timing.cr delete mode 100644 src/spectator/formatting/source_timing.cr rename src/spectator/{source.cr => location.cr} (86%) rename src/spectator/{source_example_filter.cr => location_example_filter.cr} (52%) diff --git a/src/spectator/command_line_arguments_config_source.cr b/src/spectator/command_line_arguments_config_source.cr index 07ea144..1eeba61 100644 --- a/src/spectator/command_line_arguments_config_source.cr +++ b/src/spectator/command_line_arguments_config_source.cr @@ -2,9 +2,9 @@ require "option_parser" require "./config_source" require "./formatting" require "./line_example_filter" +require "./location" +require "./location_example_filter" require "./name_example_filter" -require "./source" -require "./source_example_filter" module Spectator # Generates configuration from the command-line arguments. @@ -130,8 +130,8 @@ module Spectator private def location_option(parser, builder) parser.on("--location FILE:LINE", "Run the example at line 'LINE' in the file 'FILE', multiple allowed") do |location| Log.debug { "Filtering for examples at #{location} (--location '#{location}')" } - source = Source.parse(location) - filter = SourceExampleFilter.new(source) + location = Location.parse(location) + filter = LocationExampleFilter.new(location) builder.add_example_filter(filter) end end diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr index df24b2c..3d9ec47 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -38,32 +38,32 @@ module Spectator::DSL end # Defines a block of code to execute before any and all examples in the current group. - def before_all(source = nil, label = "before_all", &block) - hook = ExampleGroupHook.new(source: source, label: label, &block) + def before_all(location = nil, label = "before_all", &block) + hook = ExampleGroupHook.new(location: location, label: label, &block) @@builder.before_all(hook) end # Defines a block of code to execute before every example in the current group - def before_each(source = nil, label = "before_each", &block : Example -> _) - hook = ExampleHook.new(source: source, label: label, &block) + def before_each(location = nil, label = "before_each", &block : Example -> _) + hook = ExampleHook.new(location: location, label: label, &block) @@builder.before_each(hook) end # Defines a block of code to execute after any and all examples in the current group. - def after_all(source = nil, label = "after_all", &block) - hook = ExampleGroupHook.new(source: source, label: label, &block) + def after_all(location = nil, label = "after_all", &block) + hook = ExampleGroupHook.new(location: location, label: label, &block) @@builder.after_all(hook) end # Defines a block of code to execute after every example in the current group. - def after_each(source = nil, label = "after_each", &block : Example ->) - hook = ExampleHook.new(source: source, label: label, &block) + def after_each(location = nil, label = "after_each", &block : Example ->) + hook = ExampleHook.new(location: location, label: label, &block) @@builder.after_each(hook) end # Defines a block of code to execute around every example in the current group. - def around_each(source = nil, label = "around_each", &block : Example::Procsy ->) - hook = ExampleProcsyHook.new(source: source, label: label, &block) + def around_each(location = nil, label = "around_each", &block : Example::Procsy ->) + hook = ExampleProcsyHook.new(location: location, label: label, &block) @@builder.around_each(hook) end diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index af25638..3e05c11 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -1,5 +1,5 @@ require "../context" -require "../source" +require "../location" require "./builder" require "./tags" @@ -47,7 +47,7 @@ module Spectator::DSL ::Spectator::DSL::Builder.add_example( _spectator_example_name(\{{what}}), - ::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}}), + ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}), new.as(::Spectator::Context), \%tags ) do |example| diff --git a/src/spectator/dsl/expectations.cr b/src/spectator/dsl/expectations.cr index 94a90b7..137b363 100644 --- a/src/spectator/dsl/expectations.cr +++ b/src/spectator/dsl/expectations.cr @@ -1,7 +1,7 @@ require "../block" require "../expectation" require "../expectation_failed" -require "../source" +require "../location" require "../value" module Spectator::DSL @@ -10,7 +10,7 @@ module Spectator::DSL # Immediately fail the current test. # A reason can be specified with *message*. def fail(message = "Example failed", *, _file = __FILE__, _line = __LINE__) - raise ExpectationFailed.new(Source.new(_file, _line), message) + raise ExpectationFailed.new(Location.new(_file, _line), message) end # Starts an expectation. @@ -30,8 +30,8 @@ module Spectator::DSL end %expression = ::Spectator::Value.new(%actual, {{actual.stringify}}) - %source = ::Spectator::Source.new({{actual.filename}}, {{actual.line_number}}) - ::Spectator::Expectation::Target.new(%expression, %source) + %location = ::Spectator::Location.new({{actual.filename}}, {{actual.line_number}}) + ::Spectator::Expectation::Target.new(%expression, %location) end # Starts an expectation. @@ -82,8 +82,8 @@ module Spectator::DSL {% raise "Unexpected block arguments in 'expect' call" %} {% end %} - %source = ::Spectator::Source.new({{block.filename}}, {{block.line_number}}) - ::Spectator::Expectation::Target.new(%block, %source) + %location = ::Spectator::Location.new({{block.filename}}, {{block.line_number}}) + ::Spectator::Expectation::Target.new(%block, %location) end # Short-hand for expecting something of the subject. diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index bf91b75..0fd2028 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -1,4 +1,4 @@ -require "../source" +require "../location" require "./builder" require "./tags" require "./memoize" @@ -41,7 +41,7 @@ module Spectator::DSL ::Spectator::DSL::Builder.start_group( _spectator_group_name(\{{what}}), - ::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}}), + ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}), tags ) diff --git a/src/spectator/dsl/hooks.cr b/src/spectator/dsl/hooks.cr index 75dd06c..61fa15f 100644 --- a/src/spectator/dsl/hooks.cr +++ b/src/spectator/dsl/hooks.cr @@ -1,4 +1,4 @@ -require "../source" +require "../location" require "./builder" module Spectator::DSL @@ -16,7 +16,7 @@ module Spectator::DSL end ::Spectator::DSL::Builder.{{type.id}}( - ::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}}) + ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}) ) { \%hook } end end @@ -34,7 +34,7 @@ module Spectator::DSL end ::Spectator::DSL::Builder.{{type.id}}( - ::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}}) + ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}) ) do |example| example.with_context(\{{@type.name}}) do \{% if block.args.empty? %} diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 049a9f1..7ee1ffc 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -150,24 +150,24 @@ module Spectator::DSL end macro expect_any_instance_of(type, _source_file = __FILE__, _source_line = __LINE__) - %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - ::Spectator::Mocks::ExpectAnyInstance({{type}}).new(%source) + %location = ::Spectator::Location.new({{_source_file}}, {{_source_line}}) + ::Spectator::Mocks::ExpectAnyInstance({{type}}).new(%location) end macro receive(method_name, _source_file = __FILE__, _source_line = __LINE__, &block) - %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) + %location = ::Spectator::Location.new({{_source_file}}, {{_source_line}}) {% if block.is_a?(Nop) %} - ::Spectator::Mocks::NilMethodStub.new({{method_name.id.symbolize}}, %source) + ::Spectator::Mocks::NilMethodStub.new({{method_name.id.symbolize}}, %location) {% else %} - ::Spectator::Mocks::ProcMethodStub.create({{method_name.id.symbolize}}, %source) { {{block.body}} } + ::Spectator::Mocks::ProcMethodStub.create({{method_name.id.symbolize}}, %location) { {{block.body}} } {% end %} end macro receive_messages(_source_file = __FILE__, _source_line = __LINE__, **stubs) - %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) + %location = ::Spectator::Location.new({{_source_file}}, {{_source_line}}) %stubs = [] of ::Spectator::Mocks::MethodStub {% for name, value in stubs %} - %stubs << ::Spectator::Mocks::ValueMethodStub.new({{name.id.symbolize}}, %source, {{value}}) + %stubs << ::Spectator::Mocks::ValueMethodStub.new({{name.id.symbolize}}, %location, {{value}}) {% end %} %stubs end diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 3fc713b..c210d2b 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -1,9 +1,9 @@ require "./example_context_delegate" require "./example_group" require "./harness" +require "./location" require "./pending_result" require "./result" -require "./source" require "./spec/node" require "./tags" @@ -27,14 +27,14 @@ module Spectator # The *entrypoint* defines the test code (typically inside *context*). # The *name* describes the purpose of the example. # It can be a `Symbol` to describe a type. - # The *source* tracks where the example exists in source code. + # The *location* tracks where the example exists in source code. # The example will be assigned to *group* if it is provided. # A set of *tags* can be used for filtering and modifying example behavior. # Note: The tags will not be merged with the parent tags. def initialize(@context : Context, @entrypoint : self ->, - name : String? = nil, source : Source? = nil, + name : String? = nil, location : Location? = nil, group : ExampleGroup? = nil, tags = Tags.new) - super(name, source, group, tags) + super(name, location, group, tags) end # Creates a dynamic example. @@ -42,13 +42,13 @@ module Spectator # The block will be given this example instance as an argument. # The *name* describes the purpose of the example. # It can be a `Symbol` to describe a type. - # The *source* tracks where the example exists in source code. + # The *location* tracks where the example exists in source code. # The example will be assigned to *group* if it is provided. # A set of *tags* can be used for filtering and modifying example behavior. # Note: The tags will not be merged with the parent tags. - def initialize(name : String? = nil, source : Source? = nil, group : ExampleGroup? = nil, + def initialize(name : String? = nil, location : Location? = nil, group : ExampleGroup? = nil, tags = Tags.new, &block : self ->) - super(name, source, group, tags) + super(name, location, group, tags) @context = NullContext.new @entrypoint = block end @@ -140,10 +140,10 @@ module Spectator to_s(io) io << '"' - # Add source if it's available. - if (source = self.source) + # Add location if it's available. + if (location = self.location) io << " @ " - io << source + io << location end io << result diff --git a/src/spectator/example_group_hook.cr b/src/spectator/example_group_hook.cr index 06b8c7d..85f6898 100644 --- a/src/spectator/example_group_hook.cr +++ b/src/spectator/example_group_hook.cr @@ -1,11 +1,11 @@ require "./label" -require "./source" +require "./location" module Spectator # Information about a hook tied to an example group and a proc to invoke it. class ExampleGroupHook # Location of the hook in source code. - getter! source : Source + getter! location : Location # User-defined description of the hook. getter! label : Label @@ -14,14 +14,14 @@ module Spectator # Creates the hook with a proc. # The *proc* will be called when the hook is invoked. - # A *source* and *label* can be provided for debugging. - def initialize(@proc : (->), *, @source : Source? = nil, @label : Label = nil) + # A *location* and *label* can be provided for debugging. + def initialize(@proc : (->), *, @location : Location? = nil, @label : Label = nil) end # Creates the hook with a block. # The block will be executed when the hook is invoked. - # A *source* and *label* can be provided for debugging. - def initialize(*, @source : Source? = nil, @label : Label = nil, &block : -> _) + # A *location* and *label* can be provided for debugging. + def initialize(*, @location : Location? = nil, @label : Label = nil, &block : -> _) @proc = block end @@ -31,7 +31,7 @@ module Spectator end # Produces the string representation of the hook. - # Includes the source and label if they're not nil. + # Includes the location and label if they're not nil. def to_s(io) io << "example group hook" @@ -40,9 +40,9 @@ module Spectator io << label end - if (source = @source) + if (location = @location) io << " @ " - io << source + io << location end end end diff --git a/src/spectator/example_hook.cr b/src/spectator/example_hook.cr index b61fa7d..1f02445 100644 --- a/src/spectator/example_hook.cr +++ b/src/spectator/example_hook.cr @@ -1,11 +1,11 @@ require "./label" -require "./source" +require "./location" module Spectator # Information about a hook tied to an example and a proc to invoke it. class ExampleHook # Location of the hook in source code. - getter! source : Source + getter! location : Location # User-defined description of the hook. getter! label : Label @@ -14,15 +14,15 @@ module Spectator # Creates the hook with a proc. # The *proc* will be called when the hook is invoked. - # A *source* and *label* can be provided for debugging. - def initialize(@proc : (Example ->), *, @source : Source? = nil, @label : Label = nil) + # A *location* and *label* can be provided for debugging. + def initialize(@proc : (Example ->), *, @location : Location? = nil, @label : Label = nil) end # Creates the hook with a block. # The block must take a single argument - the current example. # The block will be executed when the hook is invoked. - # A *source* and *label* can be provided for debugging. - def initialize(*, @source : Source? = nil, @label : Label = nil, &block : Example -> _) + # A *location* and *label* can be provided for debugging. + def initialize(*, @location : Location? = nil, @label : Label = nil, &block : Example -> _) @proc = block end @@ -33,7 +33,7 @@ module Spectator end # Produces the string representation of the hook. - # Includes the source and label if they're not nil. + # Includes the location and label if they're not nil. def to_s(io) io << "example hook" @@ -42,9 +42,9 @@ module Spectator io << label end - if (source = @source) + if (location = @location) io << " @ " - io << source + io << location end end end diff --git a/src/spectator/example_procsy_hook.cr b/src/spectator/example_procsy_hook.cr index 0b6ea63..27ccae6 100644 --- a/src/spectator/example_procsy_hook.cr +++ b/src/spectator/example_procsy_hook.cr @@ -1,11 +1,11 @@ require "./label" -require "./source" +require "./location" module Spectator # Information about a hook tied to an example and a proc to invoke it. class ExampleProcsyHook # Location of the hook in source code. - getter! source : Source + getter! location : Location # User-defined description of the hook. getter! label : Label @@ -14,15 +14,15 @@ module Spectator # Creates the hook with a proc. # The *proc* will be called when the hook is invoked. - # A *source* and *label* can be provided for debugging. - def initialize(@proc : (Example::Procsy ->), *, @source : Source? = nil, @label : Label = nil) + # A *location* and *label* can be provided for debugging. + def initialize(@proc : (Example::Procsy ->), *, @location : Location? = nil, @label : Label = nil) end # Creates the hook with a block. # The block must take a single argument - the current example wrapped in a procsy. # The block will be executed when the hook is invoked. - # A *source* and *label* can be provided for debugging. - def initialize(*, @source : Source? = nil, @label : Label = nil, &block : Example::Procsy -> _) + # A *location* and *label* can be provided for debugging. + def initialize(*, @location : Location? = nil, @label : Label = nil, &block : Example::Procsy -> _) @proc = block end @@ -38,7 +38,7 @@ module Spectator end # Produces the string representation of the hook. - # Includes the source and label if they're not nil. + # Includes the location and label if they're not nil. def to_s(io) io << "example hook" @@ -47,9 +47,9 @@ module Spectator io << label end - if (source = @source) + if (location = @location) io << " @ " - io << source + io << location end end end diff --git a/src/spectator/expectation.cr b/src/spectator/expectation.cr index 6b38771..e7324dc 100644 --- a/src/spectator/expectation.cr +++ b/src/spectator/expectation.cr @@ -1,5 +1,5 @@ require "./expression" -require "./source" +require "./location" module Spectator # Result of evaluating a matcher on a target. @@ -7,9 +7,9 @@ module Spectator # such as whether it was successful and a description of the operation. struct Expectation # Location of the expectation in source code. - # This can be nil if the source isn't capturable, + # This can be nil if the location isn't capturable, # for instance using the *should* syntax or dynamically created expectations. - getter source : Source? + getter location : Location? # Indicates whether the expectation was met. def satisfied? @@ -48,14 +48,14 @@ module Spectator # Creates the expectation. # The *match_data* comes from the result of calling `Matcher#match`. - # The *source* is the location of the expectation in source code, if available. - def initialize(@match_data : Matchers::MatchData, @source : Source? = nil) + # The *location* is the location of the expectation in source code, if available. + def initialize(@match_data : Matchers::MatchData, @location : Location? = nil) end # Creates the JSON representation of the expectation. def to_json(json : ::JSON::Builder) json.object do - json.field("source") { @source.to_json(json) } + json.field("location") { @location.to_json(json) } json.field("satisfied", satisfied?) if (failed = @match_data.as?(Matchers::FailedMatchData)) failed_to_json(failed, json) @@ -76,15 +76,15 @@ module Spectator end # Stores part of an expectation. - # This covers the actual value (or block) being inspected and its source. + # This covers the actual value (or block) being inspected and its location. # This is the type returned by an `expect` block in the DSL. # It is not intended to be used directly, but instead by chaining methods. # Typically `#to` and `#not_to` are used. struct Target(T) # Creates the expectation target. # The *expression* is the actual value being tested and its label. - # The *source* is the location of where this expectation was defined. - def initialize(@expression : Expression(T), @source : Source) + # The *location* is the location of where this expectation was defined. + def initialize(@expression : Expression(T), @location : Location) end # Asserts that some criteria defined by the matcher is satisfied. @@ -163,7 +163,7 @@ module Spectator # Reports an expectation to the current harness. private def report(match_data : Matchers::MatchData) - expectation = Expectation.new(match_data, @source) + expectation = Expectation.new(match_data, @location) Harness.current.report(expectation) end end diff --git a/src/spectator/formatting/failure_block.cr b/src/spectator/formatting/failure_block.cr index ff24022..e1eb053 100644 --- a/src/spectator/formatting/failure_block.cr +++ b/src/spectator/formatting/failure_block.cr @@ -28,7 +28,7 @@ module Spectator::Formatting title(indent) indent.increase(inner_indent) do content(indent) - source(indent) + location(indent) end end end @@ -103,9 +103,9 @@ module Spectator::Formatting end end - # Produces the source line of the failure block. - private def source(indent) - indent.line(Comment.color(@result.example.source)) + # Produces the location line of the failure block. + private def location(indent) + indent.line(Comment.color(@result.example.location)) end # Gets the number of characters a positive integer spans in base 10. diff --git a/src/spectator/formatting/failure_command.cr b/src/spectator/formatting/failure_command.cr index 1d4247c..27c6896 100644 --- a/src/spectator/formatting/failure_command.cr +++ b/src/spectator/formatting/failure_command.cr @@ -8,7 +8,7 @@ module Spectator::Formatting # Appends the command to the output. def to_s(io) io << "crystal spec " - io << @example.source + io << @example.location end # Colorizes the command instance based on the result. diff --git a/src/spectator/formatting/json_formatter.cr b/src/spectator/formatting/json_formatter.cr index da09f18..369a896 100644 --- a/src/spectator/formatting/json_formatter.cr +++ b/src/spectator/formatting/json_formatter.cr @@ -88,7 +88,7 @@ module Spectator::Formatting @json.object do @json.field("example", result.example) @json.field("time", result.elapsed.total_seconds) - @json.field("source", result.example.source) + @json.field("location", result.example.location) end end end diff --git a/src/spectator/formatting/junit_formatter.cr b/src/spectator/formatting/junit_formatter.cr index 26a6a1a..f1dd60e 100644 --- a/src/spectator/formatting/junit_formatter.cr +++ b/src/spectator/formatting/junit_formatter.cr @@ -55,7 +55,7 @@ module Spectator::Formatting # Adds all of the individual test suite blocks. private def add_test_suites(report) - report.group_by(&.example.source.path).each do |path, results| + report.group_by(&.example.location.path).each do |path, results| JUnitTestSuite.new(path, results).to_xml(@xml) end end diff --git a/src/spectator/formatting/junit_test_case.cr b/src/spectator/formatting/junit_test_case.cr index c4b140e..30eacb3 100644 --- a/src/spectator/formatting/junit_test_case.cr +++ b/src/spectator/formatting/junit_test_case.cr @@ -31,7 +31,7 @@ module Spectator::Formatting # Java-ified class name created from the spec. private def classname - path = result.example.source.path + path = result.example.location.path file = File.basename(path) ext = File.extname(file) name = file[0...-(ext.size)] diff --git a/src/spectator/formatting/location_timing.cr b/src/spectator/formatting/location_timing.cr new file mode 100644 index 0000000..7e2c8a8 --- /dev/null +++ b/src/spectator/formatting/location_timing.cr @@ -0,0 +1,16 @@ +module Spectator::Formatting + # Produces the timing line in a profile block. + # This contains the length of time, and the example's location. + private struct LocationTiming + # Creates the location timing line. + def initialize(@span : Time::Span, @location : Location) + end + + # Appends the location timing information to the output. + def to_s(io) + io << HumanTime.new(@span).colorize.bold + io << ' ' + io << @location + end + end +end diff --git a/src/spectator/formatting/profile_block.cr b/src/spectator/formatting/profile_block.cr index d17cf86..ba7943f 100644 --- a/src/spectator/formatting/profile_block.cr +++ b/src/spectator/formatting/profile_block.cr @@ -21,7 +21,7 @@ module Spectator::Formatting private def entry(indent, result) indent.line(result.example) indent.increase do - indent.line(SourceTiming.new(result.elapsed, result.example.source)) + indent.line(LocationTiming.new(result.elapsed, result.example.location)) end end end diff --git a/src/spectator/formatting/source_timing.cr b/src/spectator/formatting/source_timing.cr deleted file mode 100644 index 527fa17..0000000 --- a/src/spectator/formatting/source_timing.cr +++ /dev/null @@ -1,16 +0,0 @@ -module Spectator::Formatting - # Produces the timing line in a profile block. - # This contains the length of time, and the example's source. - private struct SourceTiming - # Creates the source timing line. - def initialize(@span : Time::Span, @source : Source) - end - - # Appends the source timing information to the output. - def to_s(io) - io << HumanTime.new(@span).colorize.bold - io << ' ' - io << @source - end - end -end diff --git a/src/spectator/formatting/tap_formatter.cr b/src/spectator/formatting/tap_formatter.cr index 816da4f..018d879 100644 --- a/src/spectator/formatting/tap_formatter.cr +++ b/src/spectator/formatting/tap_formatter.cr @@ -51,7 +51,7 @@ module Spectator::Formatting indent.line(result.example) indent.increase do @io << "# " - indent.line(SourceTiming.new(result.elapsed, result.example.source)) + indent.line(LocationTiming.new(result.elapsed, result.example.location)) end end end diff --git a/src/spectator/includes.cr b/src/spectator/includes.cr index 9f73ade..e931000 100644 --- a/src/spectator/includes.cr +++ b/src/spectator/includes.cr @@ -37,6 +37,8 @@ require "./label" require "./lazy" require "./lazy_wrapper" require "./line_example_filter" +require "./location" +require "./location_example_filter" require "./matchers" require "./mocks" require "./name_example_filter" @@ -47,8 +49,6 @@ require "./pending_result" require "./profile" require "./report" require "./result" -require "./source" -require "./source_example_filter" require "./spec" require "./tags" require "./test_context" diff --git a/src/spectator/line_example_filter.cr b/src/spectator/line_example_filter.cr index 85cd739..dac4bcf 100644 --- a/src/spectator/line_example_filter.cr +++ b/src/spectator/line_example_filter.cr @@ -7,7 +7,7 @@ module Spectator # Checks whether the example satisfies the filter. def includes?(example) : Bool - @line == example.source.line + @line == example.location.line end end end diff --git a/src/spectator/source.cr b/src/spectator/location.cr similarity index 86% rename from src/spectator/source.cr rename to src/spectator/location.cr index b09d5df..2c6f444 100644 --- a/src/spectator/source.cr +++ b/src/spectator/location.cr @@ -1,19 +1,19 @@ require "json" module Spectator - # Define the file and line number something originated from. - struct Source + # Defines the file and line number a piece of code originated from. + struct Location # Absolute file path. getter file : String # Line number in the file. getter line : Int32 - # Creates the source. + # Creates the location. def initialize(@file, @line) end - # Parses a source from a string. + # Parses a location from a string. # The *string* should be in the form: # ```text # FILE:LINE @@ -50,7 +50,7 @@ module Spectator end end - # String representation of the source. + # String representation of the location. # This is formatted as: # ```text # FILE:LINE @@ -61,7 +61,7 @@ module Spectator io << line end - # Creates the JSON representation of the source. + # Creates the JSON representation of the location. def to_json(json : ::JSON::Builder) json.string(to_s) end diff --git a/src/spectator/source_example_filter.cr b/src/spectator/location_example_filter.cr similarity index 52% rename from src/spectator/source_example_filter.cr rename to src/spectator/location_example_filter.cr index d3ad4dc..116d1d9 100644 --- a/src/spectator/source_example_filter.cr +++ b/src/spectator/location_example_filter.cr @@ -1,14 +1,14 @@ module Spectator # Filter that matches examples in a given file and line. - class SourceExampleFilter < ExampleFilter + class LocationExampleFilter < ExampleFilter # Creates the filter. - # The *source* indicates which file and line the example must be on. - def initialize(@source : Source) + # The *location* indicates which file and line the example must be on. + def initialize(@location : Location) end # Checks whether the example satisfies the filter. def includes?(example) : Bool - @source === example.source + @location === example.location end end end diff --git a/src/spectator/mocks/exception_method_stub.cr b/src/spectator/mocks/exception_method_stub.cr index 96342bc..2f223fd 100644 --- a/src/spectator/mocks/exception_method_stub.cr +++ b/src/spectator/mocks/exception_method_stub.cr @@ -3,8 +3,8 @@ require "./generic_method_stub" module Spectator::Mocks class ExceptionMethodStub(ExceptionType) < GenericMethodStub(Nil) - def initialize(name, source, @exception : ExceptionType, args = nil) - super(name, source, args) + def initialize(name, location, @exception : ExceptionType, args = nil) + super(name, location, args) end def call(_args : GenericArguments(T, NT), &_original : -> RT) forall T, NT, RT diff --git a/src/spectator/mocks/expect_any_instance.cr b/src/spectator/mocks/expect_any_instance.cr index 0ccee05..607c425 100644 --- a/src/spectator/mocks/expect_any_instance.cr +++ b/src/spectator/mocks/expect_any_instance.cr @@ -2,7 +2,7 @@ require "./registry" module Spectator::Mocks struct ExpectAnyInstance(T) - def initialize(@source : Source) + def initialize(@location : Location) end def to(stub : MethodStub) : Nil @@ -10,7 +10,7 @@ module Spectator::Mocks Harness.current.mocks.expect(T, stub) value = Value.new(stub.name, stub.to_s) matcher = Matchers::ReceiveTypeMatcher.new(value, stub.arguments?) - partial = Expectations::ExpectationPartial.new(actual, @source) + partial = Expectations::ExpectationPartial.new(actual, @location) partial.to_eventually(matcher) end diff --git a/src/spectator/mocks/generic_method_stub.cr b/src/spectator/mocks/generic_method_stub.cr index dc8eba6..db4ff8f 100644 --- a/src/spectator/mocks/generic_method_stub.cr +++ b/src/spectator/mocks/generic_method_stub.cr @@ -7,8 +7,8 @@ module Spectator::Mocks abstract class GenericMethodStub(ReturnType) < MethodStub getter! arguments : Arguments - def initialize(name, source, @args : Arguments? = nil) - super(name, source) + def initialize(name, location, @args : Arguments? = nil) + super(name, location) end def callable?(call : MethodCall) : Bool @@ -25,7 +25,7 @@ module Spectator::Mocks io << " : " io << ReturnType io << " at " - io << @source + io << @location end end end diff --git a/src/spectator/mocks/method_stub.cr b/src/spectator/mocks/method_stub.cr index 492fc3f..8419501 100644 --- a/src/spectator/mocks/method_stub.cr +++ b/src/spectator/mocks/method_stub.cr @@ -1,13 +1,13 @@ -require "../source" +require "../location" require "./method_call" module Spectator::Mocks abstract class MethodStub getter name : Symbol - getter source : Source + getter location : Location - def initialize(@name, @source) + def initialize(@name, @location) end def callable?(call : MethodCall) : Bool diff --git a/src/spectator/mocks/multi_value_method_stub.cr b/src/spectator/mocks/multi_value_method_stub.cr index c623a4c..f025271 100644 --- a/src/spectator/mocks/multi_value_method_stub.cr +++ b/src/spectator/mocks/multi_value_method_stub.cr @@ -5,8 +5,8 @@ module Spectator::Mocks class MultiValueMethodStub(ReturnType) < GenericMethodStub(ReturnType) @index = 0 - def initialize(name, source, @values : ReturnType, args = nil) - super(name, source, args) + def initialize(name, location, @values : ReturnType, args = nil) + super(name, location, args) raise ArgumentError.new("Values must have at least one item") if @values.size < 1 end diff --git a/src/spectator/mocks/nil_method_stub.cr b/src/spectator/mocks/nil_method_stub.cr index 592d3b6..19b437a 100644 --- a/src/spectator/mocks/nil_method_stub.cr +++ b/src/spectator/mocks/nil_method_stub.cr @@ -13,36 +13,36 @@ module Spectator::Mocks end def and_return(value) - ValueMethodStub.new(@name, @source, value, @args) + ValueMethodStub.new(@name, @location, value, @args) end def and_return(*values) - MultiValueMethodStub.new(@name, @source, values.to_a, @args) + MultiValueMethodStub.new(@name, @location, values.to_a, @args) end def and_raise(exception_type : Exception.class) - ExceptionMethodStub.new(@name, @source, exception_type.new, @args) + ExceptionMethodStub.new(@name, @location, exception_type.new, @args) end def and_raise(exception : Exception) - ExceptionMethodStub.new(@name, @source, exception, @args) + ExceptionMethodStub.new(@name, @location, exception, @args) end def and_raise(message : String) - ExceptionMethodStub.new(@name, @source, Exception.new(message), @args) + ExceptionMethodStub.new(@name, @location, Exception.new(message), @args) end def and_raise(exception_type : Exception.class, *args) forall T - ExceptionMethodStub.new(@name, @source, exception_type.new(*args), @args) + ExceptionMethodStub.new(@name, @location, exception_type.new(*args), @args) end def with(*args : *T, **opts : **NT) forall T, NT args = GenericArguments.new(args, opts) - NilMethodStub.new(@name, @source, args) + NilMethodStub.new(@name, @location, args) end def and_call_original - OriginalMethodStub.new(@name, @source, @args) + OriginalMethodStub.new(@name, @location, @args) end end end diff --git a/src/spectator/mocks/proc_method_stub.cr b/src/spectator/mocks/proc_method_stub.cr index 1d55db3..de3a68d 100644 --- a/src/spectator/mocks/proc_method_stub.cr +++ b/src/spectator/mocks/proc_method_stub.cr @@ -3,12 +3,12 @@ require "./generic_method_stub" module Spectator::Mocks class ProcMethodStub(ReturnType) < GenericMethodStub(ReturnType) - def initialize(name, source, @proc : -> ReturnType, args = nil) - super(name, source, args) + def initialize(name, location, @proc : -> ReturnType, args = nil) + super(name, location, args) end - def self.create(name, source, args = nil, &block : -> T) forall T - ProcMethodStub.new(name, source, block, args) + def self.create(name, location, args = nil, &block : -> T) forall T + ProcMethodStub.new(name, location, block, args) end def call(_args : GenericArguments(T, NT), &_original : -> RT) forall T, NT, RT diff --git a/src/spectator/mocks/reflection.cr b/src/spectator/mocks/reflection.cr index cf014e1..9a03fd9 100644 --- a/src/spectator/mocks/reflection.cr +++ b/src/spectator/mocks/reflection.cr @@ -4,7 +4,7 @@ module Spectator::Mocks module Reflection private macro _spectator_reflect {% for meth in @type.methods %} - %source = ::Spectator::Source.new({{meth.filename}}, {{meth.line_number}}) + %location = ::Spectator::Location.new({{meth.filename}}, {{meth.line_number}}) %args = ::Spectator::Mocks::GenericArguments.create( {% for arg, i in meth.args %} {% matcher = if arg.restriction @@ -19,7 +19,7 @@ module Spectator::Mocks {{matcher}}{% if i < meth.args.size %},{% end %} {% end %} ) - ::Spectator::Mocks::TypeRegistry.add({{@type.id.stringify}}, {{meth.name.symbolize}}, %source, %args) + ::Spectator::Mocks::TypeRegistry.add({{@type.id.stringify}}, {{meth.name.symbolize}}, %location, %args) {% end %} end end diff --git a/src/spectator/mocks/type_registry.cr b/src/spectator/mocks/type_registry.cr index 4f960f3..631fec9 100644 --- a/src/spectator/mocks/type_registry.cr +++ b/src/spectator/mocks/type_registry.cr @@ -6,14 +6,14 @@ module Spectator::Mocks @@entries = {} of Key => Deque(MethodStub) - def add(type_name : String, method_name : Symbol, source : Source, args : Arguments) : Nil + def add(type_name : String, method_name : Symbol, location : Location, args : Arguments) : Nil key = {type_name, method_name} list = if @@entries.has_key?(key) @@entries[key] else @@entries[key] = Deque(MethodStub).new end - list << NilMethodStub.new(method_name, source, args) + list << NilMethodStub.new(method_name, location, args) end def exists?(type_name : String, call : MethodCall) : Bool diff --git a/src/spectator/mocks/value_method_stub.cr b/src/spectator/mocks/value_method_stub.cr index 43bfcaa..d3e7f5e 100644 --- a/src/spectator/mocks/value_method_stub.cr +++ b/src/spectator/mocks/value_method_stub.cr @@ -3,8 +3,8 @@ require "./generic_method_stub" module Spectator::Mocks class ValueMethodStub(ReturnType) < GenericMethodStub(ReturnType) - def initialize(name, source, @value : ReturnType, args = nil) - super(name, source, args) + def initialize(name, location, @value : ReturnType, args = nil) + super(name, location, args) end def call(_args : GenericArguments(T, NT), &_original : -> RT) forall T, NT, RT diff --git a/src/spectator/result.cr b/src/spectator/result.cr index d85bf39..2c775ef 100644 --- a/src/spectator/result.cr +++ b/src/spectator/result.cr @@ -31,7 +31,7 @@ module Spectator # Adds the common fields for a result to a JSON builder. private def add_json_fields(json : ::JSON::Builder) json.field("name", example) - json.field("location", example.source) + json.field("location", example.location) json.field("result", to_s) json.field("time", elapsed.total_seconds) json.field("expectations", expectations) diff --git a/src/spectator/spec/builder.cr b/src/spectator/spec/builder.cr index f9a4ced..3d340a7 100644 --- a/src/spectator/spec/builder.cr +++ b/src/spectator/spec/builder.cr @@ -52,16 +52,16 @@ module Spectator # This should be a symbol when describing a type - the type name is represented as a symbol. # Otherwise, a string should be used. # - # The *source* optionally defined where the group originates in source code. + # The *location* optionally defined where the group originates in source code. # # A set of *tags* can be used for filtering and modifying example behavior. # For instance, adding a "pending" tag will mark tests as pending and skip execution. # # The newly created group is returned. # It shouldn't be used outside of this class until a matching `#end_group` is called. - def start_group(name, source = nil, tags = Tags.new) : ExampleGroup - Log.trace { "Start group: #{name.inspect} @ #{source}; tags: #{tags}" } - ExampleGroup.new(name, source, current_group, tags).tap do |group| + def start_group(name, location = nil, tags = Tags.new) : ExampleGroup + Log.trace { "Start group: #{name.inspect} @ #{location}; tags: #{tags}" } + ExampleGroup.new(name, location, current_group, tags).tap do |group| @group_stack << group end end @@ -86,7 +86,7 @@ module Spectator # This should be a string or nil. # When nil, the example's name will be populated by the first expectation run inside of the test code. # - # The *source* optionally defined where the example originates in source code. + # The *location* optionally defined where the example originates in source code. # # The *context* is an instance of the context the test code should run in. # See `Context` for more information. @@ -100,9 +100,9 @@ module Spectator # It is expected that the test code runs when the block is called. # # The newly created example is returned. - def add_example(name, source, context, tags = Tags.new, &block : Example -> _) : Example - Log.trace { "Add example: #{name} @ #{source}; tags: #{tags}" } - Example.new(context, block, name, source, current_group, tags) + def add_example(name, location, context, tags = Tags.new, &block : Example -> _) : Example + Log.trace { "Add example: #{name} @ #{location}; tags: #{tags}" } + Example.new(context, block, name, location, current_group, tags) # The example is added to the current group by `Example` initializer. end diff --git a/src/spectator/spec/node.cr b/src/spectator/spec/node.cr index c499f6f..4bbde0c 100644 --- a/src/spectator/spec/node.cr +++ b/src/spectator/spec/node.cr @@ -1,5 +1,5 @@ require "../label" -require "../source" +require "../location" require "../tags" module Spectator @@ -9,7 +9,7 @@ module Spectator # but can be anything that should be iterated over when running the spec. abstract class Node # Location of the node in source code. - getter! source : Source + getter! location : Location # User-provided name or description of the node. # This does not include the group name or descriptions. @@ -41,10 +41,10 @@ module Spectator # Creates the node. # The *name* describes the purpose of the node. # It can be a `Symbol` to describe a type. - # The *source* tracks where the node exists in source code. + # The *location* tracks where the node exists in source code. # The node will be assigned to *group* if it is provided. # A set of *tags* can be used for filtering and modifying example behavior. - def initialize(@name : Label = nil, @source : Source? = nil, + def initialize(@name : Label = nil, @location : Location? = nil, group : ExampleGroup? = nil, @tags : Tags = Tags.new) # Ensure group is linked. group << self if group From 7d5c9edab768323c59e0a16e17a6591010c9c727 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 13 Feb 2021 11:44:51 -0700 Subject: [PATCH 194/399] Use cast as workaround --- spec/spectator/value_spec.cr | 53 ++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/spec/spectator/value_spec.cr b/spec/spectator/value_spec.cr index ab7652e..246e453 100644 --- a/spec/spectator/value_spec.cr +++ b/spec/spectator/value_spec.cr @@ -4,32 +4,33 @@ Spectator.describe Spectator::Value do subject { described_class.new(42, "Test Label") } it "stores the value" do - expect(&.value).to eq(42) + # NOTE: This cast is a workaround for [issue #55](https://gitlab.com/arctic-fox/spectator/-/issues/55) + value = subject.as(Spectator::Value(Int32)).value + expect(value).to eq(42) end - # TODO: Fix issue with compile-time type of `subject` being a union. - # describe "#to_s" do - # subject { super.to_s } - # - # it "contains the label" do - # is_expected.to contain("Test Label") - # end - # - # it "contains the value" do - # is_expected.to contain("42") - # end - # end - # - # describe "#inspect" do - # let(value) { described_class.new([42], "Test Label") } - # subject { value.inspect } - # - # it "contains the label" do - # is_expected.to contain("Test Label") - # end - # - # it "contains the value" do - # is_expected.to contain("[42]") - # end - # end + describe "#to_s" do + subject { super.to_s } + + it "contains the label" do + is_expected.to contain("Test Label") + end + + it "contains the value" do + is_expected.to contain("42") + end + end + + describe "#inspect" do + let(value) { described_class.new([42], "Test Label") } + subject { value.inspect } + + it "contains the label" do + is_expected.to contain("Test Label") + end + + it "contains the value" do + is_expected.to contain("[42]") + end + end end From 4af23751bca89f418c539de5a408050ebf49ba0c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 13 Feb 2021 13:30:05 -0700 Subject: [PATCH 195/399] Add specs for value types --- spec/spectator/lazy_spec.cr | 15 +++++++++++++++ spec/spectator/wrapper_spec.cr | 18 ++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 spec/spectator/lazy_spec.cr create mode 100644 spec/spectator/wrapper_spec.cr diff --git a/spec/spectator/lazy_spec.cr b/spec/spectator/lazy_spec.cr new file mode 100644 index 0000000..8458a80 --- /dev/null +++ b/spec/spectator/lazy_spec.cr @@ -0,0 +1,15 @@ +require "../spec_helper" + +Spectator.describe Spectator::Lazy do + subject { Spectator::Lazy(Int32).new } + + it "returns the value of the block" do + expect { subject.get { 42 } }.to eq(42) + end + + it "caches the value" do + count = 0 + expect { subject.get { count += 1 } }.to change { count }.from(0).to(1) + expect { subject.get { count += 1 } }.to_not change { count } + end +end diff --git a/spec/spectator/wrapper_spec.cr b/spec/spectator/wrapper_spec.cr new file mode 100644 index 0000000..aa2351c --- /dev/null +++ b/spec/spectator/wrapper_spec.cr @@ -0,0 +1,18 @@ +require "../spec_helper" + +Spectator.describe Spectator::Wrapper do + it "stores a value" do + wrapper = described_class.new(42) + expect(wrapper.get(Int32)).to eq(42) + end + + it "retrieves a value using the block trick" do + wrapper = described_class.new(Int32) + expect(wrapper.get { Int32 }).to eq(Int32) + end + + it "raises on invalid cast" do + wrapper = described_class.new(42) + expect { wrapper.get(String) }.to raise_error(TypeCastError) + end +end From 79d6ad93b390f9df2d5d1fafce79433e2ceb115c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 13 Feb 2021 17:33:52 -0700 Subject: [PATCH 196/399] Avoid using subject with structs --- spec/spectator/lazy_spec.cr | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/spectator/lazy_spec.cr b/spec/spectator/lazy_spec.cr index 8458a80..0f138bd 100644 --- a/spec/spectator/lazy_spec.cr +++ b/spec/spectator/lazy_spec.cr @@ -1,15 +1,15 @@ require "../spec_helper" Spectator.describe Spectator::Lazy do - subject { Spectator::Lazy(Int32).new } - it "returns the value of the block" do - expect { subject.get { 42 } }.to eq(42) + lazy = Spectator::Lazy(Int32).new + expect { lazy.get { 42 } }.to eq(42) end it "caches the value" do + lazy = Spectator::Lazy(Int32).new count = 0 - expect { subject.get { count += 1 } }.to change { count }.from(0).to(1) - expect { subject.get { count += 1 } }.to_not change { count } + expect { lazy.get { count += 1 } }.to change { count }.from(0).to(1) + expect { lazy.get { count += 1 } }.to_not change { count } end end From d612657b15929a288b7775479b28725f898fa61b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 31 Mar 2021 15:28:16 -0600 Subject: [PATCH 197/399] Merge branch 'release/0.10' into specs --- CHANGELOG.md | 14 ++++++++++-- shard.yml | 2 +- spec/line_number_spec.cr | 32 ++++++++++++++++++++++++++++ src/spectator/dsl/examples.cr | 2 +- src/spectator/line_example_filter.cr | 4 +++- src/spectator/location.cr | 10 +++++++-- 6 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 spec/line_number_spec.cr diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f7cae1..bccdc60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed - Removed one-liner it syntax without braces (block). +## [0.9.34] - 2021-03-31 +### Changed +- Allow filtering examples by using any line in the example block. [#19](https://github.com/icy-arctic-fox/spectator/issues/19) Thanks @matthewmcgarvey ! + +## [0.9.33] - 2021-03-22 +### Changed +- Target Crystal 1.0 + ## [0.9.32] - 2021-02-03 ### Fixed - Fix source reference with brace-less example syntax. [#20](https://github.com/icy-arctic-fox/spectator/issues/20) @@ -266,8 +274,10 @@ This has been changed so that it compiles and raises an error at runtime with a First version ready for public use. -[Unreleased]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.32...HEAD -[0.9.31]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.31...v0.9.32 +[Unreleased]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.34...HEAD +[0.9.34]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.33...v0.9.34 +[0.9.33]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.32...v0.9.33 +[0.9.32]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.31...v0.9.32 [0.9.31]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.30...v0.9.31 [0.9.30]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.29...v0.9.30 [0.9.29]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.28...v0.9.29 diff --git a/shard.yml b/shard.yml index e624ed0..830c2c0 100644 --- a/shard.yml +++ b/shard.yml @@ -6,7 +6,7 @@ description: | authors: - Michael Miller -crystal: 0.35.1 +crystal: 1.0.0 license: MIT diff --git a/spec/line_number_spec.cr b/spec/line_number_spec.cr new file mode 100644 index 0000000..5439ea0 --- /dev/null +++ b/spec/line_number_spec.cr @@ -0,0 +1,32 @@ +require "./spec_helper" + +Spectator.describe Spectator do + let(current_example) { ::Spectator::Example.current } + subject(location) { current_example.location } + + context "line numbers" do + it "contains starting line of spec" do + expect(location.line).to eq(__LINE__ - 1) + end + + it "contains ending line of spec" do + expect(location.end_line).to eq(__LINE__ + 1) + end + + it "handles multiple lines and examples" do + # Offset is important. + expect(location.line).to eq(__LINE__ - 2) + # 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. + end + end + + context "file names" do + subject { location.file } + + it "match source code" do + is_expected.to eq(__FILE__) + end + end +end diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 3e05c11..5d8b88b 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -47,7 +47,7 @@ module Spectator::DSL ::Spectator::DSL::Builder.add_example( _spectator_example_name(\{{what}}), - ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}), + ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}, \{{block.end_line_number}}), new.as(::Spectator::Context), \%tags ) do |example| diff --git a/src/spectator/line_example_filter.cr b/src/spectator/line_example_filter.cr index dac4bcf..25b5fc8 100644 --- a/src/spectator/line_example_filter.cr +++ b/src/spectator/line_example_filter.cr @@ -7,7 +7,9 @@ module Spectator # Checks whether the example satisfies the filter. def includes?(example) : Bool - @line == example.location.line + start_line = example.location.line + end_line = example.location.end_line + (start_line..end_line).covers?(@line) end end end diff --git a/src/spectator/location.cr b/src/spectator/location.cr index 2c6f444..bf1ac68 100644 --- a/src/spectator/location.cr +++ b/src/spectator/location.cr @@ -6,11 +6,17 @@ module Spectator # Absolute file path. getter file : String - # Line number in the file. + # Starting line number in the file. getter line : Int32 + # Ending line number in the file. + getter end_line : Int32 + # Creates the location. - def initialize(@file, @line) + def initialize(@file, @line, end_line = nil) + # if an end line is not provided, + # make the end line the same as the start line + @end_line = end_line || @line end # Parses a location from a string. From 5dfc60d4cd894938e024645de6376719cf3f2e52 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 26 Apr 2021 16:53:04 -0600 Subject: [PATCH 198/399] Fix nil reference error when example name is unavailable --- src/spectator/formatting/document_formatter.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/formatting/document_formatter.cr b/src/spectator/formatting/document_formatter.cr index 16efdf8..331fb13 100644 --- a/src/spectator/formatting/document_formatter.cr +++ b/src/spectator/formatting/document_formatter.cr @@ -29,7 +29,7 @@ module Spectator::Formatting # Produces a single character output based on a result. def end_example(result) @previous_hierarchy.size.times { @io.print INDENT } - @io.puts result.accept(Color) { result.example.name } + @io.puts result.accept(Color) { result.example } end # Produces a list of groups making up the hierarchy for an example. From 6c98d7107c6bc3fc1a3c4c3366badf800f6866d7 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 26 Apr 2021 17:11:53 -0600 Subject: [PATCH 199/399] Docs --- src/spectator/anything.cr | 11 ++++++++--- src/spectator/context.cr | 7 +++++++ src/spectator/source.cr | 2 +- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/spectator/anything.cr b/src/spectator/anything.cr index 511a024..2991e87 100644 --- a/src/spectator/anything.cr +++ b/src/spectator/anything.cr @@ -1,14 +1,19 @@ module Spectator + # Equals and matches everything. + # All comparison methods will always return true. struct Anything - def ==(other) + # Returns true for equality. + def ==(_other) true end - def ===(other) + # Returns true for case equality. + def ===(_other) true end - def =~(other) + # Returns true for matching. + def =~(_other) true end end diff --git a/src/spectator/context.cr b/src/spectator/context.cr index 2860ae3..475f49c 100644 --- a/src/spectator/context.cr +++ b/src/spectator/context.cr @@ -4,10 +4,17 @@ # This type is intentionally outside the `Spectator` module. # The reason for this is to prevent name collision when using the DSL to define a spec. abstract class SpectatorContext + # Produces a dummy string to represent the context as a string. + # This prevents the default behavior, which normally stringifies instance variables. + # Due to the sheer amount of types Spectator can create + # and that the Crystal compiler instantiates a `#to_s` and/or `#inspect` for each of those types, + # an explosion in method instances can be created. + # The compile time is drastically reduced by using a dummy string instead. def to_s(io) io << "Context" end + # :ditto: def inspect(io) io << "Context<" io << self.class diff --git a/src/spectator/source.cr b/src/spectator/source.cr index e69f5ff..fa28cea 100644 --- a/src/spectator/source.cr +++ b/src/spectator/source.cr @@ -1,7 +1,7 @@ require "json" module Spectator - # Define the file and line number something originated from. + # Defines the file and line numbers something originated from. struct Source # Absolute file path. getter file : String From f549fcfa7a377bc60fdfa47eebfb9d3e4c3c1b3b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 26 Apr 2021 17:17:18 -0600 Subject: [PATCH 200/399] Minor changes to configure methods --- src/spectator.cr | 2 +- src/spectator/spec/builder.cr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator.cr b/src/spectator.cr index 3dff022..88238a8 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -50,7 +50,7 @@ module Spectator # A `ConfigBuilder` is yielded to allow changing the configuration. # NOTE: The configuration set here can be overriden # with a `.spectator` file and command-line arguments. - def configure : Nil + def configure(& : ConfigBuilder -> _) : Nil yield @@config_builder end diff --git a/src/spectator/spec/builder.cr b/src/spectator/spec/builder.cr index f9a4ced..63975c4 100644 --- a/src/spectator/spec/builder.cr +++ b/src/spectator/spec/builder.cr @@ -175,7 +175,7 @@ module Spectator # Builds the configuration to use for the spec. # A `ConfigBuilder` is yielded to the block provided to this method. # That builder will be used to create the configuration. - def config + def configure(& : ConfigBuilder -> _) : Nil builder = ConfigBuilder.new yield builder @config = builder.build From 02b98ea61b5f6420a033d4776c12ecd009b95a61 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 26 Apr 2021 18:47:11 -0600 Subject: [PATCH 201/399] Remove reference to example from result Pass examples instead of results into formatters. --- src/spectator/example.cr | 10 ++--- src/spectator/fail_result.cr | 4 +- .../formatting/document_formatter.cr | 4 +- src/spectator/formatting/dots_formatter.cr | 4 +- .../formatting/error_junit_test_case.cr | 4 +- src/spectator/formatting/failure_block.cr | 19 +++++---- src/spectator/formatting/failure_command.cr | 4 +- .../formatting/failure_junit_test_case.cr | 4 +- src/spectator/formatting/formatter.cr | 4 +- src/spectator/formatting/json_formatter.cr | 12 +++--- src/spectator/formatting/junit_formatter.cr | 8 ++-- src/spectator/formatting/junit_test_case.cr | 11 ++++- src/spectator/formatting/junit_test_suite.cr | 26 ++++++------ src/spectator/formatting/profile_block.cr | 10 ++--- src/spectator/formatting/silent_formatter.cr | 4 +- .../formatting/skipped_junit_test_case.cr | 4 +- .../formatting/successful_junit_test_case.cr | 4 +- src/spectator/formatting/suite_summary.cr | 10 ++--- src/spectator/formatting/tap_formatter.cr | 16 ++++---- src/spectator/formatting/tap_test_line.cr | 14 +++---- src/spectator/harness.cr | 7 ++-- src/spectator/pending_result.cr | 2 +- src/spectator/profile.cr | 12 +++--- src/spectator/report.cr | 41 +++++++++++-------- src/spectator/result.cr | 12 ++---- src/spectator/spec/runner.cr | 20 ++++----- 26 files changed, 144 insertions(+), 126 deletions(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index c210d2b..b048469 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -16,11 +16,9 @@ module Spectator # Indicates whether the example already ran. getter? finished : Bool = false - # Retrieves the result of the last time the example ran. - def result : Result - # TODO: Set to pending immediately (requires circular dependency between Example <-> Result removed). - @result ||= PendingResult.new(self) - end + # Result of the last time the example ran. + # Is pending if the example hasn't run. + getter result : Result = PendingResult.new # Creates the example. # An instance to run the test code in is given by *context*. @@ -62,7 +60,7 @@ module Spectator if pending? Log.debug { "Skipping example #{self} - marked pending" } - return @result = PendingResult.new(self) + return @result = PendingResult.new end previous_example = @@current diff --git a/src/spectator/fail_result.cr b/src/spectator/fail_result.cr index dfe8937..c28aa6d 100644 --- a/src/spectator/fail_result.cr +++ b/src/spectator/fail_result.cr @@ -11,8 +11,8 @@ module Spectator # Creates a failure result. # The *elapsed* argument is the length of time it took to run the example. # The *error* is the exception raised that caused the failure. - def initialize(example, elapsed, @error, expectations = [] of Expectation) - super(example, elapsed, expectations) + def initialize(elapsed, @error, expectations = [] of Expectation) + super(elapsed, expectations) end # Calls the `failure` method on *visitor*. diff --git a/src/spectator/formatting/document_formatter.cr b/src/spectator/formatting/document_formatter.cr index 331fb13..77cf23a 100644 --- a/src/spectator/formatting/document_formatter.cr +++ b/src/spectator/formatting/document_formatter.cr @@ -27,9 +27,9 @@ module Spectator::Formatting end # Produces a single character output based on a result. - def end_example(result) + def end_example(example) @previous_hierarchy.size.times { @io.print INDENT } - @io.puts result.accept(Color) { result.example } + @io.puts example.result.accept(Color) { example } end # Produces a list of groups making up the hierarchy for an example. diff --git a/src/spectator/formatting/dots_formatter.cr b/src/spectator/formatting/dots_formatter.cr index 1fd35c6..15dbbc4 100644 --- a/src/spectator/formatting/dots_formatter.cr +++ b/src/spectator/formatting/dots_formatter.cr @@ -20,8 +20,8 @@ module Spectator::Formatting end # Produces a single character output based on a result. - def end_example(result) - @io.print result.accept(Character) + def end_example(example) + @io.print example.result.accept(Character) end # Interface for `Result` to pick a character for output. diff --git a/src/spectator/formatting/error_junit_test_case.cr b/src/spectator/formatting/error_junit_test_case.cr index f1a12ce..41f71d1 100644 --- a/src/spectator/formatting/error_junit_test_case.cr +++ b/src/spectator/formatting/error_junit_test_case.cr @@ -7,7 +7,9 @@ module Spectator::Formatting private getter result # Creates the JUnit test case. - def initialize(@result : ErrorResult) + def initialize(example : Example) + super + @result = example.result.as(ErrorResult) end # Adds the exception to the XML block. diff --git a/src/spectator/formatting/failure_block.cr b/src/spectator/formatting/failure_block.cr index e1eb053..4af0c0e 100644 --- a/src/spectator/formatting/failure_block.cr +++ b/src/spectator/formatting/failure_block.cr @@ -15,8 +15,13 @@ module Spectator::Formatting private struct FailureBlock # Creates the failure block. # The *index* uniquely identifies the failure in the output. - # The *result* is the outcome of the failed example. - def initialize(@index : Int32, @result : FailResult) + # The *example* is the failed example. + def initialize(@index : Int32, @example : Example) + end + + # Retrieves the failed result. + private def result + @example.result.as(FailResult) end # Creates the block of text describing the failure. @@ -39,7 +44,7 @@ module Spectator::Formatting # 1) Example name # ``` private def title(indent) - indent.line(NumberedItem.new(@index, @result.example)) + indent.line(NumberedItem.new(@index, @example)) end # Produces the main content of the failure block. @@ -47,12 +52,12 @@ module Spectator::Formatting # then an error stacktrace if an error occurred. private def content(indent) unsatisfied_expectations(indent) - error_stacktrace(indent) if @result.is_a?(ErrorResult) + error_stacktrace(indent) if result.is_a?(ErrorResult) end # Produces a list of unsatisfied expectations and their values. private def unsatisfied_expectations(indent) - @result.expectations.reject(&.satisfied?).each do |expectation| + result.expectations.reject(&.satisfied?).each do |expectation| indent.line(Color.failure(LabeledText.new("Failure", expectation.failure_message))) indent.line indent.increase do @@ -76,7 +81,7 @@ module Spectator::Formatting # Produces the stack trace for an errored result. private def error_stacktrace(indent) - error = @result.error + error = result.error first_line = error.message.try(&.lines).try(&.first) indent.line(Color.error(LabeledText.new("Error", first_line))) indent.line @@ -105,7 +110,7 @@ module Spectator::Formatting # Produces the location line of the failure block. private def location(indent) - indent.line(Comment.color(@result.example.location)) + indent.line(Comment.color(@example.location)) end # Gets the number of characters a positive integer spans in base 10. diff --git a/src/spectator/formatting/failure_command.cr b/src/spectator/formatting/failure_command.cr index 27c6896..34f8bea 100644 --- a/src/spectator/formatting/failure_command.cr +++ b/src/spectator/formatting/failure_command.cr @@ -12,8 +12,8 @@ module Spectator::Formatting end # Colorizes the command instance based on the result. - def self.color(result) - result.accept(Color) { new(result.example) } + def self.color(example) + example.result.accept(Color) { new(example) } end end end diff --git a/src/spectator/formatting/failure_junit_test_case.cr b/src/spectator/formatting/failure_junit_test_case.cr index 99181ea..37f0823 100644 --- a/src/spectator/formatting/failure_junit_test_case.cr +++ b/src/spectator/formatting/failure_junit_test_case.cr @@ -7,7 +7,9 @@ module Spectator::Formatting private getter result # Creates the JUnit test case. - def initialize(@result : FailResult) + def initialize(example : Example) + super + @result = example.result.as(FailResult) end # Status string specific to the result type. diff --git a/src/spectator/formatting/formatter.cr b/src/spectator/formatting/formatter.cr index 9efd800..ac37fef 100644 --- a/src/spectator/formatting/formatter.cr +++ b/src/spectator/formatting/formatter.cr @@ -21,7 +21,7 @@ module Spectator::Formatting abstract def start_example(example : Example) # Called when a test finishes. - # The result of the test is provided. - abstract def end_example(result : Result) + # The result of the test is available through *example*. + abstract def end_example(example : Example) end end diff --git a/src/spectator/formatting/json_formatter.cr b/src/spectator/formatting/json_formatter.cr index 369a896..36fd746 100644 --- a/src/spectator/formatting/json_formatter.cr +++ b/src/spectator/formatting/json_formatter.cr @@ -36,8 +36,8 @@ module Spectator::Formatting # Called when a test finishes. # The result of the test is provided. - def end_example(result : Result) - result.to_json(@json) + def end_example(example : Example) + example.result.to_json(@json, example) end # Adds the totals section of the document. @@ -84,11 +84,11 @@ module Spectator::Formatting end # Adds a profile entry to the document. - private def profile_entry(result) + private def profile_entry(example) @json.object do - @json.field("example", result.example) - @json.field("time", result.elapsed.total_seconds) - @json.field("location", result.example.location) + @json.field("example", example) + @json.field("time", example.result.elapsed.total_seconds) + @json.field("location", example.location) end end end diff --git a/src/spectator/formatting/junit_formatter.cr b/src/spectator/formatting/junit_formatter.cr index f1dd60e..c523072 100644 --- a/src/spectator/formatting/junit_formatter.cr +++ b/src/spectator/formatting/junit_formatter.cr @@ -37,8 +37,8 @@ module Spectator::Formatting end # Called when a test finishes. - # The result of the test is provided. - def end_example(result : Result) + # The result of the test is provided by *example*. + def end_example(example : Example) end # Creates the "testsuites" block in the XML. @@ -55,8 +55,8 @@ module Spectator::Formatting # Adds all of the individual test suite blocks. private def add_test_suites(report) - report.group_by(&.example.location.path).each do |path, results| - JUnitTestSuite.new(path, results).to_xml(@xml) + report.group_by(&.location.path).each do |path, examples| + JUnitTestSuite.new(path, examples).to_xml(@xml) end end end diff --git a/src/spectator/formatting/junit_test_case.cr b/src/spectator/formatting/junit_test_case.cr index 30eacb3..223942a 100644 --- a/src/spectator/formatting/junit_test_case.cr +++ b/src/spectator/formatting/junit_test_case.cr @@ -1,6 +1,10 @@ module Spectator::Formatting # Base type for all JUnit test case results. private abstract class JUnitTestCase + # Creates the JUnit test case. + def initialize(@example : Example) + end + # Produces the test case XML element. def to_xml(xml : ::XML::Builder) xml.element("testcase", **attributes) do @@ -11,7 +15,7 @@ module Spectator::Formatting # Attributes that go in the "testcase" XML element. private def attributes { - name: result.example, + name: example, status: status, classname: classname, } @@ -23,6 +27,9 @@ module Spectator::Formatting # Status string specific to the result type. private abstract def status : String + # Example for this test case. + private getter example : Example + # Adds additional content to the "testcase" XML block. # Override this to add more content. private def content(xml) @@ -31,7 +38,7 @@ module Spectator::Formatting # Java-ified class name created from the spec. private def classname - path = result.example.location.path + path = example.location.path file = File.basename(path) ext = File.extname(file) name = file[0...-(ext.size)] diff --git a/src/spectator/formatting/junit_test_suite.cr b/src/spectator/formatting/junit_test_suite.cr index f7abc4b..73e4e34 100644 --- a/src/spectator/formatting/junit_test_suite.cr +++ b/src/spectator/formatting/junit_test_suite.cr @@ -3,9 +3,9 @@ module Spectator::Formatting private struct JUnitTestSuite # Creates the JUnit test suite. # The *path* should be the file that all results are from. - # The *results* is a subset of all results that share the path. - def initialize(@path : String, results : Array(Result)) - @report = Report.new(results) + # The *examples* is a subset of all examples that share the path. + def initialize(@path : String, examples : Array(Example)) + @report = Report.new(examples) end # Generates the XML for the test suite (and all nested test cases). @@ -24,8 +24,8 @@ module Spectator::Formatting # Adds the test case elements to the XML. private def add_test_cases(xml) - @report.each do |result| - test_case = result.accept(JUnitTestCaseSelector) { |r| r } + @report.each do |example| + test_case = example.result.accept(JUnitTestCaseSelector) { example } test_case.to_xml(xml) end end @@ -50,23 +50,23 @@ module Spectator::Formatting extend self # Creates a successful JUnit test case. - def pass(result) - SuccessfulJUnitTestCase.new(result.as(PassResult)) + def pass(example) + SuccessfulJUnitTestCase.new(example) end # Creates a failure JUnit test case. - def failure(result) - FailureJUnitTestCase.new(result.as(FailResult)) + def failure(example) + FailureJUnitTestCase.new(example) end # Creates an error JUnit test case. - def error(result) - ErrorJUnitTestCase.new(result.as(ErrorResult)) + def error(example) + ErrorJUnitTestCase.new(example) end # Creates a skipped JUnit test case. - def pending(result) - SkippedJUnitTestCase.new(result.as(PendingResult)) + def pending(example) + SkippedJUnitTestCase.new(example) end end end diff --git a/src/spectator/formatting/profile_block.cr b/src/spectator/formatting/profile_block.cr index ba7943f..319705d 100644 --- a/src/spectator/formatting/profile_block.cr +++ b/src/spectator/formatting/profile_block.cr @@ -11,17 +11,17 @@ module Spectator::Formatting indent = Indent.new(io) indent.increase do - @profile.each do |result| - entry(indent, result) + @profile.each do |example| + entry(indent, example) end end end # Adds a result entry to the output. - private def entry(indent, result) - indent.line(result.example) + private def entry(indent, example) + indent.line(example) indent.increase do - indent.line(LocationTiming.new(result.elapsed, result.example.location)) + indent.line(LocationTiming.new(example.result.elapsed, example.location)) end end end diff --git a/src/spectator/formatting/silent_formatter.cr b/src/spectator/formatting/silent_formatter.cr index 2098480..1556534 100644 --- a/src/spectator/formatting/silent_formatter.cr +++ b/src/spectator/formatting/silent_formatter.cr @@ -19,8 +19,8 @@ module Spectator::Formatting end # Called when a test finishes. - # The result of the test is provided. - def end_example(result : Result) + # The result of the test is provided by *example*. + def end_example(example : Example) # ... crickets ... end end diff --git a/src/spectator/formatting/skipped_junit_test_case.cr b/src/spectator/formatting/skipped_junit_test_case.cr index 6b90b4d..0c0c383 100644 --- a/src/spectator/formatting/skipped_junit_test_case.cr +++ b/src/spectator/formatting/skipped_junit_test_case.cr @@ -7,7 +7,9 @@ module Spectator::Formatting private getter result # Creates the JUnit test case. - def initialize(@result : PendingResult) + def initialize(example : Example) + super + @result = example.result.as(PendingResult) end # Status string specific to the result type. diff --git a/src/spectator/formatting/successful_junit_test_case.cr b/src/spectator/formatting/successful_junit_test_case.cr index 803ea6e..cb4e85f 100644 --- a/src/spectator/formatting/successful_junit_test_case.cr +++ b/src/spectator/formatting/successful_junit_test_case.cr @@ -5,7 +5,9 @@ module Spectator::Formatting private getter result # Creates the JUnit test case. - def initialize(@result : PassResult) + def initialize(example : Example) + super + @result = example.result.as(PassResult) end # Status string specific to the result type. diff --git a/src/spectator/formatting/suite_summary.cr b/src/spectator/formatting/suite_summary.cr index 183a78e..cf733f5 100644 --- a/src/spectator/formatting/suite_summary.cr +++ b/src/spectator/formatting/suite_summary.cr @@ -33,8 +33,8 @@ module Spectator::Formatting private def failures(failures) @io.puts "Failures:" @io.puts - failures.each_with_index do |result, index| - @io.puts FailureBlock.new(index + 1, result) + failures.each_with_index do |example, index| + @io.puts FailureBlock.new(index + 1, example) end end @@ -68,10 +68,10 @@ module Spectator::Formatting @io.puts @io.puts "Failed examples:" @io.puts - failures.each do |result| - @io << FailureCommand.color(result) + failures.each do |example| + @io << FailureCommand.color(example) @io << ' ' - @io.puts Comment.color(result.example) + @io.puts Comment.color(example) end end end diff --git a/src/spectator/formatting/tap_formatter.cr b/src/spectator/formatting/tap_formatter.cr index 018d879..2c7c810 100644 --- a/src/spectator/formatting/tap_formatter.cr +++ b/src/spectator/formatting/tap_formatter.cr @@ -27,9 +27,9 @@ module Spectator::Formatting end # Called when a test finishes. - # The result of the test is provided. - def end_example(result : Result) - @io.puts TAPTestLine.new(@index, result) + # The result of the test is provided by *example*. + def end_example(example : Example) + @io.puts TAPTestLine.new(@index, example) @index += 1 end @@ -39,19 +39,19 @@ module Spectator::Formatting indent = Indent.new(@io) indent.increase do - profile.each do |result| - profile_entry(indent, result) + profile.each do |example| + profile_entry(indent, example) end end end # Adds a profile result entry to the output. - private def profile_entry(indent, result) + private def profile_entry(indent, example) @io << "# " - indent.line(result.example) + indent.line(example) indent.increase do @io << "# " - indent.line(LocationTiming.new(result.elapsed, result.example.location)) + indent.line(LocationTiming.new(example.result.elapsed, example.location)) end end end diff --git a/src/spectator/formatting/tap_test_line.cr b/src/spectator/formatting/tap_test_line.cr index 089031b..dd55523 100644 --- a/src/spectator/formatting/tap_test_line.cr +++ b/src/spectator/formatting/tap_test_line.cr @@ -2,7 +2,7 @@ module Spectator::Formatting # Produces a formatted TAP test line. private struct TAPTestLine # Creates the test line. - def initialize(@index : Int32, @result : Result) + def initialize(@index : Int32, @example : Example) end # Appends the line to the output. @@ -11,23 +11,23 @@ module Spectator::Formatting io << ' ' io << @index io << " - " - io << example + io << @example io << " # skip" if pending? end # The text "ok" or "not ok" depending on the result. private def status - @result.is_a?(FailResult) ? "not ok" : "ok" + result.is_a?(FailResult) ? "not ok" : "ok" end - # The example that was tested. - private def example - @result.example + # The result of running the example. + private def result + @example.result end # Indicates whether this test was skipped. private def pending? - @result.is_a?(PendingResult) + result.is_a?(PendingResult) end end end diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 59e8975..34bd070 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -94,14 +94,13 @@ module Spectator # Takes the *elapsed* time and a possible *error* from the test. # Returns a type of `Result`. private def translate(elapsed, error) : Result - example = Example.current # TODO: Remove this. case error when nil - PassResult.new(example, elapsed, @expectations) + PassResult.new(elapsed, @expectations) when ExpectationFailed - FailResult.new(example, elapsed, error, @expectations) + FailResult.new(elapsed, error, @expectations) else - ErrorResult.new(example, elapsed, error, @expectations) + ErrorResult.new(elapsed, error, @expectations) end end diff --git a/src/spectator/pending_result.cr b/src/spectator/pending_result.cr index 4ff64e6..6d68e19 100644 --- a/src/spectator/pending_result.cr +++ b/src/spectator/pending_result.cr @@ -7,7 +7,7 @@ module Spectator class PendingResult < Result # Creates the result. # *elapsed* is the length of time it took to run the example. - def initialize(example, elapsed = Time::Span::ZERO, expectations = [] of Expectation) + def initialize(elapsed = Time::Span::ZERO, expectations = [] of Expectation) super end diff --git a/src/spectator/profile.cr b/src/spectator/profile.cr index 0067f7a..c4021e2 100644 --- a/src/spectator/profile.cr +++ b/src/spectator/profile.cr @@ -1,14 +1,14 @@ module Spectator # Information about the runtimes of examples. class Profile - include Indexable(Result) + include Indexable(Example) # Total length of time it took to run all examples in the test suite. getter total_time : Time::Span # Creates the profiling information. # The *slowest* results must already be sorted, longest time first. - private def initialize(@slowest : Array(Result), @total_time) + private def initialize(@slowest : Array(Example), @total_time) end # Number of results in the profile. @@ -23,7 +23,7 @@ module Spectator # Length of time it took to run the results in the profile. def time - @slowest.sum(&.elapsed) + @slowest.sum(&.result.elapsed) end # Percentage (from 0 to 1) of time the results in this profile took compared to all examples. @@ -33,9 +33,9 @@ module Spectator # Produces the profile from a report. def self.generate(report, size = 10) - results = report.to_a - sorted_results = results.sort_by(&.elapsed) - slowest = sorted_results.last(size).reverse + examples = report.to_a + sorted_examples = examples.sort_by(&.result.elapsed) + slowest = sorted_examples.last(size).reverse self.new(slowest, report.example_runtime) end end diff --git a/src/spectator/report.cr b/src/spectator/report.cr index e295b5b..996fb58 100644 --- a/src/spectator/report.cr +++ b/src/spectator/report.cr @@ -3,7 +3,7 @@ require "./result" module Spectator # Outcome of all tests in a suite. class Report - include Enumerable(Result) + include Enumerable(Example) # Total length of time it took to execute the test suite. # This includes examples, hooks, and framework processes. @@ -29,14 +29,14 @@ module Spectator getter! random_seed : UInt64? # Creates the report. - # The *results* are from running the examples in the test suite. + # The *examples* are all examples in the test suite. # The *runtime* is the total time it took to execute the suite. # The *remaining_count* is the number of tests skipped due to fail-fast. # The *fail_blank* flag indicates whether it is a failure if there were no tests run. # The *random_seed* is the seed used for random number generation. - def initialize(@results : Array(Result), @runtime, @remaining_count = 0, @fail_blank = false, @random_seed = nil) - @results.each do |result| - case result + def initialize(@examples : Array(Example), @runtime, @remaining_count = 0, @fail_blank = false, @random_seed = nil) + @examples.each do |example| + case example.result when PassResult @successful_count += 1 when ErrorResult @@ -55,23 +55,28 @@ module Spectator # Creates the report. # This constructor is intended for reports of subsets of results. - # The *results* are from running the examples in the test suite. + # The *examples* are all examples in the test suite. # The runtime is calculated from the *results*. - def initialize(results : Array(Result)) - runtime = results.sum(&.elapsed) - initialize(results, runtime) + def initialize(examples : Array(Example)) + runtime = examples.sum(&.result.elapsed) + initialize(examples, runtime) end - # Yields each result in turn. + # Yields each example in turn. def each - @results.each do |result| - yield result + @examples.each do |example| + yield example end end + # Retrieves results of all examples. + def results + @examples.each.map(&.result) + end + # Number of examples. def example_count - @results.size + @examples.size end # Number of examples run (not skipped or pending). @@ -90,21 +95,21 @@ module Spectator remaining_count > 0 end - # Returns a set of results for all failed examples. + # Returns a set of all failed examples. def failures - @results.each.compact_map(&.as?(FailResult)) + @examples.select(&.result.is_a?(FailResult)) end - # Returns a set of results for all errored examples. + # Returns a set of all errored examples. def errors - @results.each.compact_map(&.as?(ErrorResult)) + @examples.select(&.result.is_a?(ErrorResult)) end # Length of time it took to run just example code. # This does not include hooks, # but it does include pre- and post-conditions. def example_runtime - @results.sum(&.elapsed) + results.sum(&.elapsed) end # Length of time spent in framework processes and hooks. diff --git a/src/spectator/result.cr b/src/spectator/result.cr index 2c775ef..f20f483 100644 --- a/src/spectator/result.cr +++ b/src/spectator/result.cr @@ -2,10 +2,6 @@ module Spectator # Base class that represents the outcome of running an example. # Sub-classes contain additional information specific to the type of result. abstract class Result - # Example that generated the result. - # TODO: Remove this. - getter example : Example - # Length of time it took to run the example. getter elapsed : Time::Span @@ -14,7 +10,7 @@ module Spectator # Creates the result. # *elapsed* is the length of time it took to run the example. - def initialize(@example, @elapsed, @expectations = [] of Expectation) + def initialize(@elapsed, @expectations = [] of Expectation) end # Calls the corresponding method for the type of result. @@ -22,14 +18,14 @@ module Spectator abstract def accept(visitor) # Creates a JSON object from the result information. - def to_json(json : ::JSON::Builder) + def to_json(json : ::JSON::Builder, example) json.object do - add_json_fields(json) + add_json_fields(json, example) end end # Adds the common fields for a result to a JSON builder. - private def add_json_fields(json : ::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) diff --git a/src/spectator/spec/runner.cr b/src/spectator/spec/runner.cr index 99ce35b..f5ccf8f 100644 --- a/src/spectator/spec/runner.cr +++ b/src/spectator/spec/runner.cr @@ -16,25 +16,25 @@ module Spectator @config.each_formatter(&.start_suite(@suite)) # Run all examples and capture the results. - results = Array(Result).new(@suite.size) + examples = Array(Example).new(@suite.size) elapsed = Time.measure do - collect_results(results) + collect_results(examples) end # Generate a report and pass it along to the formatter. - remaining = @suite.size - results.size + remaining = @suite.size - examples.size seed = (@config.random_seed if @config.randomize?) - report = Report.new(results, elapsed, remaining, @config.fail_blank?, seed) + report = Report.new(examples, 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) + # Runs all examples and adds them to a list. + private def collect_results(examples) example_order.each do |example| - result = run_example(example).as(Result) - results << result + result = run_example(example) + examples << example if @config.fail_fast? && result.is_a?(FailResult) example.group.call_once_after_all break @@ -60,14 +60,14 @@ module Spectator else example.run end - @config.each_formatter(&.end_example(result)) + @config.each_formatter(&.end_example(example)) 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) + PassResult.new(Time::Span.zero, expectations) end # Generates and returns a profile if one should be displayed. From ff5d855389167266241bb5bc1d5a5b51a02f2dec Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 6 May 2021 22:10:40 -0600 Subject: [PATCH 202/399] Cleanup harness some --- src/spectator/harness.cr | 59 +++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 34bd070..231c959 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -6,7 +6,6 @@ require "./result" module Spectator # Helper class that acts as a gateway between test code and the framework. - # This is essentially an "example runner." # # Test code should be wrapped with a call to `.run`. # This class will catch all errors raised by the test code. @@ -44,12 +43,23 @@ module Spectator # It will be reset after the test regardless of the outcome. # The result of running the test code will be returned. def self.run : Result - harness = new + with_harness do |harness| + harness.run { yield } + end + end + + # Instanciates a new harness and yields it. + # The `.current` harness is set to the new harness for the duration of the block. + # `.current` is reset to the previous value (probably nil) afterwards, even if the block raises. + # The result of the block is returned. + private def self.with_harness previous = @@current - @@current = harness - result = harness.run { yield } - @@current = previous - result + begin + harness = new + yield harness + ensure + @@current = previous + end end @deferred = Deque(->).new @@ -59,14 +69,17 @@ module Spectator # The test code should be called from within the block given to this method. def run : Result elapsed, error = capture { yield } - elapsed2, error2 = run_deferred + elapsed2, error2 = capture { run_deferred } translate(elapsed + elapsed2, error || error2) end def report(expectation : Expectation) : Nil Log.debug { "Reporting expectation #{expectation}" } @expectations << expectation + + # TODO: Move this out of harness, maybe to `Example`. Example.current.name = expectation.description unless Example.current.name? + raise ExpectationFailed.new(expectation) if expectation.failed? end @@ -78,18 +91,25 @@ module Spectator # Yields to run the test code and returns information about the outcome. # Returns a tuple with the elapsed time and an error if one occurred (otherwise nil). - private def capture - error = nil.as(Exception?) + private def capture : Tuple(Time, Exception?) + error = nil elapsed = Time.measure do - begin - yield - rescue e - error = e - end + error = catch_error { yield } end {elapsed, error} end + # Yields to run a block of code and captures exceptions. + # If the block of code raises an error, the error is caught and returned. + # If the block doesn't raise an error, then nil is returned. + private def catch : Exception? + yield + rescue e + e + else + nil + end + # Translates the outcome of running a test to a result. # Takes the *elapsed* time and a possible *error* from the test. # Returns a type of `Result`. @@ -105,16 +125,11 @@ module Spectator end # Runs all deferred blocks. + # This method executes code from tests and may raise an error. + # It should be wrapped in a call to `#capture`. private def run_deferred Log.debug { "Running deferred operations" } - error = nil.as(Exception?) - elapsed = Time.measure do - @deferred.each(&.call) - rescue ex - error = ex - end - @deferred.clear - {elapsed, error} + @deferred.each(&.call) end end end From f3afd74dc57deae16aa2207f31127ea4b18b408e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 6 May 2021 22:10:59 -0600 Subject: [PATCH 203/399] Empty classes for reporting --- src/spectator/reporters/broadcast_reporter.cr | 13 +++++++++++++ src/spectator/reporters/reporter.cr | 7 +++++++ 2 files changed, 20 insertions(+) create mode 100644 src/spectator/reporters/broadcast_reporter.cr create mode 100644 src/spectator/reporters/reporter.cr diff --git a/src/spectator/reporters/broadcast_reporter.cr b/src/spectator/reporters/broadcast_reporter.cr new file mode 100644 index 0000000..da4fc36 --- /dev/null +++ b/src/spectator/reporters/broadcast_reporter.cr @@ -0,0 +1,13 @@ +require "./reporter" + +module Spectator::Reporters + # Reports events to multiple other reporters. + # Events received by this reporter will be sent to others. + class BroadcastReporter < Reporter + # Creates the broadcast reporter. + # Takes a collection of reporters to pass events along to. + def initialize(reporters : Enumerable(Reporter)) + @reporters = reporters.to_a + end + end +end diff --git a/src/spectator/reporters/reporter.cr b/src/spectator/reporters/reporter.cr new file mode 100644 index 0000000..a050e0c --- /dev/null +++ b/src/spectator/reporters/reporter.cr @@ -0,0 +1,7 @@ +module Spectator::Reporters + # Base class and interface used to notify systems of events. + # This is typically used for producing output from test results, + # but can also be used to send data to external systems. + abstract class Reporter + end +end From 31d819e4c96e041ff69d82dc16ef582ac5aac357 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 6 May 2021 22:11:38 -0600 Subject: [PATCH 204/399] Nuke formatting types to prep for new types --- src/spectator/formatting/color.cr | 42 ------ src/spectator/formatting/comment.cr | 20 --- .../formatting/document_formatter.cr | 65 --------- src/spectator/formatting/dots_formatter.cr | 60 --------- .../formatting/error_junit_test_case.cr | 21 --- src/spectator/formatting/failure_block.cr | 126 ------------------ src/spectator/formatting/failure_command.cr | 19 --- .../formatting/failure_junit_test_case.cr | 39 ------ .../formatting/finished_junit_test_case.cr | 21 --- src/spectator/formatting/human_time.cr | 41 ------ src/spectator/formatting/indent.cr | 50 ------- src/spectator/formatting/json_formatter.cr | 95 ------------- src/spectator/formatting/junit_formatter.cr | 63 --------- src/spectator/formatting/junit_test_case.cr | 49 ------- src/spectator/formatting/junit_test_suite.cr | 73 ---------- src/spectator/formatting/labeled_text.cr | 15 --- src/spectator/formatting/location_timing.cr | 16 --- .../formatting/match_data_value_pair.cr | 16 --- src/spectator/formatting/match_data_values.cr | 24 ---- src/spectator/formatting/numbered_item.cr | 16 --- src/spectator/formatting/profile_block.cr | 28 ---- src/spectator/formatting/profile_summary.cr | 29 ---- src/spectator/formatting/random_seed_text.cr | 14 -- src/spectator/formatting/remaining_text.cr | 15 --- src/spectator/formatting/runtime.cr | 24 ---- src/spectator/formatting/silent_formatter.cr | 27 ---- .../formatting/skipped_junit_test_case.cr | 26 ---- src/spectator/formatting/stats_counter.cr | 50 ------- .../formatting/successful_junit_test_case.cr | 18 --- src/spectator/formatting/suite_summary.cr | 78 ----------- src/spectator/formatting/tap_formatter.cr | 58 -------- src/spectator/formatting/tap_test_line.cr | 33 ----- 32 files changed, 1271 deletions(-) delete mode 100644 src/spectator/formatting/color.cr delete mode 100644 src/spectator/formatting/comment.cr delete mode 100644 src/spectator/formatting/document_formatter.cr delete mode 100644 src/spectator/formatting/dots_formatter.cr delete mode 100644 src/spectator/formatting/error_junit_test_case.cr delete mode 100644 src/spectator/formatting/failure_block.cr delete mode 100644 src/spectator/formatting/failure_command.cr delete mode 100644 src/spectator/formatting/failure_junit_test_case.cr delete mode 100644 src/spectator/formatting/finished_junit_test_case.cr delete mode 100644 src/spectator/formatting/human_time.cr delete mode 100644 src/spectator/formatting/indent.cr delete mode 100644 src/spectator/formatting/json_formatter.cr delete mode 100644 src/spectator/formatting/junit_formatter.cr delete mode 100644 src/spectator/formatting/junit_test_case.cr delete mode 100644 src/spectator/formatting/junit_test_suite.cr delete mode 100644 src/spectator/formatting/labeled_text.cr delete mode 100644 src/spectator/formatting/location_timing.cr delete mode 100644 src/spectator/formatting/match_data_value_pair.cr delete mode 100644 src/spectator/formatting/match_data_values.cr delete mode 100644 src/spectator/formatting/numbered_item.cr delete mode 100644 src/spectator/formatting/profile_block.cr delete mode 100644 src/spectator/formatting/profile_summary.cr delete mode 100644 src/spectator/formatting/random_seed_text.cr delete mode 100644 src/spectator/formatting/remaining_text.cr delete mode 100644 src/spectator/formatting/runtime.cr delete mode 100644 src/spectator/formatting/silent_formatter.cr delete mode 100644 src/spectator/formatting/skipped_junit_test_case.cr delete mode 100644 src/spectator/formatting/stats_counter.cr delete mode 100644 src/spectator/formatting/successful_junit_test_case.cr delete mode 100644 src/spectator/formatting/suite_summary.cr delete mode 100644 src/spectator/formatting/tap_formatter.cr delete mode 100644 src/spectator/formatting/tap_test_line.cr diff --git a/src/spectator/formatting/color.cr b/src/spectator/formatting/color.cr deleted file mode 100644 index 889f878..0000000 --- a/src/spectator/formatting/color.cr +++ /dev/null @@ -1,42 +0,0 @@ -require "colorize" - -module Spectator::Formatting - # Method for colorizing output. - module Color - extend self - - # Symbols in `Colorize` representing result types and formatting types. - private COLORS = { - success: :green, - failure: :red, - error: :magenta, - pending: :yellow, - comment: :cyan, - } - - # Colorizes some text with the success color. - def pass(text) - text.colorize(COLORS[:success]) - end - - # Colorizes some text with the failure color. - def failure(text) - text.colorize(COLORS[:failure]) - end - - # Colorizes some text with the error color. - def error(text) - text.colorize(COLORS[:error]) - end - - # Colorizes some text with the pending/skipped color. - def pending(text) - text.colorize(COLORS[:pending]) - end - - # Colorizes some text with the comment color. - def comment(text) - text.colorize(COLORS[:comment]) - end - end -end diff --git a/src/spectator/formatting/comment.cr b/src/spectator/formatting/comment.cr deleted file mode 100644 index 6a42685..0000000 --- a/src/spectator/formatting/comment.cr +++ /dev/null @@ -1,20 +0,0 @@ -module Spectator::Formatting - # Produces a stringified comment for output. - private struct Comment(T) - # Creates the comment. - def initialize(@text : T) - end - - # Appends the comment to the output. - def to_s(io) - io << '#' - io << ' ' - io << @text - end - - # Creates a colorized version of the comment. - def self.color(text) - Color.comment(new(text)) - end - end -end diff --git a/src/spectator/formatting/document_formatter.cr b/src/spectator/formatting/document_formatter.cr deleted file mode 100644 index 77cf23a..0000000 --- a/src/spectator/formatting/document_formatter.cr +++ /dev/null @@ -1,65 +0,0 @@ -require "../example_group" -require "./formatter" -require "./suite_summary" - -module Spectator::Formatting - # Produces an indented document-style output. - # Each nested group of examples increases the indent. - # Example names are output in a color based on their result. - class DocumentFormatter < Formatter - include SuiteSummary - - private INDENT = " " - - @previous_hierarchy = [] of ExampleGroup - - # Creates the formatter. - # By default, output is sent to STDOUT. - def initialize(@io : IO = STDOUT) - end - - # Does nothing when an example is started. - def start_example(example) - hierarchy = group_hierarchy(example) - tuple = hierarchy_diff(@previous_hierarchy, hierarchy) - print_sub_hierarchy(*tuple) - @previous_hierarchy = hierarchy - end - - # Produces a single character output based on a result. - def end_example(example) - @previous_hierarchy.size.times { @io.print INDENT } - @io.puts example.result.accept(Color) { example } - end - - # Produces a list of groups making up the hierarchy for an example. - private def group_hierarchy(example) - hierarchy = [] of ExampleGroup - group = example.group - while group.is_a?(ExampleGroup) - hierarchy << group if group.name? - group = group.group? - end - hierarchy.reverse - end - - # Generates a difference between two hierarchies. - private def hierarchy_diff(first, second) - index = -1 - diff = second.skip_while do |group| - index += 1 - first.size > index && first[index] == group - end - {index, diff} - end - - # Displays an indented hierarchy starting partially into the whole hierarchy. - private def print_sub_hierarchy(index, sub_hierarchy) - sub_hierarchy.each do |group| - index.times { @io.print INDENT } - @io.puts group.name - index += 1 - end - end - end -end diff --git a/src/spectator/formatting/dots_formatter.cr b/src/spectator/formatting/dots_formatter.cr deleted file mode 100644 index 15dbbc4..0000000 --- a/src/spectator/formatting/dots_formatter.cr +++ /dev/null @@ -1,60 +0,0 @@ -require "./formatter" -require "./suite_summary" - -module Spectator::Formatting - # Produces a single character for each example. - # A dot is output for each successful example (hence the name). - # Other characters are output for non-successful results. - # At the end of the test suite, a summary of failures and results is displayed. - class DotsFormatter < Formatter - include SuiteSummary - - # Creates the formatter. - # By default, output is sent to STDOUT. - def initialize(@io : IO = STDOUT) - end - - # Does nothing when an example is started. - def start_example(example) - # ... - end - - # Produces a single character output based on a result. - def end_example(example) - @io.print example.result.accept(Character) - end - - # Interface for `Result` to pick a character for output. - private module Character - extend self - - # Characters for each of the result types. - private CHARACTERS = { - success: '.', - failure: 'F', - error: 'E', - pending: '*', - } - - # Character output for a successful example. - def pass - Color.pass(CHARACTERS[:success]) - end - - # Character output for a failed example. - def failure - Color.failure(CHARACTERS[:failure]) - end - - # Character output for an errored example. - def error - Color.error(CHARACTERS[:error]) - end - - # Character output for a pending or skipped example. - def pending - Color.pending(CHARACTERS[:pending]) - end - end - end -end diff --git a/src/spectator/formatting/error_junit_test_case.cr b/src/spectator/formatting/error_junit_test_case.cr deleted file mode 100644 index 41f71d1..0000000 --- a/src/spectator/formatting/error_junit_test_case.cr +++ /dev/null @@ -1,21 +0,0 @@ -require "./failure_junit_test_case" - -module Spectator::Formatting - # JUnit test case for a errored result. - private class ErrorJUnitTestCase < FailureJUnitTestCase - # Result for this test case. - private getter result - - # Creates the JUnit test case. - def initialize(example : Example) - super - @result = example.result.as(ErrorResult) - end - - # Adds the exception to the XML block. - private def content(xml) - xml.element("error", message: @result.error.message, type: @result.error.class) - super - end - end -end diff --git a/src/spectator/formatting/failure_block.cr b/src/spectator/formatting/failure_block.cr deleted file mode 100644 index 4af0c0e..0000000 --- a/src/spectator/formatting/failure_block.cr +++ /dev/null @@ -1,126 +0,0 @@ -module Spectator::Formatting - # Constructs a block of text containing information about a failed example. - # - # A failure block takes the form: - # - # ```text - # 1) Example name - # Failure: Reason or message - # - # Expected: value - # got: value - # - # # spec/source_spec.cr:42 - # ``` - private struct FailureBlock - # Creates the failure block. - # The *index* uniquely identifies the failure in the output. - # The *example* is the failed example. - def initialize(@index : Int32, @example : Example) - end - - # Retrieves the failed result. - private def result - @example.result.as(FailResult) - end - - # Creates the block of text describing the failure. - def to_s(io) - indent = Indent.new(io) - inner_indent = integer_length(@index) + 2 # +2 for ) and space after number. - - indent.increase do - title(indent) - indent.increase(inner_indent) do - content(indent) - location(indent) - end - end - end - - # Produces the title of the failure block. - # The line takes the form: - # ```text - # 1) Example name - # ``` - private def title(indent) - indent.line(NumberedItem.new(@index, @example)) - end - - # Produces the main content of the failure block. - # Any failed expectations are displayed, - # then an error stacktrace if an error occurred. - private def content(indent) - unsatisfied_expectations(indent) - error_stacktrace(indent) if result.is_a?(ErrorResult) - end - - # Produces a list of unsatisfied expectations and their values. - private def unsatisfied_expectations(indent) - result.expectations.reject(&.satisfied?).each do |expectation| - indent.line(Color.failure(LabeledText.new("Failure", expectation.failure_message))) - indent.line - indent.increase do - matcher_values(indent, expectation) - end - indent.line - end - end - - # Produces the values list for an expectation - private def matcher_values(indent, expectation) - MatchDataValues.new(expectation.values).each do |pair| - colored_pair = if expectation.satisfied? - Color.pass(pair) - else - Color.failure(pair) - end - indent.line(colored_pair) - end - end - - # Produces the stack trace for an errored result. - private def error_stacktrace(indent) - error = result.error - first_line = error.message.try(&.lines).try(&.first) - indent.line(Color.error(LabeledText.new("Error", first_line))) - indent.line - indent.increase do - loop do - display_error(indent, error) - if (next_error = error.cause) - error = next_error - else - break - end - end - end - indent.line - end - - # Display a single error and its stacktrace. - private def display_error(indent, error) : Nil - indent.line(Color.error(LabeledText.new(error.class.to_s, error))) - indent.increase do - error.backtrace.each do |frame| - indent.line(Color.error(frame)) - end - end - end - - # Produces the location line of the failure block. - private def location(indent) - indent.line(Comment.color(@example.location)) - end - - # Gets the number of characters a positive integer spans in base 10. - private def integer_length(index) - count = 1 - while index >= 10 - index /= 10 - count += 1 - end - count - end - end -end diff --git a/src/spectator/formatting/failure_command.cr b/src/spectator/formatting/failure_command.cr deleted file mode 100644 index 34f8bea..0000000 --- a/src/spectator/formatting/failure_command.cr +++ /dev/null @@ -1,19 +0,0 @@ -module Spectator::Formatting - # Produces a stringified command to run a failed test. - private struct FailureCommand - # Creates the failure command. - def initialize(@example : Example) - end - - # Appends the command to the output. - def to_s(io) - io << "crystal spec " - io << @example.location - end - - # Colorizes the command instance based on the result. - def self.color(example) - example.result.accept(Color) { new(example) } - end - end -end diff --git a/src/spectator/formatting/failure_junit_test_case.cr b/src/spectator/formatting/failure_junit_test_case.cr deleted file mode 100644 index 37f0823..0000000 --- a/src/spectator/formatting/failure_junit_test_case.cr +++ /dev/null @@ -1,39 +0,0 @@ -require "./finished_junit_test_case" - -module Spectator::Formatting - # JUnit test case for a failed result. - private class FailureJUnitTestCase < FinishedJUnitTestCase - # Result for this test case. - private getter result - - # Creates the JUnit test case. - def initialize(example : Example) - super - @result = example.result.as(FailResult) - end - - # Status string specific to the result type. - private def status : String - "FAIL" - end - - # Adds the failed expectations to the XML block. - private def content(xml) - super - @result.expectations.reject(&.satisfied?).each do |expectation| - xml.element("failure", message: expectation.failure_message) do - expectation_values(expectation.values, xml) - end - end - end - - # Adds the expectation values to the failure block. - private def expectation_values(labeled_values, xml) - labeled_values.each do |entry| - label = entry.first - value = entry.last - xml.text("#{label}: #{value}\n") - end - end - end -end diff --git a/src/spectator/formatting/finished_junit_test_case.cr b/src/spectator/formatting/finished_junit_test_case.cr deleted file mode 100644 index 0db79de..0000000 --- a/src/spectator/formatting/finished_junit_test_case.cr +++ /dev/null @@ -1,21 +0,0 @@ -require "./junit_test_case" - -module Spectator::Formatting - # Commonalities of all test cases that ran (success or failure). - private abstract class FinishedJUnitTestCase < JUnitTestCase - # Produces the test case XML element. - def to_xml(xml : ::XML::Builder) - xml.element("testcase", **full_attributes) do - content(xml) - end - end - - # Attributes that go in the "testcase" XML element. - private def full_attributes - attributes.merge( - time: result.elapsed.total_seconds, - assertions: result.expectations.size - ) - end - end -end diff --git a/src/spectator/formatting/human_time.cr b/src/spectator/formatting/human_time.cr deleted file mode 100644 index c7ec687..0000000 --- a/src/spectator/formatting/human_time.cr +++ /dev/null @@ -1,41 +0,0 @@ -module Spectator::Formatting - # Provides a more human-friendly formatting for a time span. - # This produces a string with the minimum of - # microseconds, milliseconds, seconds, minutes, hours, or days. - private struct HumanTime - @string : String - - # Creates the wrapper - def initialize(span) - @string = simplify(span) - end - - # Produces the human-friendly string for a time span. - def to_s(io) - io << @string - end - - # Does the actual work of converting a time span to string. - private def simplify(span) - millis = span.total_milliseconds - return "#{(millis * 1000).round.to_i} microseconds" if millis < 1 - - seconds = span.total_seconds - return "#{millis.round(2)} milliseconds" if seconds < 1 - return "#{seconds.round(2)} seconds" if seconds < 60 - - int_seconds = seconds.to_i - minutes = int_seconds // 60 - int_seconds %= 60 - return sprintf("%i:%02i", minutes, int_seconds) if minutes < 60 - - hours = minutes // 60 - minutes %= 60 - return sprintf("%i:%02i:%02i", hours, minutes, int_seconds) if hours < 24 - - days = hours // 24 - hours %= 24 - sprintf("%i days %i:%02i:%02i", days, hours, minutes, int_seconds) - end - end -end diff --git a/src/spectator/formatting/indent.cr b/src/spectator/formatting/indent.cr deleted file mode 100644 index 103747b..0000000 --- a/src/spectator/formatting/indent.cr +++ /dev/null @@ -1,50 +0,0 @@ -module Spectator::Formatting - # Tracks indentation for text output. - # To use, create an instance and call `#increase` when a block should be indented. - # The `#increase` method yields, so additional `#increase` and `#line` methods can be called. - # Then call `#line` to produce a line of text at the current indent. - # ``` - # indent = Indent.new(io) - # indent.increase do - # indent.line("Text") - # indent.increase do - # indent.line("More text") - # end - # end - # ``` - private struct Indent - # Default number of spaces to indent by. - INDENT_SIZE = 2 - - # Creates the identation tracker. - # The *io* is the stream to output to. - # The *indent_size* is how much (number of spaces) to indent at each level. - # The *initial_indent* is what the ident should be set to. - def initialize(@io : IO, @indent_size = INDENT_SIZE, inital_indent @indent = 0) - end - - # Indents the text and yields. - def increase(&block) - increase(@indent_size, &block) - end - - # Indents the text by a specified amount and yields. - def increase(amount) : Nil - @indent += amount - yield - ensure - @indent -= amount - end - - # Produces an empty line. - def line - @io.puts - end - - # Produces a line of indented text. - def line(text) - @indent.times { @io << ' ' } - @io.puts text - end - end -end diff --git a/src/spectator/formatting/json_formatter.cr b/src/spectator/formatting/json_formatter.cr deleted file mode 100644 index 36fd746..0000000 --- a/src/spectator/formatting/json_formatter.cr +++ /dev/null @@ -1,95 +0,0 @@ -require "json" -require "./formatter" - -module Spectator::Formatting - # Produces a JSON document containing the test results. - class JsonFormatter < Formatter - # Creates the formatter. - # By default, output is sent to STDOUT. - def initialize(io : IO = STDOUT) - @json = ::JSON::Builder.new(io) - end - - # Called when a test suite is starting to execute. - def start_suite(suite : TestSuite) - @json.start_document - @json.start_object - @json.string("examples") - @json.start_array - end - - # Called when a test suite finishes. - # The results from the entire suite are provided. - # The *profile* value is not nil when profiling results should be displayed. - def end_suite(report : Report, profile : Profile?) - @json.end_array # examples - totals(report) - timing(report) - profile(profile) if profile - @json.field("result", report.failed? ? "fail" : "success") - @json.end_object - end - - # Called before a test starts. - def start_example(example : Example) - end - - # Called when a test finishes. - # The result of the test is provided. - def end_example(example : Example) - example.result.to_json(@json, example) - end - - # Adds the totals section of the document. - private def totals(report) - @json.field("totals") do - @json.object do - @json.field("examples", report.example_count) - @json.field("success", report.successful_count) - @json.field("fail", report.failed_count) - @json.field("error", report.error_count) - @json.field("pending", report.pending_count) - @json.field("remaining", report.remaining_count) - end - end - end - - # Adds the timings section of the document. - private def timing(report) - @json.field("timing") do - @json.object do - @json.field("runtime", report.runtime.total_seconds) - @json.field("examples", report.example_runtime.total_seconds) - @json.field("overhead", report.overhead_time.total_seconds) - end - end - end - - # Adds the profile information to the document. - private def profile(profile) - @json.field("profile") do - @json.object do - @json.field("count", profile.size) - @json.field("time", profile.total_time.total_seconds) - @json.field("percentage", profile.percentage) - @json.field("results") do - @json.array do - profile.each do |result| - profile_entry(result) - end - end - end - end - end - end - - # Adds a profile entry to the document. - private def profile_entry(example) - @json.object do - @json.field("example", example) - @json.field("time", example.result.elapsed.total_seconds) - @json.field("location", example.location) - end - end - end -end diff --git a/src/spectator/formatting/junit_formatter.cr b/src/spectator/formatting/junit_formatter.cr deleted file mode 100644 index c523072..0000000 --- a/src/spectator/formatting/junit_formatter.cr +++ /dev/null @@ -1,63 +0,0 @@ -require "xml" - -module Spectator::Formatting - # Formatter for producing a JUnit XML report. - class JUnitFormatter < Formatter - # Name of the JUnit output file. - private JUNIT_XML_FILE = "output.xml" - - # Name of the top-level test suites block. - private NAME = "Spec" - - # Creates the formatter. - # By default, output is sent to STDOUT. - def initialize(output_dir : String) - path = File.join(output_dir, JUNIT_XML_FILE) - @io = File.open(path, "w") - @xml = XML::Builder.new(@io) - end - - # Called when a test suite is starting to execute. - def start_suite(suite : TestSuite) - @xml.start_document(encoding: "UTF-8") - end - - # Called when a test suite finishes. - # The results from the entire suite are provided. - # The *profile* value does nothing for this formatter. - def end_suite(report : Report, profile : Profile?) - test_suites_block(report) - @xml.end_document - @xml.flush - @io.close - end - - # Called before a test starts. - def start_example(example : Example) - end - - # Called when a test finishes. - # The result of the test is provided by *example*. - def end_example(example : Example) - end - - # Creates the "testsuites" block in the XML. - private def test_suites_block(report) - @xml.element("testsuites", - tests: report.example_count, - failures: report.failed_count, - errors: report.error_count, - time: report.runtime.total_seconds, - name: NAME) do - add_test_suites(report) - end - end - - # Adds all of the individual test suite blocks. - private def add_test_suites(report) - report.group_by(&.location.path).each do |path, examples| - JUnitTestSuite.new(path, examples).to_xml(@xml) - end - end - end -end diff --git a/src/spectator/formatting/junit_test_case.cr b/src/spectator/formatting/junit_test_case.cr deleted file mode 100644 index 223942a..0000000 --- a/src/spectator/formatting/junit_test_case.cr +++ /dev/null @@ -1,49 +0,0 @@ -module Spectator::Formatting - # Base type for all JUnit test case results. - private abstract class JUnitTestCase - # Creates the JUnit test case. - def initialize(@example : Example) - end - - # Produces the test case XML element. - def to_xml(xml : ::XML::Builder) - xml.element("testcase", **attributes) do - content(xml) - end - end - - # Attributes that go in the "testcase" XML element. - private def attributes - { - name: example, - status: status, - classname: classname, - } - end - - # Result to pull values from. - private abstract def result - - # Status string specific to the result type. - private abstract def status : String - - # Example for this test case. - private getter example : Example - - # Adds additional content to the "testcase" XML block. - # Override this to add more content. - private def content(xml) - # ... - end - - # Java-ified class name created from the spec. - private def classname - path = example.location.path - file = File.basename(path) - ext = File.extname(file) - name = file[0...-(ext.size)] - dir = path[0...-(file.size + 1)] - {dir.gsub('/', '.').underscore, name.camelcase}.join('.') - end - end -end diff --git a/src/spectator/formatting/junit_test_suite.cr b/src/spectator/formatting/junit_test_suite.cr deleted file mode 100644 index 73e4e34..0000000 --- a/src/spectator/formatting/junit_test_suite.cr +++ /dev/null @@ -1,73 +0,0 @@ -module Spectator::Formatting - # Mapping of a single spec file into a JUnit test suite. - private struct JUnitTestSuite - # Creates the JUnit test suite. - # The *path* should be the file that all results are from. - # The *examples* is a subset of all examples that share the path. - def initialize(@path : String, examples : Array(Example)) - @report = Report.new(examples) - end - - # Generates the XML for the test suite (and all nested test cases). - def to_xml(xml : ::XML::Builder) - xml.element("testsuite", - tests: @report.example_count, - failures: @report.failed_count, - errors: @report.error_count, - skipped: @report.pending_count, - time: @report.runtime.total_seconds, - name: name, - package: package) do - add_test_cases(xml) - end - end - - # Adds the test case elements to the XML. - private def add_test_cases(xml) - @report.each do |example| - test_case = example.result.accept(JUnitTestCaseSelector) { example } - test_case.to_xml(xml) - end - end - - # Java-ified name of the test suite. - private def name - file = File.basename(@path) - ext = File.extname(file) - name = file[0...-(ext.size)] - name.camelcase - end - - # Java-ified package (path) of the test suite. - private def package - file = File.basename(@path) - dir = @path[0...-(file.size + 1)] - dir.gsub('/', '.').underscore - end - - # Selector for creating a JUnit test case based on a result. - private module JUnitTestCaseSelector - extend self - - # Creates a successful JUnit test case. - def pass(example) - SuccessfulJUnitTestCase.new(example) - end - - # Creates a failure JUnit test case. - def failure(example) - FailureJUnitTestCase.new(example) - end - - # Creates an error JUnit test case. - def error(example) - ErrorJUnitTestCase.new(example) - end - - # Creates a skipped JUnit test case. - def pending(example) - SkippedJUnitTestCase.new(example) - end - end - end -end diff --git a/src/spectator/formatting/labeled_text.cr b/src/spectator/formatting/labeled_text.cr deleted file mode 100644 index b99152e..0000000 --- a/src/spectator/formatting/labeled_text.cr +++ /dev/null @@ -1,15 +0,0 @@ -module Spectator::Formatting - # Produces a stringified message with a prefix. - private struct LabeledText(T) - # Creates the labeled text. - def initialize(@label : String, @text : T) - end - - # Appends the message to the output. - def to_s(io) - io << @label - io << ": " - io << @text - end - end -end diff --git a/src/spectator/formatting/location_timing.cr b/src/spectator/formatting/location_timing.cr deleted file mode 100644 index 7e2c8a8..0000000 --- a/src/spectator/formatting/location_timing.cr +++ /dev/null @@ -1,16 +0,0 @@ -module Spectator::Formatting - # Produces the timing line in a profile block. - # This contains the length of time, and the example's location. - private struct LocationTiming - # Creates the location timing line. - def initialize(@span : Time::Span, @location : Location) - end - - # Appends the location timing information to the output. - def to_s(io) - io << HumanTime.new(@span).colorize.bold - io << ' ' - io << @location - end - end -end diff --git a/src/spectator/formatting/match_data_value_pair.cr b/src/spectator/formatting/match_data_value_pair.cr deleted file mode 100644 index dd7f255..0000000 --- a/src/spectator/formatting/match_data_value_pair.cr +++ /dev/null @@ -1,16 +0,0 @@ -module Spectator::Formatting - # A single labeled value from the `Spectator::Matchers::MatchData#value` method. - private struct MatchDataValuePair - # Creates the pair formatter. - def initialize(@key : Symbol, @value : String, @padding : Int32) - end - - # Appends the pair to the output. - def to_s(io) - @padding.times { io << ' ' } - io << @key - io << ": " - io << @value - end - end -end diff --git a/src/spectator/formatting/match_data_values.cr b/src/spectator/formatting/match_data_values.cr deleted file mode 100644 index bf194a9..0000000 --- a/src/spectator/formatting/match_data_values.cr +++ /dev/null @@ -1,24 +0,0 @@ -module Spectator::Formatting - # Produces a `MatchDataValuePair` for each key-value pair - # from `Spectator::Matchers::MatchData#values`. - private struct MatchDataValues - include Enumerable(Tuple(Symbol, String)) - - @max_key_length : Int32 - - # Creates the values mapper. - def initialize(@values : Array(Tuple(Symbol, String))) - @max_key_length = @values.map(&.first.to_s.size).max - end - - # Yields pairs that can be printed to output. - def each - @values.each do |labeled_value| - key = labeled_value.first - key_length = key.to_s.size - padding = @max_key_length - key_length - yield MatchDataValuePair.new(key, labeled_value.last, padding) - end - end - end -end diff --git a/src/spectator/formatting/numbered_item.cr b/src/spectator/formatting/numbered_item.cr deleted file mode 100644 index 15afd16..0000000 --- a/src/spectator/formatting/numbered_item.cr +++ /dev/null @@ -1,16 +0,0 @@ -module Spectator::Formatting - # Produces a stringified value with a numerical prefix. - private struct NumberedItem(T) - # Creates the numbered item. - def initialize(@number : Int32, @text : T) - end - - # Appends the numbered item to the output. - def to_s(io) - io << @number - io << ')' - io << ' ' - io << @text - end - end -end diff --git a/src/spectator/formatting/profile_block.cr b/src/spectator/formatting/profile_block.cr deleted file mode 100644 index 319705d..0000000 --- a/src/spectator/formatting/profile_block.cr +++ /dev/null @@ -1,28 +0,0 @@ -module Spectator::Formatting - # Contents of a profile block. - private struct ProfileBlock - # Creates the block. - def initialize(@profile : Profile) - end - - # Appends the block to the output. - def to_s(io) - io.puts(ProfileSummary.new(@profile)) - - indent = Indent.new(io) - indent.increase do - @profile.each do |example| - entry(indent, example) - end - end - end - - # Adds a result entry to the output. - private def entry(indent, example) - indent.line(example) - indent.increase do - indent.line(LocationTiming.new(example.result.elapsed, example.location)) - end - end - end -end diff --git a/src/spectator/formatting/profile_summary.cr b/src/spectator/formatting/profile_summary.cr deleted file mode 100644 index 9d6a05c..0000000 --- a/src/spectator/formatting/profile_summary.cr +++ /dev/null @@ -1,29 +0,0 @@ -module Spectator::Formatting - # Top line of a profile block which gives a summary. - private struct ProfileSummary - # Creates the summary line. - def initialize(@profile : Profile) - end - - # Appends the summary to the output. - def to_s(io) - io << "Top " - io << @profile.size - io << " slowest examples (" - io << human_time - io << ", " - io.printf("%.2f", percentage) - io << "% of total time):" - end - - # Creates a human-friendly string for the total time. - private def human_time - HumanTime.new(@profile.total_time) - end - - # Percentage (0 to 100) of total time. - private def percentage - @profile.percentage * 100 - end - end -end diff --git a/src/spectator/formatting/random_seed_text.cr b/src/spectator/formatting/random_seed_text.cr deleted file mode 100644 index 998fed1..0000000 --- a/src/spectator/formatting/random_seed_text.cr +++ /dev/null @@ -1,14 +0,0 @@ -module Spectator::Formatting - # Text displayed when using a random seed. - private struct RandomSeedText - # Creates the text object. - def initialize(@seed : UInt64) - end - - # Appends the command to the output. - def to_s(io) - io << "Randomized with seed " - io << @seed - end - end -end diff --git a/src/spectator/formatting/remaining_text.cr b/src/spectator/formatting/remaining_text.cr deleted file mode 100644 index b3639ec..0000000 --- a/src/spectator/formatting/remaining_text.cr +++ /dev/null @@ -1,15 +0,0 @@ -module Spectator::Formatting - # Text displayed when fail-fast is enabled and tests were skipped. - private struct RemainingText - # Creates the text object. - def initialize(@count : Int32) - end - - # Appends the command to the output. - def to_s(io) - io << "Text execution aborted (fail-fast) - " - io << @count - io << " examples were omitted." - end - end -end diff --git a/src/spectator/formatting/runtime.cr b/src/spectator/formatting/runtime.cr deleted file mode 100644 index 95541e3..0000000 --- a/src/spectator/formatting/runtime.cr +++ /dev/null @@ -1,24 +0,0 @@ -module Spectator::Formatting - # Produces a stringified time span for the runtime. - private struct Runtime - # Creates the runtime instance. - def initialize(@runtime : Time::Span) - end - - # Appends the runtime to the output. - # The text will be formatted as follows, - # depending on the length of time: - # ```text - # Finished in ## microseconds - # Finished in ## milliseconds - # Finished in ## seconds - # Finished in #:## - # Finished in #:##:## - # Finished in # days #:##:## - # ``` - def to_s(io) - io << "Finished in " - io << HumanTime.new(@runtime) - end - end -end diff --git a/src/spectator/formatting/silent_formatter.cr b/src/spectator/formatting/silent_formatter.cr deleted file mode 100644 index 1556534..0000000 --- a/src/spectator/formatting/silent_formatter.cr +++ /dev/null @@ -1,27 +0,0 @@ -module Spectator::Formatting - # Formatter that outputs nothing. - # Useful for testing and larger automated processes. - class SilentFormatter < Formatter - # Called when a test suite is starting to execute. - def start_suite(suite : TestSuite) - # ... crickets ... - end - - # Called when a test suite finishes. - # The results from the entire suite are provided. - def end_suite(report : Report, profile : Profile?) - # ... crickets ... - end - - # Called before a test starts. - def start_example(example : Example) - # ... crickets ... - end - - # Called when a test finishes. - # The result of the test is provided by *example*. - def end_example(example : Example) - # ... crickets ... - end - end -end diff --git a/src/spectator/formatting/skipped_junit_test_case.cr b/src/spectator/formatting/skipped_junit_test_case.cr deleted file mode 100644 index 0c0c383..0000000 --- a/src/spectator/formatting/skipped_junit_test_case.cr +++ /dev/null @@ -1,26 +0,0 @@ -require "./junit_test_case" - -module Spectator::Formatting - # JUnit test case for a pending result. - private class SkippedJUnitTestCase < JUnitTestCase - # Result for this test case. - private getter result - - # Creates the JUnit test case. - def initialize(example : Example) - super - @result = example.result.as(PendingResult) - end - - # Status string specific to the result type. - private def status : String - "TODO" - end - - # Adds the skipped tag to the XML block. - private def content(xml) - super - xml.element("skipped") - end - end -end diff --git a/src/spectator/formatting/stats_counter.cr b/src/spectator/formatting/stats_counter.cr deleted file mode 100644 index fa0e697..0000000 --- a/src/spectator/formatting/stats_counter.cr +++ /dev/null @@ -1,50 +0,0 @@ -module Spectator::Formatting - # Produces a stringified stats counter from result totals. - private struct StatsCounter - # Creates the instance with each of the counters. - private def initialize(@examples : Int32, @failures : Int32, @errors : Int32, @pending : Int32, @failed : Bool) - end - - # Creates the instance from the counters in a report. - def initialize(report) - initialize(report.example_count, report.failed_count, report.error_count, report.pending_count, report.failed?) - end - - # Produces a colorized formatting for the stats, - # depending on the number of each type of result. - def color - if @errors > 0 - Color.error(self) - elsif @failed || @failures > 0 - Color.failure(self) - elsif @pending > 0 - Color.pending(self) - else - Color.pass(self) - end - end - - # Appends the counters to the output. - # The format will be: - # ```text - # # examples, # failures, # errors, # pending - # ``` - def to_s(io) - stats.each_with_index do |stat, value, index| - io << ", " if index > 0 - io << value - io << ' ' - io << stat - end - end - - private def stats - { - examples: @examples, - failures: @failures, - errors: @errors, - pending: @pending, - } - end - end -end diff --git a/src/spectator/formatting/successful_junit_test_case.cr b/src/spectator/formatting/successful_junit_test_case.cr deleted file mode 100644 index cb4e85f..0000000 --- a/src/spectator/formatting/successful_junit_test_case.cr +++ /dev/null @@ -1,18 +0,0 @@ -module Spectator::Formatting - # JUnit test case for a successful result. - private class SuccessfulJUnitTestCase < FinishedJUnitTestCase - # Result for this test case. - private getter result - - # Creates the JUnit test case. - def initialize(example : Example) - super - @result = example.result.as(PassResult) - end - - # Status string specific to the result type. - private def status : String - "PASS" - end - end -end diff --git a/src/spectator/formatting/suite_summary.cr b/src/spectator/formatting/suite_summary.cr deleted file mode 100644 index cf733f5..0000000 --- a/src/spectator/formatting/suite_summary.cr +++ /dev/null @@ -1,78 +0,0 @@ -module Spectator::Formatting - # Mix-in for producing a human-readable summary of a test suite. - module SuiteSummary - # Does nothing when starting a test suite. - def start_suite(suite) - # ... - end - - # Produces the summary of test suite from a report. - # A block describing each failure is displayed. - # At the end, the totals and runtime are printed. - # The *profile* value is not nil when profiling results should be displayed. - def end_suite(report, profile : Profile?) - if report.example_count > 0 - @io.puts if is_a?(DotsFormatter) - @io.puts - end - failures(report.failures) if report.failed_count > 0 - profile(profile) if profile - stats(report) - remaining(report) if report.remaining? - if report.failed? - if report.examples_ran > 0 - failure_commands(report.failures) - else - @io.puts Color.failure("Failing because no tests were run (fail-blank)") - end - end - end - - # Produces the failure section of the summary. - # This has a "Failures" title followed by a block for each failure. - private def failures(failures) - @io.puts "Failures:" - @io.puts - failures.each_with_index do |example, index| - @io.puts FailureBlock.new(index + 1, example) - end - end - - # Produces the profiling section of the summary. - private def profile(profile) - @io.puts ProfileBlock.new(profile) - end - - # Produces the statistical section of the summary. - # This contains how long the suite took to run - # and the counts for the results (total, failures, errors, and pending). - private def stats(report) - @io.puts Runtime.new(report.runtime) - @io.puts StatsCounter.new(report).color - if (seed = report.random_seed?) - @io.puts - @io.puts RandomSeedText.new(seed) - end - end - - # Produces the skipped tests text if fail-fast is enabled and tests were omitted. - private def remaining(report) - text = RemainingText.new(report.remaining_count) - @io.puts Color.failure(text) - end - - # Produces the failure commands section of the summary. - # This provides a set of commands the user can run - # to test just the examples that failed. - private def failure_commands(failures) - @io.puts - @io.puts "Failed examples:" - @io.puts - failures.each do |example| - @io << FailureCommand.color(example) - @io << ' ' - @io.puts Comment.color(example) - end - end - end -end diff --git a/src/spectator/formatting/tap_formatter.cr b/src/spectator/formatting/tap_formatter.cr deleted file mode 100644 index 2c7c810..0000000 --- a/src/spectator/formatting/tap_formatter.cr +++ /dev/null @@ -1,58 +0,0 @@ -module Spectator::Formatting - # Formatter for the "Test Anything Protocol". - # For details, see: https://testanything.org/ - class TAPFormatter < Formatter - # Creates the formatter. - # By default, output is sent to STDOUT. - def initialize(@io : IO = STDOUT) - @index = 1 - end - - # Called when a test suite is starting to execute. - def start_suite(suite : TestSuite) - @io << "1.." - @io.puts suite.size - end - - # Called when a test suite finishes. - # The results from the entire suite are provided. - # The *profile* value is not nil when profiling results should be displayed. - def end_suite(report : Report, profile : Profile?) - @io.puts "Bail out!" if report.remaining? - profile(profile) if profile - end - - # Called before a test starts. - def start_example(example : Example) - end - - # Called when a test finishes. - # The result of the test is provided by *example*. - def end_example(example : Example) - @io.puts TAPTestLine.new(@index, example) - @index += 1 - end - - # Displays profiling information. - private def profile(profile) - @io.puts(Comment.new(ProfileSummary.new(profile))) - - indent = Indent.new(@io) - indent.increase do - profile.each do |example| - profile_entry(indent, example) - end - end - end - - # Adds a profile result entry to the output. - private def profile_entry(indent, example) - @io << "# " - indent.line(example) - indent.increase do - @io << "# " - indent.line(LocationTiming.new(example.result.elapsed, example.location)) - end - end - end -end diff --git a/src/spectator/formatting/tap_test_line.cr b/src/spectator/formatting/tap_test_line.cr deleted file mode 100644 index dd55523..0000000 --- a/src/spectator/formatting/tap_test_line.cr +++ /dev/null @@ -1,33 +0,0 @@ -module Spectator::Formatting - # Produces a formatted TAP test line. - private struct TAPTestLine - # Creates the test line. - def initialize(@index : Int32, @example : Example) - end - - # Appends the line to the output. - def to_s(io) - io << status - io << ' ' - io << @index - io << " - " - io << @example - io << " # skip" if pending? - end - - # The text "ok" or "not ok" depending on the result. - private def status - result.is_a?(FailResult) ? "not ok" : "ok" - end - - # The result of running the example. - private def result - @example.result - end - - # Indicates whether this test was skipped. - private def pending? - result.is_a?(PendingResult) - end - end -end From d7bc3764292c34274f0cf1355570f43f00825f9d Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 7 May 2021 20:05:00 -0600 Subject: [PATCH 205/399] Some initial work on formatters --- src/spectator/formatting/formatter.cr | 29 ++++--------------- .../formatting/progress_formatter.cr | 16 ++++++++++ 2 files changed, 21 insertions(+), 24 deletions(-) create mode 100644 src/spectator/formatting/progress_formatter.cr diff --git a/src/spectator/formatting/formatter.cr b/src/spectator/formatting/formatter.cr index ac37fef..784a2ff 100644 --- a/src/spectator/formatting/formatter.cr +++ b/src/spectator/formatting/formatter.cr @@ -1,27 +1,8 @@ +require "../reporters" + module Spectator::Formatting - # Interface for reporting test progress and results. - # - # The methods should be called in this order: - # 1. `#start_suite` - # 2. `#start_example` - # 3. `#end_example` - # 4. `#end_suite` - # - # Steps 2 and 3 are called for each example in the suite. - abstract class Formatter - # Called when a test suite is starting to execute. - abstract def start_suite(suite : TestSuite) - - # Called when a test suite finishes. - # The results from the entire suite are provided. - # The *profile* value is not nil when profiling results should be displayed. - abstract def end_suite(report : Report, profile : Profile?) - - # Called before a test starts. - abstract def start_example(example : Example) - - # Called when a test finishes. - # The result of the test is available through *example*. - abstract def end_example(example : Example) + abstract class Formatter < Reporters::Reporter + def initialize(@output : IO) + end end end diff --git a/src/spectator/formatting/progress_formatter.cr b/src/spectator/formatting/progress_formatter.cr new file mode 100644 index 0000000..dbb0f6e --- /dev/null +++ b/src/spectator/formatting/progress_formatter.cr @@ -0,0 +1,16 @@ +require "./formatter" + +module Spectator::Formatting + # Output formatter that produces a single character for each test as it completes. + # A '.' indicates a pass, 'F' a failure, and '*' a skipped or pending test. + class ProgressFormatter < Formatter + def pass(notification) + end + + def fail(notification) + end + + def pending(notification) + end + end +end From 6bea36d8b64fc743b6f3e989d988d06290b6be8e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 7 May 2021 20:09:33 -0600 Subject: [PATCH 206/399] Move Node out of Spec namespace --- src/spectator/dsl/examples.cr | 2 +- src/spectator/dsl/groups.cr | 2 +- src/spectator/example.cr | 4 +- src/spectator/example_group.cr | 14 +++--- src/spectator/example_iterator.cr | 8 +-- src/spectator/node.cr | 81 ++++++++++++++++++++++++++++++ src/spectator/spec/node.cr | 83 ------------------------------- 7 files changed, 96 insertions(+), 98 deletions(-) create mode 100644 src/spectator/node.cr delete mode 100644 src/spectator/spec/node.cr diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 5d8b88b..9cc9c85 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -65,7 +65,7 @@ module Spectator::DSL # Inserts the correct representation of a example's name. # If *what* is a string, then it is dropped in as-is. # For anything else, it is stringified. - # This is intended to be used to convert a description from the spec DSL to `Spec::Node#name`. + # This is intended to be used to convert a description from the spec DSL to `Node#name`. private macro _spectator_example_name(what) {% if what.is_a?(StringLiteral) || what.is_a?(StringInterpolation) || diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 0fd2028..fa78ade 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -56,7 +56,7 @@ module Spectator::DSL # If *what* appears to be a type name, it will be symbolized. # If it's a string, then it is dropped in as-is. # For anything else, it is stringified. - # This is intended to be used to convert a description from the spec DSL to `Spec::Node#name`. + # This is intended to be used to convert a description from the spec DSL to `Node#name`. private macro _spectator_group_name(what) {% if (what.is_a?(Generic) || what.is_a?(Path) || diff --git a/src/spectator/example.cr b/src/spectator/example.cr index c210d2b..2ec9ea0 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -2,14 +2,14 @@ require "./example_context_delegate" require "./example_group" require "./harness" require "./location" +require "./node" require "./pending_result" require "./result" -require "./spec/node" require "./tags" module Spectator # Standard example that runs a test case. - class Example < Spec::Node + class Example < Node # Currently running example. class_getter! current : Example diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index c5f00c6..29e9b46 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -1,15 +1,15 @@ require "./events" require "./example_procsy_hook" -require "./spec/node" +require "./node" module Spectator # Collection of examples and sub-groups. - class ExampleGroup < Spec::Node - include Enumerable(Spec::Node) + class ExampleGroup < Node + include Enumerable(Node) include Events - include Iterable(Spec::Node) + include Iterable(Node) - @nodes = [] of Spec::Node + @nodes = [] of Node group_event before_all do |hooks| Log.trace { "Processing before_all hooks for #{self}" } @@ -65,7 +65,7 @@ module Spectator # Removes the specified *node* from the group. # The node will be unassigned from this group. - def delete(node : Spec::Node) + def delete(node : Node) # Only remove from the group if it is associated with this group. return unless node.group == self @@ -92,7 +92,7 @@ module Spectator # Assigns the node to this group. # If the node already belongs to a group, # it will be removed from the previous group before adding it to this group. - def <<(node : Spec::Node) + def <<(node : Node) # Remove from existing group if the node is part of one. if (previous = node.group?) previous.delete(node) diff --git a/src/spectator/example_iterator.cr b/src/spectator/example_iterator.cr index abcfd33..f28a8bf 100644 --- a/src/spectator/example_iterator.cr +++ b/src/spectator/example_iterator.cr @@ -1,6 +1,6 @@ require "./example" require "./example_group" -require "./spec/node" +require "./node" module Spectator # Iterates through all examples in a group and its nested groups. @@ -9,12 +9,12 @@ module Spectator # Stack that contains the iterators for each group. # A stack is used to track where in the tree this iterator is. - @stack : Array(Iterator(Spec::Node)) + @stack : Array(Iterator(Node)) # Creates a new iterator. # The *group* is the example group to iterate through. def initialize(@group : ExampleGroup) - iter = @group.each.as(Iterator(Spec::Node)) + iter = @group.each.as(Iterator(Node)) @stack = [iter] end @@ -39,7 +39,7 @@ module Spectator # Restart the iterator at the beginning. def rewind # Same code as `#initialize`, but return self. - iter = @group.each.as(Iterator(Spec::Node)) + iter = @group.each.as(Iterator(Node)) @stack = [iter] self end diff --git a/src/spectator/node.cr b/src/spectator/node.cr new file mode 100644 index 0000000..587af76 --- /dev/null +++ b/src/spectator/node.cr @@ -0,0 +1,81 @@ +require "./label" +require "./location" +require "./tags" + +module Spectator + # A single item in a test spec. + # This is commonly an `Example` or `ExampleGroup`, + # but can be anything that should be iterated over when running the spec. + abstract class Node + # Location of the node in source code. + getter! location : Location + + # User-provided name or description of the node. + # This does not include the group name or descriptions. + # Use `#to_s` to get the full name. + # + # This value will be nil if no name was provided. + # In this case, and the node is a runnable example, + # the name should be set to the description + # of the first matcher that runs in the test case. + # + # If this value is a `Symbol`, the user specified a type for the name. + getter! name : Label + + # Updates the name of the node. + protected def name=(@name : String) + end + + # Group the node belongs to. + getter! group : ExampleGroup + + # User-defined keywords used for filtering and behavior modification. + getter tags : Tags + + # Assigns the node to the specified *group*. + # This is an internal method and should only be called from `ExampleGroup`. + # `ExampleGroup` manages the association of nodes to groups. + protected setter group : ExampleGroup? + + # Creates the node. + # The *name* describes the purpose of the node. + # It can be a `Symbol` to describe a type. + # The *location* tracks where the node exists in source code. + # The node will be assigned to *group* if it is provided. + # A set of *tags* can be used for filtering and modifying example behavior. + def initialize(@name : Label = nil, @location : Location? = nil, + group : ExampleGroup? = nil, @tags : Tags = Tags.new) + # Ensure group is linked. + group << self if group + end + + # Indicates whether the node has completed. + abstract def finished? : Bool + + # Checks if the node has been marked as pending. + # Pending items should be skipped during execution. + def pending? + tags.includes?(:pending) + end + + # Constructs the full name or description of the node. + # This prepends names of groups this node is part of. + def to_s(io) + name = @name + + # Prefix with group's full name if the node belongs to a group. + if (group = @group) + group.to_s(io) + + # Add padding between the node names + # only if the names don't appear to be symbolic. + # Skip blank group names (like the root group). + io << ' ' unless !group.name? || # ameba:disable Style/NegatedConditionsInUnless + (group.name?.is_a?(Symbol) && name.is_a?(String) && + (name.starts_with?('#') || name.starts_with?('.'))) + end + + name.to_s(io) + end + end +end diff --git a/src/spectator/spec/node.cr b/src/spectator/spec/node.cr deleted file mode 100644 index 4bbde0c..0000000 --- a/src/spectator/spec/node.cr +++ /dev/null @@ -1,83 +0,0 @@ -require "../label" -require "../location" -require "../tags" - -module Spectator - class Spec - # A single item in a test spec. - # This is commonly an `Example` or `ExampleGroup`, - # but can be anything that should be iterated over when running the spec. - abstract class Node - # Location of the node in source code. - getter! location : Location - - # User-provided name or description of the node. - # This does not include the group name or descriptions. - # Use `#to_s` to get the full name. - # - # This value will be nil if no name was provided. - # In this case, and the node is a runnable example, - # the name should be set to the description - # of the first matcher that runs in the test case. - # - # If this value is a `Symbol`, the user specified a type for the name. - getter! name : Label - - # Updates the name of the node. - protected def name=(@name : String) - end - - # Group the node belongs to. - getter! group : ExampleGroup - - # User-defined keywords used for filtering and behavior modification. - getter tags : Tags - - # Assigns the node to the specified *group*. - # This is an internal method and should only be called from `ExampleGroup`. - # `ExampleGroup` manages the association of nodes to groups. - protected setter group : ExampleGroup? - - # Creates the node. - # The *name* describes the purpose of the node. - # It can be a `Symbol` to describe a type. - # The *location* tracks where the node exists in source code. - # The node will be assigned to *group* if it is provided. - # A set of *tags* can be used for filtering and modifying example behavior. - def initialize(@name : Label = nil, @location : Location? = nil, - group : ExampleGroup? = nil, @tags : Tags = Tags.new) - # Ensure group is linked. - group << self if group - end - - # Indicates whether the node has completed. - abstract def finished? : Bool - - # Checks if the node has been marked as pending. - # Pending items should be skipped during execution. - def pending? - tags.includes?(:pending) - end - - # Constructs the full name or description of the node. - # This prepends names of groups this node is part of. - def to_s(io) - name = @name - - # Prefix with group's full name if the node belongs to a group. - if (group = @group) - group.to_s(io) - - # Add padding between the node names - # only if the names don't appear to be symbolic. - # Skip blank group names (like the root group). - io << ' ' unless !group.name? || # ameba:disable Style/NegatedConditionsInUnless - (group.name?.is_a?(Symbol) && name.is_a?(String) && - (name.starts_with?('#') || name.starts_with?('.'))) - end - - name.to_s(io) - end - end - end -end From e47e625016e6e67193feaab78e902346960808be Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 7 May 2021 21:04:17 -0600 Subject: [PATCH 207/399] Remove circular dependency with Node and ExampleGroup --- src/spectator/example.cr | 43 +++++++++++++++++++++++++++------- src/spectator/example_group.cr | 40 +++++++++++++++++++++++++++++++ src/spectator/node.cr | 30 ++---------------------- 3 files changed, 76 insertions(+), 37 deletions(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 2ec9ea0..ac0dfa1 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -13,6 +13,14 @@ module Spectator # Currently running example. class_getter! current : Example + # Group the node belongs to. + getter! group : ExampleGroup + + # Assigns the node to the specified *group*. + # This is an internal method and should only be called from `ExampleGroup`. + # `ExampleGroup` manages the association of nodes to groups. + protected setter group : ExampleGroup? + # Indicates whether the example already ran. getter? finished : Bool = false @@ -33,8 +41,11 @@ module Spectator # Note: The tags will not be merged with the parent tags. def initialize(@context : Context, @entrypoint : self ->, name : String? = nil, location : Location? = nil, - group : ExampleGroup? = nil, tags = Tags.new) - super(name, location, group, tags) + @group : ExampleGroup? = nil, tags = Tags.new) + super(name, location, tags) + + # Ensure group is linked. + group << self if group end # Creates a dynamic example. @@ -48,9 +59,13 @@ module Spectator # Note: The tags will not be merged with the parent tags. def initialize(name : String? = nil, location : Location? = nil, group : ExampleGroup? = nil, tags = Tags.new, &block : self ->) - super(name, location, group, tags) + super(name, location, tags) + @context = NullContext.new @entrypoint = block + + # Ensure group is linked. + group << self if group end # Executes the test case. @@ -87,10 +102,10 @@ module Spectator end private def run_internal - group?.try(&.call_before_each(self)) + @group.try(&.call_before_each(self)) @entrypoint.call(self) @finished = true - group?.try(&.call_after_each(self)) + @group.try(&.call_after_each(self)) end # Executes code within the example's test context. @@ -126,11 +141,21 @@ module Spectator # Constructs the full name or description of the example. # This prepends names of groups this example is part of. def to_s(io) - if name? - super - else - io << "" + name = @name + + # Prefix with group's full name if the node belongs to a group. + if (group = @group) + group.to_s(io) + + # Add padding between the node names + # only if the names don't appear to be symbolic. + # Skip blank group names (like the root group). + io << ' ' unless !group.name? || # ameba:disable Style/NegatedConditionsInUnless + (group.name?.is_a?(Symbol) && name.is_a?(String) && + (name.starts_with?('#') || name.starts_with?('.'))) end + + super end # Exposes information about the example useful for debugging. diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index 29e9b46..45c4cf1 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -11,6 +11,14 @@ module Spectator @nodes = [] of Node + # Parent group this group belongs to. + getter! group : ExampleGroup + + # Assigns this group to the specified *group*. + # This is an internal method and should only be called from `ExampleGroup`. + # `ExampleGroup` manages the association of nodes to groups. + protected setter group : ExampleGroup? + group_event before_all do |hooks| Log.trace { "Processing before_all hooks for #{self}" } @@ -63,6 +71,18 @@ module Spectator end end + # Creates the example group. + # The *name* describes the purpose of the group. + # It can be a `Symbol` to describe a type. + # The *location* tracks where the group exists in source code. + # This group will be assigned to the parent *group* if it is provided. + # A set of *tags* can be used for filtering and modifying example behavior. + def initialize(@name : Label = nil, @location : Location? = nil, + @group : ExampleGroup? = nil, @tags : Tags = Tags.new) + # Ensure group is linked. + group << self if group + end + # Removes the specified *node* from the group. # The node will be unassigned from this group. def delete(node : Node) @@ -88,6 +108,26 @@ module Spectator @nodes.all?(&.finished?) end + # Constructs the full name or description of the example group. + # This prepends names of groups this group is part of. + def to_s(io) + name = @name + + # Prefix with group's full name if the node belongs to a group. + if (group = @group) + group.to_s(io) + + # Add padding between the node names + # only if the names don't appear to be symbolic. + # Skip blank group names (like the root group). + io << ' ' unless !group.name? || # ameba:disable Style/NegatedConditionsInUnless + (group.name?.is_a?(Symbol) && name.is_a?(String) && + (name.starts_with?('#') || name.starts_with?('.'))) + end + + super + end + # Adds the specified *node* to the group. # Assigns the node to this group. # If the node already belongs to a group, diff --git a/src/spectator/node.cr b/src/spectator/node.cr index 587af76..4afe273 100644 --- a/src/spectator/node.cr +++ b/src/spectator/node.cr @@ -26,27 +26,15 @@ module Spectator protected def name=(@name : String) end - # Group the node belongs to. - getter! group : ExampleGroup - # User-defined keywords used for filtering and behavior modification. getter tags : Tags - # Assigns the node to the specified *group*. - # This is an internal method and should only be called from `ExampleGroup`. - # `ExampleGroup` manages the association of nodes to groups. - protected setter group : ExampleGroup? - # Creates the node. # The *name* describes the purpose of the node. # It can be a `Symbol` to describe a type. # The *location* tracks where the node exists in source code. - # The node will be assigned to *group* if it is provided. # A set of *tags* can be used for filtering and modifying example behavior. - def initialize(@name : Label = nil, @location : Location? = nil, - group : ExampleGroup? = nil, @tags : Tags = Tags.new) - # Ensure group is linked. - group << self if group + def initialize(@name : Label = nil, @location : Location? = nil, @tags : Tags = Tags.new) end # Indicates whether the node has completed. @@ -61,21 +49,7 @@ module Spectator # Constructs the full name or description of the node. # This prepends names of groups this node is part of. def to_s(io) - name = @name - - # Prefix with group's full name if the node belongs to a group. - if (group = @group) - group.to_s(io) - - # Add padding between the node names - # only if the names don't appear to be symbolic. - # Skip blank group names (like the root group). - io << ' ' unless !group.name? || # ameba:disable Style/NegatedConditionsInUnless - (group.name?.is_a?(Symbol) && name.is_a?(String) && - (name.starts_with?('#') || name.starts_with?('.'))) - end - - name.to_s(io) + (@name || "").to_s(io) end end end From d7ba47cc4967f9686db67c52457230a76b8dda88 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 8 May 2021 12:00:29 -0600 Subject: [PATCH 208/399] Clean up hook code --- src/spectator/example_group.cr | 68 +++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index 45c4cf1..65392e8 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -19,56 +19,66 @@ module Spectator # `ExampleGroup` manages the association of nodes to groups. protected setter group : ExampleGroup? - group_event before_all do |hooks| - Log.trace { "Processing before_all hooks for #{self}" } - - if (parent = group?) - parent.call_once_before_all + # Calls all hooks from the parent group if there is a parent. + # The *hook* is the method name of the group hook to invoke. + private macro call_parent_hooks(hook) + if (parent = @group) + parent.{{hook.id}} end + end + # Calls all hooks from the parent group if there is a parent. + # The *hook* is the method name of the example hook to invoke. + # The current *example* must be provided. + private macro call_parent_hooks(hook, example) + if (parent = @group) + parent.{{hook.id}}({{example}}) + end + end + + # Calls group hooks of the current group. + private def call_hooks(hooks) hooks.each do |hook| Log.trace { "Invoking hook #{hook}" } hook.call end end + # Calls example hooks of the current group. + # Requires the current *example*. + private def call_hooks(hooks, example) + hooks.each do |hook| + Log.trace { "Invoking hook #{hook}" } + hook.call(example) + end + end + + group_event before_all do |hooks| + Log.trace { "Processing before_all hooks for #{self}" } + + call_parent_hooks(:call_once_before_all) + call_hooks(hooks) + end + group_event after_all do |hooks| Log.trace { "Processing after_all hooks for #{self}" } - hooks.each do |hook| - Log.trace { "Invoking hook #{hook}" } - hook.call - end - - if (parent = group?) - parent.call_once_after_all - end + call_hooks(hooks) + call_parent_hooks(:call_once_after_all) end example_event before_each do |hooks, example| Log.trace { "Processing before_each hooks for #{self}" } - if (parent = group?) - parent.call_before_each(example) - end - - hooks.each do |hook| - Log.trace { "Invoking hook #{hook}" } - hook.call(example) - end + call_parent_hooks(:call_before_each, example) + call_hooks(hooks, example) end example_event after_each do |hooks, example| Log.trace { "Processing after_each hooks for #{self}" } - hooks.each do |hook| - Log.trace { "Invoking hook #{hook}" } - hook.call(example) - end - - if (parent = group?) - parent.call_after_each(example) - end + call_hooks(hooks, example) + call_parent_hooks(:call_after_each, example) end # Creates the example group. From ef7fca3f95dd84e306f9786283bde201dbc700e4 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 8 May 2021 12:10:27 -0600 Subject: [PATCH 209/399] Bit of cleanup around parent/group --- src/spectator/example.cr | 18 +++++++++--------- src/spectator/example_group.cr | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index ac0dfa1..574a839 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -57,8 +57,8 @@ module Spectator # The example will be assigned to *group* if it is provided. # A set of *tags* can be used for filtering and modifying example behavior. # Note: The tags will not be merged with the parent tags. - def initialize(name : String? = nil, location : Location? = nil, group : ExampleGroup? = nil, - tags = Tags.new, &block : self ->) + def initialize(name : String? = nil, location : Location? = nil, + @group : ExampleGroup? = nil, tags = Tags.new, &block : self ->) super(name, location, tags) @context = NullContext.new @@ -85,13 +85,13 @@ module Spectator begin @result = Harness.run do - group?.try(&.call_once_before_all) - if (parent = group?) + @group.try(&.call_once_before_all) + if (parent = @group) parent.call_around_each(self) { run_internal } else run_internal end - if (parent = group?) + if (parent = @group) parent.call_once_after_all if parent.finished? end end @@ -144,14 +144,14 @@ module Spectator name = @name # Prefix with group's full name if the node belongs to a group. - if (group = @group) - group.to_s(io) + if (parent = @group) + parent.to_s(io) # Add padding between the node names # only if the names don't appear to be symbolic. # Skip blank group names (like the root group). - io << ' ' unless !group.name? || # ameba:disable Style/NegatedConditionsInUnless - (group.name?.is_a?(Symbol) && name.is_a?(String) && + io << ' ' unless !parent.name? || # ameba:disable Style/NegatedConditionsInUnless + (parent.name?.is_a?(Symbol) && name.is_a?(String) && (name.starts_with?('#') || name.starts_with?('.'))) end diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index 65392e8..b1b08fe 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -124,14 +124,14 @@ module Spectator name = @name # Prefix with group's full name if the node belongs to a group. - if (group = @group) - group.to_s(io) + if (parent = @group) + parent.to_s(io) # Add padding between the node names # only if the names don't appear to be symbolic. # Skip blank group names (like the root group). - io << ' ' unless !group.name? || # ameba:disable Style/NegatedConditionsInUnless - (group.name?.is_a?(Symbol) && name.is_a?(String) && + io << ' ' unless !parent.name? || # ameba:disable Style/NegatedConditionsInUnless + (parent.name?.is_a?(Symbol) && name.is_a?(String) && (name.starts_with?('#') || name.starts_with?('.'))) end From d292c01e747a986a4092536072dcf5a1c410653f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 8 May 2021 12:43:41 -0600 Subject: [PATCH 210/399] Remove direct references to Example in ExampleGroup --- src/spectator/example.cr | 5 +++++ src/spectator/example_group.cr | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 574a839..7c641a6 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -180,6 +180,11 @@ module Spectator json.string(to_s) end + # Creates a procsy from this example and the provided block. + def procsy(&block : ->) + Procsy.new(self, &block) + end + # Wraps an example to behave like a `Proc`. # This is typically used for an *around_each* hook. # Invoking `#call` or `#run` will run the example. diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index b1b08fe..8031e20 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -163,26 +163,26 @@ module Spectator # Defines a hook for the *around_each* event. # The block of code given to this method is invoked when the event occurs. # The current example is provided as a block argument. - def around_each(&block : Example::Procsy ->) : Nil + def around_each(&block) : Nil hook = ExampleProcsyHook.new(label: "around_each", &block) add_around_each_hook(hook) end # Signals that the *around_each* event has occurred. # All hooks associated with the event will be called. - def call_around_each(example : Example, &block : -> _) : Nil + def call_around_each(example, &block : -> _) : Nil # Avoid overhead if there's no hooks. return yield if @around_hooks.empty? # Start with a procsy that wraps the original code. - procsy = Example::Procsy.new(example, &block) + procsy = example.procsy(&block) procsy = wrap_around_each(procsy) procsy.call end # Wraps a procsy with the *around_each* hooks from this example group. # The returned procsy will call each hook then *procsy*. - protected def wrap_around_each(procsy : Example::Procsy) : Example::Procsy + protected def wrap_around_each(procsy) # Avoid overhead if there's no hooks. return procsy if @around_hooks.empty? From 88b323bc27a3d157de5773a50a928d04631a0bd4 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 8 May 2021 13:22:13 -0600 Subject: [PATCH 211/399] Move common inspect code up to Node --- src/spectator/example.cr | 13 ++----------- src/spectator/node.cr | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 7c641a6..17cf51a 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -160,17 +160,8 @@ module Spectator # Exposes information about the example useful for debugging. def inspect(io) - # Full example name. - io << '"' - to_s(io) - io << '"' - - # Add location if it's available. - if (location = self.location) - io << " @ " - io << location - end - + super + io << ' ' io << result end diff --git a/src/spectator/node.cr b/src/spectator/node.cr index 4afe273..7a562e1 100644 --- a/src/spectator/node.cr +++ b/src/spectator/node.cr @@ -51,5 +51,19 @@ module Spectator def to_s(io) (@name || "").to_s(io) end + + # Exposes information about the node useful for debugging. + def inspect(io) + # Full node name. + io << '"' + to_s(io) + io << '"' + + # Add location if it's available. + if (location = self.location) + io << " @ " + io << location + end + end end end From 83d7657b18319a9cffb96afb56f382c68b86c043 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 8 May 2021 22:51:54 -0600 Subject: [PATCH 212/399] Use enum flags for run modes --- src/spectator/config.cr | 31 ++++-------------- src/spectator/config_builder.cr | 57 +++++++++++++++++++++------------ src/spectator/run_flags.cr | 21 ++++++++++++ 3 files changed, 64 insertions(+), 45 deletions(-) create mode 100644 src/spectator/run_flags.cr diff --git a/src/spectator/config.cr b/src/spectator/config.cr index 025697d..8ecb38e 100644 --- a/src/spectator/config.cr +++ b/src/spectator/config.cr @@ -1,44 +1,25 @@ require "./example_filter" require "./formatting" +require "./run_flags" module Spectator # Provides customization and describes specifics for how Spectator will run and report tests. class Config @formatters : Array(Formatting::Formatter) - # Indicates whether the test should abort on first failure. - getter? fail_fast : Bool - - # Indicates whether the test should fail if there are no examples. - getter? fail_blank : Bool - - # Indicates whether the test should be done as a dry-run. - # Examples won't run, but the output will show that they did. - getter? dry_run : Bool - - # Indicates whether examples run in a random order. - getter? randomize : Bool + # Flags indicating how the spec should run. + getter run_flags : RunFlags # Seed used for random number generation. getter random_seed : UInt64 - # Indicates whether timing information should be displayed. - getter? profile : Bool - - # Filter determining examples to run. - getter example_filter : ExampleFilter - # Creates a new configuration. # Properties are pulled from *source*. # Typically, *source* is a `ConfigBuilder`. def initialize(source) @formatters = source.formatters - @fail_fast = source.fail_fast? - @fail_blank = source.fail_blank? - @dry_run = source.dry_run? - @randomize = source.randomize? + @run_flags = source.run_flags @random_seed = source.random_seed - @profile = source.profile? @example_filter = source.example_filter end @@ -47,7 +28,7 @@ module Spectator # Otherwise, the items are left alone and returned as-is. # The array of *items* is never modified. def shuffle(items) - return items unless randomize? + return items unless run_flags.randomize? items.shuffle(random) end @@ -57,7 +38,7 @@ module Spectator # Otherwise, the items are left alone and returned as-is. # The array of *items* is modified, the items are shuffled in-place. def shuffle!(items) - return items unless randomize? + return items unless run_flags.randomize? items.shuffle!(random) end diff --git a/src/spectator/config_builder.cr b/src/spectator/config_builder.cr index 3fb10cb..182eaed 100644 --- a/src/spectator/config_builder.cr +++ b/src/spectator/config_builder.cr @@ -2,6 +2,7 @@ require "./composite_example_filter" require "./config" require "./example_filter" require "./null_example_filter" +require "./run_flags" module Spectator # Mutable configuration used to produce a final configuration. @@ -18,11 +19,7 @@ module Spectator @primary_formatter : Formatting::Formatter? @additional_formatters = [] of Formatting::Formatter - @fail_fast = false - @fail_blank = false - @dry_run = false - @randomize = false - @profile = false + @run_flags = RunFlags::None @filters = [] of ExampleFilter # Creates a configuration. @@ -55,79 +52,99 @@ module Spectator # Enables fail-fast mode. def fail_fast - self.fail_fast = true + @run_flags |= RunFlags::FailFast end # Sets the fail-fast flag. def fail_fast=(flag) - @fail_fast = flag + if flag + @run_flags |= RunFlags::FailFast + else + @run_flags &= ~RunFlags::FailFast + end end # Indicates whether fail-fast mode is enabled. protected def fail_fast? - @fail_fast + @run_flags.fail_fast? end # Enables fail-blank mode (fail on no tests). def fail_blank - self.fail_blank = true + @run_flags |= RunFlags::FailBlank end # Enables or disables fail-blank mode. def fail_blank=(flag) - @fail_blank = flag + if flag + @run_flags |= RunFlags::FailBlank + else + @run_flags &= ~RunFlags::FailBlank + end end # Indicates whether fail-fast mode is enabled. # That is, it is a failure if there are no tests. protected def fail_blank? - @fail_blank + @run_flags.fail_blank? end # Enables dry-run mode. def dry_run - self.dry_run = true + @run_flags |= RunFlags::DryRun end # Enables or disables dry-run mode. def dry_run=(flag) - @dry_run = flag + if flag + @run_flags |= RunFlags::DryRun + else + @run_flags &= ~RunFlags::DryRun + end end # Indicates whether dry-run mode is enabled. # In this mode, no tests are run, but output acts like they were. protected def dry_run? - @dry_run + @run_flags.dry_run? end # Randomizes test execution order. def randomize - self.randomize = true + @run_flags |= RunFlags::Randomize end # Enables or disables running tests in a random order. def randomize=(flag) - @randomize = flag + if flag + @run_flags |= RunFlags::Randomize + else + @run_flags &= ~RunFlags::Randomize + end end # Indicates whether tests are run in a random order. protected def randomize? - @randomize + @run_flags.randomize? end # Displays profiling information def profile - self.profile = true + @run_flags |= RunFlags::Profile end # Enables or disables displaying profiling information. def profile=(flag) - @profile = flag + if flag + @run_flags |= RunFlags::Profile + else + @run_flags &= ~RunFlags::Profile + end end # Indicates whether profiling information should be displayed. protected def profile? - @profile + @run_flags.profile? end # Adds a filter to determine which examples can run. diff --git a/src/spectator/run_flags.cr b/src/spectator/run_flags.cr new file mode 100644 index 0000000..3ce2d9e --- /dev/null +++ b/src/spectator/run_flags.cr @@ -0,0 +1,21 @@ +module Spectator + # Toggles indicating how the test spec should execute. + @[Flags] + enum RunFlags + # Indicates whether the test should abort on first failure. + FailFast + + # Indicates whether the test should fail if there are no examples. + FailBlank + + # Indicates whether the test should be done as a dry-run. + # Examples won't run, but the output will show that they did. + DryRun + + # Indicates whether examples run in a random order. + Randomize + + # Indicates whether timing information should be generated. + Profile + end +end From e7c3d8f060d8e74058f26bf9c10009a7dd4c7d70 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 8 May 2021 22:53:39 -0600 Subject: [PATCH 213/399] Use filter if only one is provided This is less overhead than creating a composite filter for one child filter. --- src/spectator/config_builder.cr | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/spectator/config_builder.cr b/src/spectator/config_builder.cr index 182eaed..d0e2661 100644 --- a/src/spectator/config_builder.cr +++ b/src/spectator/config_builder.cr @@ -156,10 +156,10 @@ module Spectator # If no filters were added with `#add_example_filter`, # then the returned filter will allow all examples to be run. protected def example_filter - if @filters.empty? - NullExampleFilter.new - else - CompositeExampleFilter.new(@filters) + case (filters = @filters) + when .empty? then NullExampleFilter.new + when .one? then filters.first + else CompositeExampleFilter.new(filters) end end end From f2459cfe94b688154abf50a201a3b2c180ecd403 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 8 May 2021 23:33:59 -0600 Subject: [PATCH 214/399] Override === to call includes? Allows the use of a filter in pattern matching scenarios (select). --- src/spectator/example_filter.cr | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/spectator/example_filter.cr b/src/spectator/example_filter.cr index 0aa3ab2..5c040c3 100644 --- a/src/spectator/example_filter.cr +++ b/src/spectator/example_filter.cr @@ -5,5 +5,10 @@ module Spectator abstract class ExampleFilter # Checks if an example is in the filter, and should be run. abstract def includes?(example : Example) : Bool + + # :ditto: + def ===(example : Example) + includes?(example) + end end end From 8a4735b9e69f68094e77c8b5266e25c78a14317e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 8 May 2021 23:35:29 -0600 Subject: [PATCH 215/399] Use Formatters instead of Reporters (and Formatters) --- src/spectator/formatting/broadcast_formatter.cr | 13 +++++++++++++ src/spectator/formatting/formatter.cr | 9 ++++----- src/spectator/reporters/broadcast_reporter.cr | 13 ------------- src/spectator/reporters/reporter.cr | 7 ------- 4 files changed, 17 insertions(+), 25 deletions(-) create mode 100644 src/spectator/formatting/broadcast_formatter.cr delete mode 100644 src/spectator/reporters/broadcast_reporter.cr delete mode 100644 src/spectator/reporters/reporter.cr diff --git a/src/spectator/formatting/broadcast_formatter.cr b/src/spectator/formatting/broadcast_formatter.cr new file mode 100644 index 0000000..3992777 --- /dev/null +++ b/src/spectator/formatting/broadcast_formatter.cr @@ -0,0 +1,13 @@ +require "./formatter" + +module Spectator::Formatting + # Reports events to multiple other formatters. + # Events received by this formatter will be sent to others. + class BroadcastFormatter < Formatter + # Creates the broadcast formatter. + # Takes a collection of formatters to pass events along to. + def initialize(formatters : Enumerable(Formatter)) + @formatters = formatters.to_a + end + end +end diff --git a/src/spectator/formatting/formatter.cr b/src/spectator/formatting/formatter.cr index 784a2ff..125c149 100644 --- a/src/spectator/formatting/formatter.cr +++ b/src/spectator/formatting/formatter.cr @@ -1,8 +1,7 @@ -require "../reporters" - module Spectator::Formatting - abstract class Formatter < Reporters::Reporter - def initialize(@output : IO) - end + # Base class and interface used to notify systems of events. + # This is typically used for producing output from test results, + # but can also be used to send data to external systems. + abstract class Formatter end end diff --git a/src/spectator/reporters/broadcast_reporter.cr b/src/spectator/reporters/broadcast_reporter.cr deleted file mode 100644 index da4fc36..0000000 --- a/src/spectator/reporters/broadcast_reporter.cr +++ /dev/null @@ -1,13 +0,0 @@ -require "./reporter" - -module Spectator::Reporters - # Reports events to multiple other reporters. - # Events received by this reporter will be sent to others. - class BroadcastReporter < Reporter - # Creates the broadcast reporter. - # Takes a collection of reporters to pass events along to. - def initialize(reporters : Enumerable(Reporter)) - @reporters = reporters.to_a - end - end -end diff --git a/src/spectator/reporters/reporter.cr b/src/spectator/reporters/reporter.cr deleted file mode 100644 index a050e0c..0000000 --- a/src/spectator/reporters/reporter.cr +++ /dev/null @@ -1,7 +0,0 @@ -module Spectator::Reporters - # Base class and interface used to notify systems of events. - # This is typically used for producing output from test results, - # but can also be used to send data to external systems. - abstract class Reporter - end -end From e7138080a6b4e637ef2c0b46f2d519d640e4df06 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 8 May 2021 23:37:33 -0600 Subject: [PATCH 216/399] Clean up and simplify Config and Builder --- src/spectator/config.cr | 25 +++-- src/spectator/config/builder.cr | 171 ++++++++++++++++++++++++++++++++ src/spectator/config_builder.cr | 166 ------------------------------- 3 files changed, 187 insertions(+), 175 deletions(-) create mode 100644 src/spectator/config/builder.cr delete mode 100644 src/spectator/config_builder.cr diff --git a/src/spectator/config.cr b/src/spectator/config.cr index 8ecb38e..ddcf098 100644 --- a/src/spectator/config.cr +++ b/src/spectator/config.cr @@ -1,11 +1,15 @@ +require "./config/*" require "./example_filter" -require "./formatting" +require "./example_group" +require "./example_iterator" +require "./formatting/formatter" require "./run_flags" module Spectator # Provides customization and describes specifics for how Spectator will run and report tests. class Config - @formatters : Array(Formatting::Formatter) + # Primary formatter all events will be sent to. + getter formatter : Formatting::Formatter # Flags indicating how the spec should run. getter run_flags : RunFlags @@ -15,14 +19,19 @@ module Spectator # Creates a new configuration. # Properties are pulled from *source*. - # Typically, *source* is a `ConfigBuilder`. + # Typically, *source* is a `Config::Builder`. def initialize(source) - @formatters = source.formatters + @formatter = source.formatter @run_flags = source.run_flags @random_seed = source.random_seed @example_filter = source.example_filter end + # Produces the default configuration. + def self.default : self + Builder.new.build + end + # Shuffles the items in an array using the configured random settings. # If `#randomize?` is true, the *items* are shuffled and returned as a new array. # Otherwise, the items are left alone and returned as-is. @@ -43,11 +52,9 @@ module Spectator items.shuffle!(random) end - # Yields each formatter that should be reported to. - def each_formatter - @formatters.each do |formatter| - yield formatter - end + # Creates an iterator configured to select the filtered examples. + def iterator(group : ExampleGroup) + ExampleIterator.new(group).select(@example_filter) end # Retrieves the configured random number generator. diff --git a/src/spectator/config/builder.cr b/src/spectator/config/builder.cr new file mode 100644 index 0000000..a61a465 --- /dev/null +++ b/src/spectator/config/builder.cr @@ -0,0 +1,171 @@ +require "../composite_example_filter" +require "../example_filter" +require "../null_example_filter" +require "../reporters" +require "../run_flags" + +module Spectator + class Config + # Mutable configuration used to produce a final configuration. + # Use the setters in this class to incrementally build a configuration. + # Then call `#build` to create the final configuration. + class Builder + # Seed used for random number generation. + property random_seed : UInt64 = Random.rand(UInt64) + + @primary_formatter : Formatting::Formatter? + @additional_formatters = [] of Formatting::Formatter + @run_flags = RunFlags::None + @filters = [] of ExampleFilter + + # Creates a configuration. + def build : Config + Config.new(self) + end + + # Sets the primary formatter to use for reporting test progress and results. + def formatter=(formatter : Formatting::Formatter) + @primary_formatter = formatter + end + + # Adds an extra formatter to use for reporting test progress and results. + def add_formatter(formatter : Formatting::Formatter) + @additional_formatters << formatter + end + + # Retrieves the formatters to use. + # If one wasn't specified by the user, + # then `#default_formatter` is returned. + private def formatters + @additional_formatters + [(@primary_formatter || default_formatter)] + end + + # The formatter that should be used if one wasn't provided. + private def default_formatter + Formatting::ProgressFormatter.new + end + + # A single formatter that will satisfy the configured output. + # If one formatter was configured, then it is returned. + # Otherwise, a `Formatting::BroadcastFormatter` is returned. + protected def formatter + case (formatters = self.formatters) + when .one? then formatters.first + else Formatting::BroadcastFormatter.new(formatters) + end + + # Enables fail-fast mode. + def fail_fast + @run_flags |= RunFlags::FailFast + end + + # Sets the fail-fast flag. + def fail_fast=(flag) + if flag + @run_flags |= RunFlags::FailFast + else + @run_flags &= ~RunFlags::FailFast + end + end + + # Indicates whether fail-fast mode is enabled. + protected def fail_fast? + @run_flags.fail_fast? + end + + # Enables fail-blank mode (fail on no tests). + def fail_blank + @run_flags |= RunFlags::FailBlank + end + + # Enables or disables fail-blank mode. + def fail_blank=(flag) + if flag + @run_flags |= RunFlags::FailBlank + else + @run_flags &= ~RunFlags::FailBlank + end + end + + # Indicates whether fail-fast mode is enabled. + # That is, it is a failure if there are no tests. + protected def fail_blank? + @run_flags.fail_blank? + end + + # Enables dry-run mode. + def dry_run + @run_flags |= RunFlags::DryRun + end + + # Enables or disables dry-run mode. + def dry_run=(flag) + if flag + @run_flags |= RunFlags::DryRun + else + @run_flags &= ~RunFlags::DryRun + end + end + + # Indicates whether dry-run mode is enabled. + # In this mode, no tests are run, but output acts like they were. + protected def dry_run? + @run_flags.dry_run? + end + + # Randomizes test execution order. + def randomize + @run_flags |= RunFlags::Randomize + end + + # Enables or disables running tests in a random order. + def randomize=(flag) + if flag + @run_flags |= RunFlags::Randomize + else + @run_flags &= ~RunFlags::Randomize + end + end + + # Indicates whether tests are run in a random order. + protected def randomize? + @run_flags.randomize? + end + + # Displays profiling information + def profile + @run_flags |= RunFlags::Profile + end + + # Enables or disables displaying profiling information. + def profile=(flag) + if flag + @run_flags |= RunFlags::Profile + else + @run_flags &= ~RunFlags::Profile + end + end + + # Indicates whether profiling information should be displayed. + protected def profile? + @run_flags.profile? + end + + # Adds a filter to determine which examples can run. + def add_example_filter(filter : ExampleFilter) + @filters << filter + end + + # Retrieves a filter that determines which examples can run. + # If no filters were added with `#add_example_filter`, + # then the returned filter will allow all examples to be run. + protected def example_filter + case (filters = @filters) + when .empty? then NullExampleFilter.new + when .one? then filters.first + else CompositeExampleFilter.new(filters) + end + end + end + end +end diff --git a/src/spectator/config_builder.cr b/src/spectator/config_builder.cr deleted file mode 100644 index d0e2661..0000000 --- a/src/spectator/config_builder.cr +++ /dev/null @@ -1,166 +0,0 @@ -require "./composite_example_filter" -require "./config" -require "./example_filter" -require "./null_example_filter" -require "./run_flags" - -module Spectator - # Mutable configuration used to produce a final configuration. - # Use the setters in this class to incrementally build a configuration. - # Then call `#build` to create the final configuration. - class ConfigBuilder - # Creates a default configuration. - def self.default - new.build - end - - # Seed used for random number generation. - property random_seed : UInt64 = Random.rand(UInt64) - - @primary_formatter : Formatting::Formatter? - @additional_formatters = [] of Formatting::Formatter - @run_flags = RunFlags::None - @filters = [] of ExampleFilter - - # Creates a configuration. - def build : Config - Config.new(self) - end - - # Sets the primary formatter to use for reporting test progress and results. - def formatter=(formatter : Formatting::Formatter) - @primary_formatter = formatter - end - - # Adds an extra formater to use for reporting test progress and results. - def add_formatter(formatter : Formatting::Formatter) - @additional_formatters << formatter - end - - # Retrieves the formatters to use. - # If one wasn't specified by the user, - # then `#default_formatter` is returned. - protected def formatters - @additional_formatters + [(@primary_formatter || default_formatter)] - end - - # The formatter that should be used, - # if one wasn't provided. - private def default_formatter - Formatting::DotsFormatter.new - end - - # Enables fail-fast mode. - def fail_fast - @run_flags |= RunFlags::FailFast - end - - # Sets the fail-fast flag. - def fail_fast=(flag) - if flag - @run_flags |= RunFlags::FailFast - else - @run_flags &= ~RunFlags::FailFast - end - end - - # Indicates whether fail-fast mode is enabled. - protected def fail_fast? - @run_flags.fail_fast? - end - - # Enables fail-blank mode (fail on no tests). - def fail_blank - @run_flags |= RunFlags::FailBlank - end - - # Enables or disables fail-blank mode. - def fail_blank=(flag) - if flag - @run_flags |= RunFlags::FailBlank - else - @run_flags &= ~RunFlags::FailBlank - end - end - - # Indicates whether fail-fast mode is enabled. - # That is, it is a failure if there are no tests. - protected def fail_blank? - @run_flags.fail_blank? - end - - # Enables dry-run mode. - def dry_run - @run_flags |= RunFlags::DryRun - end - - # Enables or disables dry-run mode. - def dry_run=(flag) - if flag - @run_flags |= RunFlags::DryRun - else - @run_flags &= ~RunFlags::DryRun - end - end - - # Indicates whether dry-run mode is enabled. - # In this mode, no tests are run, but output acts like they were. - protected def dry_run? - @run_flags.dry_run? - end - - # Randomizes test execution order. - def randomize - @run_flags |= RunFlags::Randomize - end - - # Enables or disables running tests in a random order. - def randomize=(flag) - if flag - @run_flags |= RunFlags::Randomize - else - @run_flags &= ~RunFlags::Randomize - end - end - - # Indicates whether tests are run in a random order. - protected def randomize? - @run_flags.randomize? - end - - # Displays profiling information - def profile - @run_flags |= RunFlags::Profile - end - - # Enables or disables displaying profiling information. - def profile=(flag) - if flag - @run_flags |= RunFlags::Profile - else - @run_flags &= ~RunFlags::Profile - end - end - - # Indicates whether profiling information should be displayed. - protected def profile? - @run_flags.profile? - end - - # Adds a filter to determine which examples can run. - def add_example_filter(filter : ExampleFilter) - @filters << filter - end - - # Retrieves a filter that determines which examples can run. - # If no filters were added with `#add_example_filter`, - # then the returned filter will allow all examples to be run. - protected def example_filter - case (filters = @filters) - when .empty? then NullExampleFilter.new - when .one? then filters.first - else CompositeExampleFilter.new(filters) - end - end - end -end From f09a6a87e558d9cda672e30cae9e57fd7d532173 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 8 May 2021 23:38:13 -0600 Subject: [PATCH 217/399] Remove TestSuite middle-man object --- src/spectator/spec.cr | 25 +++++++++++++++++-------- src/spectator/spec/runner.cr | 23 +++++++++-------------- src/spectator/test_suite.cr | 25 ------------------------- 3 files changed, 26 insertions(+), 47 deletions(-) delete mode 100644 src/spectator/test_suite.cr diff --git a/src/spectator/spec.cr b/src/spectator/spec.cr index a034332..03f3d51 100644 --- a/src/spectator/spec.cr +++ b/src/spectator/spec.cr @@ -1,19 +1,28 @@ require "./config" -require "./example" require "./example_group" -require "./test_suite" +require "./spec/*" module Spectator - # Contains examples to be tested. + # Contains examples to be tested and configuration for running them. class Spec + # Creates the spec. + # The *root* is the top-most example group. + # All examples in this group and groups nested under are candidates for execution. + # The *config* provides settings controlling how tests will be executed. def initialize(@root : ExampleGroup, @config : Config) end - def run(filter : ExampleFilter) - suite = TestSuite.new(@root, filter) - Runner.new(suite, @config).run + # Runs all selected examples and returns the results. + def run + Runner.new(examples, @config).run + end + + # Selects and shuffles the examples that should run. + private def examples + iterator = @config.iterator(@root) + iterator.to_a.tap do |examples| + @config.shuffle!(examples) + end end end end - -require "./spec/*" diff --git a/src/spectator/spec/runner.cr b/src/spectator/spec/runner.cr index f5ccf8f..69d8940 100644 --- a/src/spectator/spec/runner.cr +++ b/src/spectator/spec/runner.cr @@ -2,14 +2,18 @@ require "../example" module Spectator class Spec + # Logic for executing examples and collecting results. private struct Runner - def initialize(@suite : TestSuite, @config : Config) + # Creates the runner. + # The collection of *examples* should be pre-filtered and shuffled. + # This runner will run each example in the order provided. + def initialize(@examples : Enumerable(Example), @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, + # Runs the spec. + # This will run the provided examples + # and invoke the reporters to communicate results. + # True will be returned if the spec ran successfully, # or false if there was at least one failure. def run : Bool # Indicate the suite is starting. @@ -42,15 +46,6 @@ module Spectator 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) diff --git a/src/spectator/test_suite.cr b/src/spectator/test_suite.cr deleted file mode 100644 index 2106193..0000000 --- a/src/spectator/test_suite.cr +++ /dev/null @@ -1,25 +0,0 @@ -module Spectator - # Encapsulates the tests to run and additional properties about them. - # Use `#each` to enumerate over all tests in the suite. - class TestSuite - include Enumerable(Example) - - # Creates the test suite. - # The example *group* provided will be run. - # The *filter* identifies which examples to run from the *group*. - def initialize(@group : ExampleGroup, @filter : ExampleFilter) - end - - # Yields each example in the test suite. - def each : Nil - iterator.each do |example| - yield example if @filter.includes?(example) - end - end - - # Creates an iterator for the example group. - private def iterator - ExampleIterator.new(@group) - end - end -end From 7294f2da67345213595ad3eb8aa5eed971c6f0f1 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 8 May 2021 23:45:54 -0600 Subject: [PATCH 218/399] Change ConfigSource to "applicator" Remove unecessary abstract base class. Shorten name of CLI arguments config applicator. --- src/spectator.cr | 4 +- .../command_line_arguments_config_source.cr | 207 ----------------- .../config/cli_arguments_applicator.cr | 208 ++++++++++++++++++ src/spectator/config_source.cr | 10 - 4 files changed, 210 insertions(+), 219 deletions(-) delete mode 100644 src/spectator/command_line_arguments_config_source.cr create mode 100644 src/spectator/config/cli_arguments_applicator.cr delete mode 100644 src/spectator/config_source.cr diff --git a/src/spectator.cr b/src/spectator.cr index 88238a8..bff5ef3 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -107,11 +107,11 @@ module Spectator private def apply_config_file(file_path = CONFIG_FILE_PATH) : Nil return unless File.exists?(file_path) args = File.read(file_path).lines - CommandLineArgumentsConfigSource.new(args).apply(@@config_builder) + Config::CLIArgumentsApplicator.new(args).apply(@@config_builder) end # Applies configuration options from the command-line arguments private def apply_command_line_args : Nil - CommandLineArgumentsConfigSource.new.apply(@@config_builder) + Config::CLIArgumentsApplicator.new.apply(@@config_builder) end end diff --git a/src/spectator/command_line_arguments_config_source.cr b/src/spectator/command_line_arguments_config_source.cr deleted file mode 100644 index 1eeba61..0000000 --- a/src/spectator/command_line_arguments_config_source.cr +++ /dev/null @@ -1,207 +0,0 @@ -require "option_parser" -require "./config_source" -require "./formatting" -require "./line_example_filter" -require "./location" -require "./location_example_filter" -require "./name_example_filter" - -module Spectator - # Generates configuration from the command-line arguments. - class CommandLineArgumentsConfigSource < ConfigSource - # Logger for this class. - Log = Spectator::Log.for("config") - - # Creates the configuration source. - # By default, the command-line arguments (ARGV) are used. - # But custom arguments can be passed in. - def initialize(@args : Array(String) = ARGV) - end - - # Applies the specified configuration to a builder. - # Calling this method from multiple sources builds up the final configuration. - def apply(builder : ConfigBuilder) : Nil - OptionParser.parse(@args) do |parser| - control_parser_options(parser, builder) - filter_parser_options(parser, builder) - output_parser_options(parser, builder) - end - end - - # Adds options to the parser for controlling the test execution. - private def control_parser_options(parser, builder) - fail_fast_option(parser, builder) - fail_blank_option(parser, builder) - dry_run_option(parser, builder) - random_option(parser, builder) - seed_option(parser, builder) - order_option(parser, builder) - end - - # Adds the fail-fast option to the parser. - private def fail_fast_option(parser, builder) - parser.on("-f", "--fail-fast", "Stop testing on first failure") do - Log.debug { "Enabling fail-fast (-f)" } - builder.fail_fast - end - end - - # Adds the fail-blank option to the parser. - private def fail_blank_option(parser, builder) - parser.on("-b", "--fail-blank", "Fail if there are no examples") do - Log.debug { "Enabling fail-blank (-b)" } - builder.fail_blank - end - end - - # Adds the dry-run option to the parser. - private def dry_run_option(parser, builder) - parser.on("-d", "--dry-run", "Don't run any tests, output what would have run") do - Log.debug { "Enabling dry-run (-d)" } - builder.dry_run - end - end - - # Adds the randomize examples option to the parser. - private def random_option(parser, builder) - parser.on("-r", "--rand", "Randomize the execution order of tests") do - Log.debug { "Randomizing test order (-r)" } - builder.randomize - end - end - - # Adds the random seed option to the parser. - private def seed_option(parser, builder) - parser.on("--seed INTEGER", "Set the seed for the random number generator (implies -r)") do |seed| - Log.debug { "Randomizing test order and setting RNG seed to #{seed}" } - builder.randomize - builder.random_seed = seed.to_u64 - end - end - - # Adds the example order option to the parser. - private def order_option(parser, builder) - parser.on("--order ORDER", "Set the test execution order. ORDER should be one of: defined, rand, or rand:SEED") do |method| - case method.downcase - when "defined" - Log.debug { "Disabling randomized tests (--order defined)" } - builder.randomize = false - when /^rand/ - builder.randomize - parts = method.split(':', 2) - if (seed = parts[1]?) - Log.debug { "Randomizing test order and setting RNG seed to #{seed} (--order rand:#{seed})" } - builder.random_seed = seed.to_u64 - else - Log.debug { "Randomizing test order (--order rand)" } - end - else - nil - end - end - end - - # Adds options to the parser for filtering examples. - private def filter_parser_options(parser, builder) - example_option(parser, builder) - line_option(parser, builder) - location_option(parser, builder) - end - - # Adds the example filter option to the parser. - private def example_option(parser, builder) - parser.on("-e", "--example STRING", "Run examples whose full nested names include STRING") do |pattern| - Log.debug { "Filtering for examples named '#{pattern}' (-e '#{pattern}')" } - filter = NameExampleFilter.new(pattern) - builder.add_example_filter(filter) - end - end - - # Adds the line filter option to the parser. - private def line_option(parser, builder) - parser.on("-l", "--line LINE", "Run examples whose line matches LINE") do |line| - Log.debug { "Filtering for examples on line #{line} (-l #{line})" } - filter = LineExampleFilter.new(line.to_i) - builder.add_example_filter(filter) - end - end - - # Adds the location filter option to the parser. - private def location_option(parser, builder) - parser.on("--location FILE:LINE", "Run the example at line 'LINE' in the file 'FILE', multiple allowed") do |location| - Log.debug { "Filtering for examples at #{location} (--location '#{location}')" } - location = Location.parse(location) - filter = LocationExampleFilter.new(location) - builder.add_example_filter(filter) - end - end - - # Adds options to the parser for changing output. - private def output_parser_options(parser, builder) - verbose_option(parser, builder) - help_option(parser, builder) - profile_option(parser, builder) - json_option(parser, builder) - tap_option(parser, builder) - junit_option(parser, builder) - no_color_option(parser, builder) - end - - # Adds the verbose output option to the parser. - private def verbose_option(parser, builder) - parser.on("-v", "--verbose", "Verbose output using document formatter") do - Log.debug { "Setting output format to document (-v)" } - builder.formatter = Formatting::DocumentFormatter.new - end - end - - # Adds the help output option to the parser. - private def help_option(parser, builder) - parser.on("-h", "--help", "Show this help") do - puts parser - exit - end - end - - # Adds the profile output option to the parser. - private def profile_option(parser, builder) - parser.on("-p", "--profile", "Display the 10 slowest specs") do - Log.debug { "Enabling timing information (-p)" } - builder.profile - end - end - - # Adds the JSON output option to the parser. - private def json_option(parser, builder) - parser.on("--json", "Generate JSON output") do - Log.debug { "Setting output format to JSON (--json)" } - builder.formatter = Formatting::JsonFormatter.new - end - end - - # Adds the TAP output option to the parser. - private def tap_option(parser, builder) - parser.on("--tap", "Generate TAP output (Test Anything Protocol)") do - Log.debug { "Setting output format to TAP (--tap)" } - builder.formatter = Formatting::TAPFormatter.new - end - end - - # Adds the JUnit output option to the parser. - private def junit_option(parser, builder) - parser.on("--junit_output OUTPUT_DIR", "Generate JUnit XML output") do |output_dir| - Log.debug { "Setting output format to JUnit XML (--junit_output '#{output_dir}')" } - formatter = Formatting::JUnitFormatter.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 - Log.debug { "Disabling color output (--no-color)" } - Colorize.enabled = false - end - end - end -end diff --git a/src/spectator/config/cli_arguments_applicator.cr b/src/spectator/config/cli_arguments_applicator.cr new file mode 100644 index 0000000..396157f --- /dev/null +++ b/src/spectator/config/cli_arguments_applicator.cr @@ -0,0 +1,208 @@ +require "option_parser" +require "../formatting" +require "../line_example_filter" +require "../location" +require "../location_example_filter" +require "../name_example_filter" + +module Spectator + class Config + # Applies command-line arguments to a configuration. + class CLIArgumentsApplicator + # Logger for this class. + Log = Spectator::Log.for("config") + + # Creates the configuration source. + # By default, the command-line arguments (ARGV) are used. + # But custom arguments can be passed in. + def initialize(@args : Array(String) = ARGV) + end + + # Applies the specified configuration to a builder. + # Calling this method from multiple sources builds up the final configuration. + def apply(builder) : Nil + OptionParser.parse(@args) do |parser| + control_parser_options(parser, builder) + filter_parser_options(parser, builder) + output_parser_options(parser, builder) + end + end + + # Adds options to the parser for controlling the test execution. + private def control_parser_options(parser, builder) + fail_fast_option(parser, builder) + fail_blank_option(parser, builder) + dry_run_option(parser, builder) + random_option(parser, builder) + seed_option(parser, builder) + order_option(parser, builder) + end + + # Adds the fail-fast option to the parser. + private def fail_fast_option(parser, builder) + parser.on("-f", "--fail-fast", "Stop testing on first failure") do + Log.debug { "Enabling fail-fast (-f)" } + builder.fail_fast + end + end + + # Adds the fail-blank option to the parser. + private def fail_blank_option(parser, builder) + parser.on("-b", "--fail-blank", "Fail if there are no examples") do + Log.debug { "Enabling fail-blank (-b)" } + builder.fail_blank + end + end + + # Adds the dry-run option to the parser. + private def dry_run_option(parser, builder) + parser.on("-d", "--dry-run", "Don't run any tests, output what would have run") do + Log.debug { "Enabling dry-run (-d)" } + builder.dry_run + end + end + + # Adds the randomize examples option to the parser. + private def random_option(parser, builder) + parser.on("-r", "--rand", "Randomize the execution order of tests") do + Log.debug { "Randomizing test order (-r)" } + builder.randomize + end + end + + # Adds the random seed option to the parser. + private def seed_option(parser, builder) + parser.on("--seed INTEGER", "Set the seed for the random number generator (implies -r)") do |seed| + Log.debug { "Randomizing test order and setting RNG seed to #{seed}" } + builder.randomize + builder.random_seed = seed.to_u64 + end + end + + # Adds the example order option to the parser. + private def order_option(parser, builder) + parser.on("--order ORDER", "Set the test execution order. ORDER should be one of: defined, rand, or rand:SEED") do |method| + case method.downcase + when "defined" + Log.debug { "Disabling randomized tests (--order defined)" } + builder.randomize = false + when /^rand/ + builder.randomize + parts = method.split(':', 2) + if (seed = parts[1]?) + Log.debug { "Randomizing test order and setting RNG seed to #{seed} (--order rand:#{seed})" } + builder.random_seed = seed.to_u64 + else + Log.debug { "Randomizing test order (--order rand)" } + end + else + nil + end + end + end + + # Adds options to the parser for filtering examples. + private def filter_parser_options(parser, builder) + example_option(parser, builder) + line_option(parser, builder) + location_option(parser, builder) + end + + # Adds the example filter option to the parser. + private def example_option(parser, builder) + parser.on("-e", "--example STRING", "Run examples whose full nested names include STRING") do |pattern| + Log.debug { "Filtering for examples named '#{pattern}' (-e '#{pattern}')" } + filter = NameExampleFilter.new(pattern) + builder.add_example_filter(filter) + end + end + + # Adds the line filter option to the parser. + private def line_option(parser, builder) + parser.on("-l", "--line LINE", "Run examples whose line matches LINE") do |line| + Log.debug { "Filtering for examples on line #{line} (-l #{line})" } + filter = LineExampleFilter.new(line.to_i) + builder.add_example_filter(filter) + end + end + + # Adds the location filter option to the parser. + private def location_option(parser, builder) + parser.on("--location FILE:LINE", "Run the example at line 'LINE' in the file 'FILE', multiple allowed") do |location| + Log.debug { "Filtering for examples at #{location} (--location '#{location}')" } + location = Location.parse(location) + filter = LocationExampleFilter.new(location) + builder.add_example_filter(filter) + end + end + + # Adds options to the parser for changing output. + private def output_parser_options(parser, builder) + verbose_option(parser, builder) + help_option(parser, builder) + profile_option(parser, builder) + json_option(parser, builder) + tap_option(parser, builder) + junit_option(parser, builder) + no_color_option(parser, builder) + end + + # Adds the verbose output option to the parser. + private def verbose_option(parser, builder) + parser.on("-v", "--verbose", "Verbose output using document formatter") do + Log.debug { "Setting output format to document (-v)" } + builder.formatter = Formatting::DocumentFormatter.new + end + end + + # Adds the help output option to the parser. + private def help_option(parser, builder) + parser.on("-h", "--help", "Show this help") do + puts parser + exit + end + end + + # Adds the profile output option to the parser. + private def profile_option(parser, builder) + parser.on("-p", "--profile", "Display the 10 slowest specs") do + Log.debug { "Enabling timing information (-p)" } + builder.profile + end + end + + # Adds the JSON output option to the parser. + private def json_option(parser, builder) + parser.on("--json", "Generate JSON output") do + Log.debug { "Setting output format to JSON (--json)" } + builder.formatter = Formatting::JSONFormatter.new + end + end + + # Adds the TAP output option to the parser. + private def tap_option(parser, builder) + parser.on("--tap", "Generate TAP output (Test Anything Protocol)") do + Log.debug { "Setting output format to TAP (--tap)" } + builder.formatter = Formatting::TAPFormatter.new + end + end + + # Adds the JUnit output option to the parser. + private def junit_option(parser, builder) + parser.on("--junit_output OUTPUT_DIR", "Generate JUnit XML output") do |output_dir| + Log.debug { "Setting output format to JUnit XML (--junit_output '#{output_dir}')" } + formatter = Formatting::JUnitFormatter.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 + Log.debug { "Disabling color output (--no-color)" } + Colorize.enabled = false + end + end + end + end +end diff --git a/src/spectator/config_source.cr b/src/spectator/config_source.cr deleted file mode 100644 index 472ea8a..0000000 --- a/src/spectator/config_source.cr +++ /dev/null @@ -1,10 +0,0 @@ -require "./config_builder" - -module Spectator - # Interface for all places that configuration can originate. - abstract class ConfigSource - # Applies the specified configuration to a builder. - # Calling this method from multiple sources builds up the final configuration. - abstract def apply(builder : ConfigBuilder) : Nil - end -end From 72b2e7ebcb8b4a83c7b2592874cee46051c8dd5d Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 8 May 2021 23:46:19 -0600 Subject: [PATCH 219/399] Fix paths and references --- src/spectator.cr | 4 ++-- src/spectator/config/builder.cr | 3 ++- src/spectator/includes.cr | 4 ---- src/spectator/spec/builder.cr | 9 ++++----- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/spectator.cr b/src/spectator.cr index bff5ef3..515d9ad 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -43,14 +43,14 @@ module Spectator exit(1) if autorun? && !run end - @@config_builder = ConfigBuilder.new + @@config_builder = Config::Builder.new @@config : Config? # Provides a means to configure how Spectator will run and report tests. # A `ConfigBuilder` is yielded to allow changing the configuration. # NOTE: The configuration set here can be overriden # with a `.spectator` file and command-line arguments. - def configure(& : ConfigBuilder -> _) : Nil + def configure(& : Config::Builder -> _) : Nil yield @@config_builder end diff --git a/src/spectator/config/builder.cr b/src/spectator/config/builder.cr index a61a465..39daeee 100644 --- a/src/spectator/config/builder.cr +++ b/src/spectator/config/builder.cr @@ -1,7 +1,7 @@ require "../composite_example_filter" require "../example_filter" +require "../formatting" require "../null_example_filter" -require "../reporters" require "../run_flags" module Spectator @@ -52,6 +52,7 @@ module Spectator case (formatters = self.formatters) when .one? then formatters.first else Formatting::BroadcastFormatter.new(formatters) + end end # Enables fail-fast mode. diff --git a/src/spectator/includes.cr b/src/spectator/includes.cr index e931000..9e9dd5f 100644 --- a/src/spectator/includes.cr +++ b/src/spectator/includes.cr @@ -7,11 +7,8 @@ require "./abstract_expression" require "./anything" require "./block" -require "./command_line_arguments_config_source" require "./composite_example_filter" -require "./config_builder" require "./config" -require "./config_source" require "./context" require "./context_delegate" require "./context_method" @@ -52,6 +49,5 @@ require "./result" require "./spec" require "./tags" require "./test_context" -require "./test_suite" require "./value" require "./wrapper" diff --git a/src/spectator/spec/builder.cr b/src/spectator/spec/builder.cr index dd76a7b..6aa6188 100644 --- a/src/spectator/spec/builder.cr +++ b/src/spectator/spec/builder.cr @@ -1,5 +1,4 @@ require "../config" -require "../config_builder" require "../example" require "../example_context_method" require "../example_group" @@ -173,10 +172,10 @@ module Spectator end # Builds the configuration to use for the spec. - # A `ConfigBuilder` is yielded to the block provided to this method. + # A `Config::Builder` is yielded to the block provided to this method. # That builder will be used to create the configuration. - def configure(& : ConfigBuilder -> _) : Nil - builder = ConfigBuilder.new + def configure(& : Config::Builder -> _) : Nil + builder = Config::Builder.new yield builder @config = builder.build end @@ -206,7 +205,7 @@ module Spectator # Retrieves the configuration. # If one wasn't previously set, a default configuration is used. private def config : Config - @config || ConfigBuilder.default + @config || Config.default end end end From b8b6b3b609fda2340978a9c535124dae15a46963 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 12 May 2021 21:39:50 -0600 Subject: [PATCH 220/399] Add pass?/fail? methods to Result types --- src/spectator/fail_result.cr | 10 ++++++++++ src/spectator/pass_result.cr | 10 ++++++++++ src/spectator/pending_result.cr | 10 ++++++++++ src/spectator/result.cr | 6 ++++++ 4 files changed, 36 insertions(+) diff --git a/src/spectator/fail_result.cr b/src/spectator/fail_result.cr index c28aa6d..15d1f56 100644 --- a/src/spectator/fail_result.cr +++ b/src/spectator/fail_result.cr @@ -25,6 +25,16 @@ module Spectator visitor.failure(yield self) end + # Indicates whether the example passed. + def pass? : Bool + false + end + + # Indicates whether the example failed. + def fail? : Bool + true + end + # One-word description of the result. def to_s(io) io << "fail" diff --git a/src/spectator/pass_result.cr b/src/spectator/pass_result.cr index e059057..1093590 100644 --- a/src/spectator/pass_result.cr +++ b/src/spectator/pass_result.cr @@ -13,6 +13,16 @@ module Spectator visitor.pass(yield self) end + # Indicates whether the example passed. + def pass? : Bool + true + end + + # Indicates whether the example failed. + def fail? : Bool + false + end + # One-word description of the result. def to_s(io) io << "pass" diff --git a/src/spectator/pending_result.cr b/src/spectator/pending_result.cr index 6d68e19..364f2f4 100644 --- a/src/spectator/pending_result.cr +++ b/src/spectator/pending_result.cr @@ -21,6 +21,16 @@ module Spectator visitor.pending(yield self) end + # Indicates whether the example passed. + def pass? : Bool + false + end + + # Indicates whether the example failed. + def fail? : Bool + false + end + # One-word description of the result. def to_s(io) io << "pending" diff --git a/src/spectator/result.cr b/src/spectator/result.cr index f20f483..d2ef98b 100644 --- a/src/spectator/result.cr +++ b/src/spectator/result.cr @@ -17,6 +17,12 @@ module Spectator # This is the visitor design pattern. abstract def accept(visitor) + # Indicates whether the example passed. + abstract def pass? : Bool + + # Indicates whether the example failed. + abstract def fail? : Bool + # Creates a JSON object from the result information. def to_json(json : ::JSON::Builder, example) json.object do From ff084bb3cdf30f74f87aa3403f516502e0cccae2 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 12 May 2021 21:40:48 -0600 Subject: [PATCH 221/399] Fix dumb mistakes --- src/spectator/harness.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 231c959..6a44ea8 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -91,10 +91,10 @@ module Spectator # Yields to run the test code and returns information about the outcome. # Returns a tuple with the elapsed time and an error if one occurred (otherwise nil). - private def capture : Tuple(Time, Exception?) + private def capture : Tuple(Time::Span, Exception?) error = nil elapsed = Time.measure do - error = catch_error { yield } + error = catch { yield } end {elapsed, error} end From 83c4b01e84ca3c33fed641866bcf61a0d2c2c1e5 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 12 May 2021 21:41:12 -0600 Subject: [PATCH 222/399] Set up placeholder formatters --- src/spectator/formatting/broadcast_formatter.cr | 3 +-- src/spectator/formatting/document_formatter.cr | 14 ++++++++++++++ src/spectator/formatting/json_formatter.cr | 14 ++++++++++++++ src/spectator/formatting/junit_formatter.cr | 17 +++++++++++++++++ src/spectator/formatting/tap_formatter.cr | 14 ++++++++++++++ 5 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 src/spectator/formatting/document_formatter.cr create mode 100644 src/spectator/formatting/json_formatter.cr create mode 100644 src/spectator/formatting/junit_formatter.cr create mode 100644 src/spectator/formatting/tap_formatter.cr diff --git a/src/spectator/formatting/broadcast_formatter.cr b/src/spectator/formatting/broadcast_formatter.cr index 3992777..5cc58b0 100644 --- a/src/spectator/formatting/broadcast_formatter.cr +++ b/src/spectator/formatting/broadcast_formatter.cr @@ -6,8 +6,7 @@ module Spectator::Formatting class BroadcastFormatter < Formatter # Creates the broadcast formatter. # Takes a collection of formatters to pass events along to. - def initialize(formatters : Enumerable(Formatter)) - @formatters = formatters.to_a + def initialize(@formatters : Enumerable(Formatter)) end end end diff --git a/src/spectator/formatting/document_formatter.cr b/src/spectator/formatting/document_formatter.cr new file mode 100644 index 0000000..c5cb5db --- /dev/null +++ b/src/spectator/formatting/document_formatter.cr @@ -0,0 +1,14 @@ +require "./formatter" + +module Spectator::Formatting + class DocumentFormatter < Formatter + def pass(notification) + end + + def fail(notification) + end + + def pending(notification) + end + end +end diff --git a/src/spectator/formatting/json_formatter.cr b/src/spectator/formatting/json_formatter.cr new file mode 100644 index 0000000..8ef2e24 --- /dev/null +++ b/src/spectator/formatting/json_formatter.cr @@ -0,0 +1,14 @@ +require "./formatter" + +module Spectator::Formatting + class JSONFormatter < Formatter + def pass(notification) + end + + def fail(notification) + end + + def pending(notification) + end + end +end diff --git a/src/spectator/formatting/junit_formatter.cr b/src/spectator/formatting/junit_formatter.cr new file mode 100644 index 0000000..f05adc8 --- /dev/null +++ b/src/spectator/formatting/junit_formatter.cr @@ -0,0 +1,17 @@ +require "./formatter" + +module Spectator::Formatting + class JUnitFormatter < Formatter + def initialize(output_dir) + end + + def pass(notification) + end + + def fail(notification) + end + + def pending(notification) + end + end +end diff --git a/src/spectator/formatting/tap_formatter.cr b/src/spectator/formatting/tap_formatter.cr new file mode 100644 index 0000000..69f8d86 --- /dev/null +++ b/src/spectator/formatting/tap_formatter.cr @@ -0,0 +1,14 @@ +require "./formatter" + +module Spectator::Formatting + class TAPFormatter < Formatter + def pass(notification) + end + + def fail(notification) + end + + def pending(notification) + end + end +end From 81f509c0836b72cc308ac077b9ed3966ec41c9f6 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 12 May 2021 21:41:34 -0600 Subject: [PATCH 223/399] Get config, spec, builder, and runner working together --- src/spectator.cr | 3 +- src/spectator/config.cr | 3 + src/spectator/config/builder.cr | 4 +- .../config/cli_arguments_applicator.cr | 1 + src/spectator/spec.cr | 2 +- src/spectator/spec/runner.cr | 75 +++++++++++-------- 6 files changed, 52 insertions(+), 36 deletions(-) diff --git a/src/spectator.cr b/src/spectator.cr index 515d9ad..bc0130b 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -1,3 +1,4 @@ +require "colorize" require "log" require "./spectator/includes" @@ -67,7 +68,7 @@ module Spectator # Build the spec and run it. DSL::Builder.config = config spec = DSL::Builder.build - spec.run(config.example_filter) + spec.run rescue ex # Catch all unhandled exceptions here. # Examples are already wrapped, so any exceptions they throw are caught. diff --git a/src/spectator/config.cr b/src/spectator/config.cr index ddcf098..9b0d891 100644 --- a/src/spectator/config.cr +++ b/src/spectator/config.cr @@ -17,6 +17,9 @@ module Spectator # Seed used for random number generation. getter random_seed : UInt64 + # Filter used to select which examples to run. + getter example_filter : ExampleFilter + # Creates a new configuration. # Properties are pulled from *source*. # Typically, *source* is a `Config::Builder`. diff --git a/src/spectator/config/builder.cr b/src/spectator/config/builder.cr index 39daeee..4c8fa6d 100644 --- a/src/spectator/config/builder.cr +++ b/src/spectator/config/builder.cr @@ -13,9 +13,11 @@ module Spectator # Seed used for random number generation. property random_seed : UInt64 = Random.rand(UInt64) + # Toggles indicating how the test spec should execute. + property run_flags = RunFlags::None + @primary_formatter : Formatting::Formatter? @additional_formatters = [] of Formatting::Formatter - @run_flags = RunFlags::None @filters = [] of ExampleFilter # Creates a configuration. diff --git a/src/spectator/config/cli_arguments_applicator.cr b/src/spectator/config/cli_arguments_applicator.cr index 396157f..75b32c5 100644 --- a/src/spectator/config/cli_arguments_applicator.cr +++ b/src/spectator/config/cli_arguments_applicator.cr @@ -1,3 +1,4 @@ +require "colorize" require "option_parser" require "../formatting" require "../line_example_filter" diff --git a/src/spectator/spec.cr b/src/spectator/spec.cr index 03f3d51..06ba0c4 100644 --- a/src/spectator/spec.cr +++ b/src/spectator/spec.cr @@ -14,7 +14,7 @@ module Spectator # Runs all selected examples and returns the results. def run - Runner.new(examples, @config).run + Runner.new(examples, @config.run_flags).run end # Selects and shuffles the examples that should run. diff --git a/src/spectator/spec/runner.cr b/src/spectator/spec/runner.cr index 69d8940..a0c1e63 100644 --- a/src/spectator/spec/runner.cr +++ b/src/spectator/spec/runner.cr @@ -1,4 +1,5 @@ require "../example" +require "../run_flags" module Spectator class Spec @@ -7,7 +8,7 @@ module Spectator # Creates the runner. # The collection of *examples* should be pre-filtered and shuffled. # This runner will run each example in the order provided. - def initialize(@examples : Enumerable(Example), @config : Config) + def initialize(@examples : Enumerable(Example), @run_flags = RunFlags::None) end # Runs the spec. @@ -16,32 +17,25 @@ module Spectator # True will be returned if the spec 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)) + executed = [] of Example + elapsed = Time.measure { executed = run_examples } - # Run all examples and capture the results. - examples = Array(Example).new(@suite.size) - elapsed = Time.measure do - collect_results(examples) - end + # TODO: Generate a report and pass it along to the formatter. - # Generate a report and pass it along to the formatter. - remaining = @suite.size - examples.size - seed = (@config.random_seed if @config.randomize?) - report = Report.new(examples, elapsed, remaining, @config.fail_blank?, seed) - @config.each_formatter(&.end_suite(report, profile(report))) - - !report.failed? + false # TODO: Report real result end - # Runs all examples and adds them to a list. - private def collect_results(examples) - example_order.each do |example| - result = run_example(example) - examples << example - if @config.fail_fast? && result.is_a?(FailResult) - example.group.call_once_after_all - break + # Attempts to run all examples. + # Returns a list of examples that ran. + private def run_examples + Array(Example).new(example_count).tap do |executed| + @examples.each do |example| + result = run_example(example) + executed << example + + # Bail out if the example failed + # and configured to stop after the first failure. + break fail_fast if fail_fast? && result.fail? end end end @@ -49,18 +43,15 @@ module Spectator # 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(example)) - result + if dry_run? + dry_run_result + else + example.run + end end - # Creates a fake result for an example. - private def dry_run_result(example) + # Creates a fake result. + private def dry_run_result expectations = [] of Expectation PassResult.new(Time::Span.zero, expectations) end @@ -69,6 +60,24 @@ module Spectator private def profile(report) Profile.generate(report) if @config.profile? end + + # Indicates whether examples should be simulated, but not run. + private def dry_run? + @run_flags.dry_run? + end + + # Indicates whether test execution should stop after the first failure. + private def fail_fast? + @run_flags.fail_fast? + end + + private def fail_fast : Nil + end + + # Number of examples configured to run. + private def example_count + @examples.size + end end end end From 1ea209184e8c3918caeda1e26d0ee44b147d9f0e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 12 May 2021 21:41:56 -0600 Subject: [PATCH 224/399] Formatting --- src/spectator/config/builder.cr | 6 +++--- src/spectator/harness.cr | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/spectator/config/builder.cr b/src/spectator/config/builder.cr index 4c8fa6d..ca086be 100644 --- a/src/spectator/config/builder.cr +++ b/src/spectator/config/builder.cr @@ -53,7 +53,7 @@ module Spectator protected def formatter case (formatters = self.formatters) when .one? then formatters.first - else Formatting::BroadcastFormatter.new(formatters) + else Formatting::BroadcastFormatter.new(formatters) end end @@ -165,8 +165,8 @@ module Spectator protected def example_filter case (filters = @filters) when .empty? then NullExampleFilter.new - when .one? then filters.first - else CompositeExampleFilter.new(filters) + when .one? then filters.first + else CompositeExampleFilter.new(filters) end end end diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 6a44ea8..27b2679 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -92,7 +92,7 @@ module Spectator # Yields to run the test code and returns information about the outcome. # Returns a tuple with the elapsed time and an error if one occurred (otherwise nil). private def capture : Tuple(Time::Span, Exception?) - error = nil + error = nil elapsed = Time.measure do error = catch { yield } end From 4eb457f1970ae1a79664816662608bae044055d9 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 15 May 2021 17:01:29 -0600 Subject: [PATCH 225/399] Define and document formatter methods --- .../formatting/document_formatter.cr | 8 -- src/spectator/formatting/formatter.cr | 103 ++++++++++++++++++ src/spectator/formatting/json_formatter.cr | 8 -- src/spectator/formatting/junit_formatter.cr | 9 -- .../formatting/progress_formatter.cr | 8 -- src/spectator/formatting/tap_formatter.cr | 8 -- src/spectator/spec/runner.cr | 4 +- 7 files changed, 106 insertions(+), 42 deletions(-) diff --git a/src/spectator/formatting/document_formatter.cr b/src/spectator/formatting/document_formatter.cr index c5cb5db..61c298e 100644 --- a/src/spectator/formatting/document_formatter.cr +++ b/src/spectator/formatting/document_formatter.cr @@ -2,13 +2,5 @@ require "./formatter" module Spectator::Formatting class DocumentFormatter < Formatter - def pass(notification) - end - - def fail(notification) - end - - def pending(notification) - end end end diff --git a/src/spectator/formatting/formatter.cr b/src/spectator/formatting/formatter.cr index 125c149..bfbbec2 100644 --- a/src/spectator/formatting/formatter.cr +++ b/src/spectator/formatting/formatter.cr @@ -2,6 +2,109 @@ module Spectator::Formatting # Base class and interface used to notify systems of events. # This is typically used for producing output from test results, # but can also be used to send data to external systems. + # + # All event methods are implemented as no-ops. + # To respond to an event, override its method. + # Every method receives a notification object containing information about the event. + # + # Methods are called in this order: + # 1. `#start` + # 2. `#example_started` + # 3. `#example_finished` + # 4. `#example_passed` + # 5. `#example_pending` + # 6. `#example_failed` + # 7. `#stop` + # 8. `#start_dump` + # 9. `#dump_pending` + # 10. `#dump_failures` + # 11. `#dump_summary` + # 12. `#dump_profile` + # 13. `#close` + # + # Only one of the `#example_passed`, `#example_pending`, or `#example_failed` methods + # will be called after `#example_finished`, depending on the outcome of the test. + # + # The "dump" methods are called after all tests that will run have run. + # They are provided summarized information. abstract class Formatter + # This method is the first method to be invoked + # and will be called only once. + # It is called before any examples run. + def start(_notification) + end + + # Invoked just before an example runs. + # This method is called once for every example. + def example_started(_notification) + end + + # Invoked just after an example completes. + # This method is called once for every example. + # One of `#example_passed`, `#example_pending` or `#example_failed` + # will be called immediately after this method, depending on the example's result. + def example_finished(_notification) + end + + # Invoked after an example completes successfully. + # This is called right after `#example_finished`. + def example_passed(_notification) + end + + # Invoked after an example is skipped or marked as pending. + # This is called right after `#example_finished`. + def example_pending(_notification) + end + + # Invoked after an example fails. + # This is called right after `#example_finished`. + # Errors are considered failures and will cause this method to be called. + def example_failed(_notification) + end + + # Called whenever the example or framework produces a message. + # This is typically used for logging. + def message(_notification) + end + + # Invoked after all tests that will run have completed. + # When this method is called, it should be considered that the testing is done. + # Summary (dump) methods will be called after this. + def stop(_notification) + end + + # Invoked after all examples finished. + # Indicates that summarized report data is about to be produced. + # This method is called after `#stop` and before `#dump_pending`. + def start_dump(_notification) + end + + # Invoked after testing completes with a list of pending examples. + # This method will be called with an empty list if there were no pending (skipped) examples. + # Called after `#start_dump` and before `#dump_failures`. + def dump_pending(_notification) + end + + # Invoked after testing completes with a list of failed examples. + # This method will be called with an empty list if there were no failures. + # Called after `#dump_pending` and before `#dump_summary`. + def dump_failures(_notification) + end + + # Invoked after testing completes with summarized information from the test suite. + # Called after `#dump_failures` and before `#dump_profile`. + def dump_summary(_notification) + end + + # Invoked after testing completes with profiling information. + # This method is only called if profiling is enabled. + # Called after `#dump_summary` and before `#close`. + def dump_profile(_notification) + end + + # Invoked at the end of the program. + # Allows the formatter to perform any cleanup and teardown. + def close(_notification) + end end end diff --git a/src/spectator/formatting/json_formatter.cr b/src/spectator/formatting/json_formatter.cr index 8ef2e24..996e94a 100644 --- a/src/spectator/formatting/json_formatter.cr +++ b/src/spectator/formatting/json_formatter.cr @@ -2,13 +2,5 @@ require "./formatter" module Spectator::Formatting class JSONFormatter < Formatter - def pass(notification) - end - - def fail(notification) - end - - def pending(notification) - end end end diff --git a/src/spectator/formatting/junit_formatter.cr b/src/spectator/formatting/junit_formatter.cr index f05adc8..32cec45 100644 --- a/src/spectator/formatting/junit_formatter.cr +++ b/src/spectator/formatting/junit_formatter.cr @@ -4,14 +4,5 @@ module Spectator::Formatting class JUnitFormatter < Formatter def initialize(output_dir) end - - def pass(notification) - end - - def fail(notification) - end - - def pending(notification) - end end end diff --git a/src/spectator/formatting/progress_formatter.cr b/src/spectator/formatting/progress_formatter.cr index dbb0f6e..05321aa 100644 --- a/src/spectator/formatting/progress_formatter.cr +++ b/src/spectator/formatting/progress_formatter.cr @@ -4,13 +4,5 @@ module Spectator::Formatting # Output formatter that produces a single character for each test as it completes. # A '.' indicates a pass, 'F' a failure, and '*' a skipped or pending test. class ProgressFormatter < Formatter - def pass(notification) - end - - def fail(notification) - end - - def pending(notification) - end end end diff --git a/src/spectator/formatting/tap_formatter.cr b/src/spectator/formatting/tap_formatter.cr index 69f8d86..3da03db 100644 --- a/src/spectator/formatting/tap_formatter.cr +++ b/src/spectator/formatting/tap_formatter.cr @@ -2,13 +2,5 @@ require "./formatter" module Spectator::Formatting class TAPFormatter < Formatter - def pass(notification) - end - - def fail(notification) - end - - def pending(notification) - end end end diff --git a/src/spectator/spec/runner.cr b/src/spectator/spec/runner.cr index a0c1e63..4bc9d7b 100644 --- a/src/spectator/spec/runner.cr +++ b/src/spectator/spec/runner.cr @@ -8,7 +8,9 @@ module Spectator # Creates the runner. # The collection of *examples* should be pre-filtered and shuffled. # This runner will run each example in the order provided. - def initialize(@examples : Enumerable(Example), @run_flags = RunFlags::None) + # The *formatter* will be called for various events. + def initialize(@examples : Enumerable(Example), + @formatter : Formatting::Formatter, @run_flags = RunFlags::None) end # Runs the spec. From 618e9e195a2f128a9d11ffe0f261210dc2fd720a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 15 May 2021 18:31:52 -0600 Subject: [PATCH 226/399] Begin triggering events for formatters --- src/spectator/formatting/formatter.cr | 6 +++ src/spectator/formatting/notifications.cr | 7 ++++ src/spectator/spec.cr | 2 +- src/spectator/spec/events.cr | 49 +++++++++++++++++++++++ src/spectator/spec/runner.cr | 15 ++++++- 5 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 src/spectator/formatting/notifications.cr create mode 100644 src/spectator/spec/events.cr diff --git a/src/spectator/formatting/formatter.cr b/src/spectator/formatting/formatter.cr index bfbbec2..22e59eb 100644 --- a/src/spectator/formatting/formatter.cr +++ b/src/spectator/formatting/formatter.cr @@ -31,11 +31,13 @@ module Spectator::Formatting # This method is the first method to be invoked # and will be called only once. # It is called before any examples run. + # The *notification* will be a `StartNotification` type of object. def start(_notification) end # Invoked just before an example runs. # This method is called once for every example. + # The *notification* will be an `ExampleNotification` type of object. def example_started(_notification) end @@ -43,22 +45,26 @@ module Spectator::Formatting # This method is called once for every example. # One of `#example_passed`, `#example_pending` or `#example_failed` # will be called immediately after this method, depending on the example's result. + # The *notification* will be an `ExampleNotification` type of object. def example_finished(_notification) end # Invoked after an example completes successfully. # This is called right after `#example_finished`. + # The *notification* will be an `ExampleNotification` type of object. def example_passed(_notification) end # Invoked after an example is skipped or marked as pending. # This is called right after `#example_finished`. + # The *notification* will be an `ExampleNotification` type of object. def example_pending(_notification) end # Invoked after an example fails. # This is called right after `#example_finished`. # Errors are considered failures and will cause this method to be called. + # The *notification* will be an `ExampleNotification` type of object. def example_failed(_notification) end diff --git a/src/spectator/formatting/notifications.cr b/src/spectator/formatting/notifications.cr new file mode 100644 index 0000000..afe57e2 --- /dev/null +++ b/src/spectator/formatting/notifications.cr @@ -0,0 +1,7 @@ +require "../example" + +module Spectator::Formatting + record StartNotification, example_count : Int32 + + record ExampleNotification, example : Example +end diff --git a/src/spectator/spec.cr b/src/spectator/spec.cr index 06ba0c4..df84a54 100644 --- a/src/spectator/spec.cr +++ b/src/spectator/spec.cr @@ -14,7 +14,7 @@ module Spectator # Runs all selected examples and returns the results. def run - Runner.new(examples, @config.run_flags).run + Runner.new(examples, @config.formatter, @config.run_flags).run end # Selects and shuffles the examples that should run. diff --git a/src/spectator/spec/events.cr b/src/spectator/spec/events.cr new file mode 100644 index 0000000..b22eee0 --- /dev/null +++ b/src/spectator/spec/events.cr @@ -0,0 +1,49 @@ +module Spectator + class Spec + # Mix-in for announcing events from a `Runner`. + # All events invoke their corresponding method on the formatter. + module Events + # Triggers the 'start' event. + # See `Formatting::Formatter#start` + private def start + notification = Formatting::StartNotification.new(example_count) + formatter.start(notification) + end + + # Triggers the 'example started' event. + # Must be passed the *example* about to run. + # See `Formatting::Formatter#example_started` + private def example_started(example) + notification = Formatting::ExampleNotification.new(example) + formatter.example_started(notification) + end + + # Triggers the 'example started' event. + # Also triggers the example result event corresponding to the example's outcome. + # Must be passed the completed *example*. + # See `Formatting::Formatter#example_finished` + private def example_finished(example) + notification = Formatting::ExampleNotification.new(example) + formatter.example_started(notification) + + case example.result + when .fail? then formatter.example_failed(notification) + when .pass? then formatter.example_passed(notification) + else formatter.example_pending(notification) + end + end + + # Triggers the 'stop' event. + # See `Formatting::Formatter#stop` + private def stop + formatter.stop(nil) + end + + # Triggers the 'close' event. + # See `Formatting::Formatter#close` + private def close + formatter.close(nil) + end + end + end +end diff --git a/src/spectator/spec/runner.cr b/src/spectator/spec/runner.cr index 4bc9d7b..850c3e0 100644 --- a/src/spectator/spec/runner.cr +++ b/src/spectator/spec/runner.cr @@ -1,10 +1,16 @@ require "../example" require "../run_flags" +require "./events" module Spectator class Spec # Logic for executing examples and collecting results. private struct Runner + include Events + + # Formatter to send events to. + private getter formatter : Formatting::Formatter + # Creates the runner. # The collection of *examples* should be pre-filtered and shuffled. # This runner will run each example in the order provided. @@ -19,12 +25,16 @@ module Spectator # True will be returned if the spec ran successfully, # or false if there was at least one failure. def run : Bool + start executed = [] of Example elapsed = Time.measure { executed = run_examples } + stop # TODO: Generate a report and pass it along to the formatter. false # TODO: Report real result + ensure + close end # Attempts to run all examples. @@ -45,11 +55,14 @@ module Spectator # Runs a single example and returns the result. # The formatter is given the example and result information. private def run_example(example) - if dry_run? + example_started(example) + result = if dry_run? dry_run_result else example.run end + example_finished(example) + result end # Creates a fake result. From d5c4d5e822b2d5318441c0aa296b03cb4add81e9 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 15 May 2021 19:42:59 -0600 Subject: [PATCH 227/399] Call `fail` instead of `failure` on visitor --- src/spectator/fail_result.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/fail_result.cr b/src/spectator/fail_result.cr index 15d1f56..a0b01e0 100644 --- a/src/spectator/fail_result.cr +++ b/src/spectator/fail_result.cr @@ -17,12 +17,12 @@ module Spectator # Calls the `failure` method on *visitor*. def accept(visitor) - visitor.failure + visitor.fail end # Calls the `failure` method on *visitor*. def accept(visitor) - visitor.failure(yield self) + visitor.fail(yield self) end # Indicates whether the example passed. From a36982d6d6bdf2e7c9d81f86e04077915afb395c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 15 May 2021 19:43:13 -0600 Subject: [PATCH 228/399] Use visitor pattern --- src/spectator/spec/events.cr | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/spectator/spec/events.cr b/src/spectator/spec/events.cr index b22eee0..1accae3 100644 --- a/src/spectator/spec/events.cr +++ b/src/spectator/spec/events.cr @@ -24,13 +24,9 @@ module Spectator # See `Formatting::Formatter#example_finished` private def example_finished(example) notification = Formatting::ExampleNotification.new(example) + visitor = ResultVisitor.new(formatter, notification) formatter.example_started(notification) - - case example.result - when .fail? then formatter.example_failed(notification) - when .pass? then formatter.example_passed(notification) - else formatter.example_pending(notification) - end + example.result.accept(visitor) end # Triggers the 'stop' event. @@ -44,6 +40,34 @@ module Spectator private def close formatter.close(nil) end + + # Provides methods for the various result types. + private struct ResultVisitor + # Creates the visitor. + # Requires the *formatter* to notify and the *notification* to send it. + def initialize(@formatter : Formatting::Formatter, @notification : Formatting::ExampleNotification) + end + + # Invokes the example passed method. + def pass + @formatter.example_passed(@notification) + end + + # Invokes the example failed method. + def fail + @formatter.example_failed(@notification) + end + + # Invokes the example failed method. + def error + @formatter.example_failed(@notification) + end + + # Invokes the example pending method. + def pending + @formatter.example_pending(@notification) + end + end end end end From e2f40519277d3d9cb61389cbdbdb5a6b329e655b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 15 May 2021 19:44:06 -0600 Subject: [PATCH 229/399] Split error handler from failure method --- src/spectator/formatting/formatter.cr | 10 +++++++++- src/spectator/spec/events.cr | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/spectator/formatting/formatter.cr b/src/spectator/formatting/formatter.cr index 22e59eb..abdc500 100644 --- a/src/spectator/formatting/formatter.cr +++ b/src/spectator/formatting/formatter.cr @@ -63,11 +63,19 @@ module Spectator::Formatting # Invoked after an example fails. # This is called right after `#example_finished`. - # Errors are considered failures and will cause this method to be called. # The *notification* will be an `ExampleNotification` type of object. + # + # NOTE: Errors are normally considered failures, + # however `#example_error` is called instead if one occurs in an exmaple. def example_failed(_notification) end + # Invoked after an example fails from an unexpected error. + # This is called right after `#example_finished`. + # The *notification* will be an `ExampleNotification` type of object. + def example_error(_notification) + end + # Called whenever the example or framework produces a message. # This is typically used for logging. def message(_notification) diff --git a/src/spectator/spec/events.cr b/src/spectator/spec/events.cr index 1accae3..4ef8dff 100644 --- a/src/spectator/spec/events.cr +++ b/src/spectator/spec/events.cr @@ -58,9 +58,9 @@ module Spectator @formatter.example_failed(@notification) end - # Invokes the example failed method. + # Invokes the example error method. def error - @formatter.example_failed(@notification) + @formatter.example_error(@notification) end # Invokes the example pending method. From eebcba0749d2efe22269030dd91930aefe227d00 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 15 May 2021 19:45:01 -0600 Subject: [PATCH 230/399] Mostly implemented progress formatter --- .../formatting/progress_formatter.cr | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/spectator/formatting/progress_formatter.cr b/src/spectator/formatting/progress_formatter.cr index 05321aa..8f098fc 100644 --- a/src/spectator/formatting/progress_formatter.cr +++ b/src/spectator/formatting/progress_formatter.cr @@ -1,8 +1,37 @@ +require "colorize" require "./formatter" module Spectator::Formatting # Output formatter that produces a single character for each test as it completes. - # A '.' indicates a pass, 'F' a failure, and '*' a skipped or pending test. + # A '.' indicates a pass, 'F' a failure, 'E' an error, and '*' a skipped or pending test. class ProgressFormatter < Formatter + @pass_char : Colorize::Object(Char) = '.'.colorize(:green) + @fail_char : Colorize::Object(Char) = 'F'.colorize(:red) + @error_char : Colorize::Object(Char) = 'E'.colorize(:red) + @skip_char : Colorize::Object(Char) = '*'.colorize(:yellow) + + # Creates the formatter. + def initialize(@io : IO = STDOUT) + end + + # Produces a pass character. + def example_passed(_notification) + @pass_char.to_s(@io) + end + + # Produces a fail character. + def example_failed(_notification) + @fail_char.to_s(@io) + end + + # Produces an error character. + def example_error(_notification) + @error_char.to_s(@io) + end + + # Produces a skip character. + def example_pending(_notification) + @skip_char.to_s(@io) + end end end From aa03e3527de594f213a634af29760f8be8481cef Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 15 May 2021 19:45:17 -0600 Subject: [PATCH 231/399] Formatting --- src/spectator/spec/runner.cr | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/spectator/spec/runner.cr b/src/spectator/spec/runner.cr index 850c3e0..3ffe030 100644 --- a/src/spectator/spec/runner.cr +++ b/src/spectator/spec/runner.cr @@ -16,7 +16,7 @@ module Spectator # This runner will run each example in the order provided. # The *formatter* will be called for various events. def initialize(@examples : Enumerable(Example), - @formatter : Formatting::Formatter, @run_flags = RunFlags::None) + @formatter : Formatting::Formatter, @run_flags = RunFlags::None) end # Runs the spec. @@ -33,6 +33,7 @@ module Spectator # TODO: Generate a report and pass it along to the formatter. false # TODO: Report real result + ensure close end @@ -57,10 +58,10 @@ module Spectator private def run_example(example) example_started(example) result = if dry_run? - dry_run_result - else - example.run - end + dry_run_result + else + example.run + end example_finished(example) result end From 5da21f8edec396366428ab6e5f140ee13b7f893b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 15 May 2021 19:51:44 -0600 Subject: [PATCH 232/399] Update Ameba and address issues --- shard.yml | 2 +- src/spectator/matchers/array_matcher.cr | 4 ++-- src/spectator/matchers/respond_matcher.cr | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/shard.yml b/shard.yml index 830c2c0..cc50ced 100644 --- a/shard.yml +++ b/shard.yml @@ -13,4 +13,4 @@ license: MIT development_dependencies: ameba: github: crystal-ameba/ameba - version: ~> 0.13.1 + version: ~> 0.14.3 diff --git a/src/spectator/matchers/array_matcher.cr b/src/spectator/matchers/array_matcher.cr index 77dc14c..bb355a3 100644 --- a/src/spectator/matchers/array_matcher.cr +++ b/src/spectator/matchers/array_matcher.cr @@ -110,9 +110,9 @@ module Spectator::Matchers end end.reject do |(_, count)| count <= 0 - end.map do |(element, count)| + end.flat_map do |(element, count)| Array.new(count, element) - end.flatten + end end private def unexpected(value, label) diff --git a/src/spectator/matchers/respond_matcher.cr b/src/spectator/matchers/respond_matcher.cr index 6d36f80..1f73f57 100644 --- a/src/spectator/matchers/respond_matcher.cr +++ b/src/spectator/matchers/respond_matcher.cr @@ -27,7 +27,8 @@ module Spectator::Matchers # A successful match with `#match` should normally fail for this method, and vice-versa. def negated_match(actual : Expression(T)) : MatchData forall T snapshot = snapshot_values(actual.value) - if snapshot.values.any? + # Intentionally check truthiness of each value. + if snapshot.values.any? # ameba:disable Performance/AnyInsteadOfEmpty FailedMatchData.new(description, "#{actual.label} responds to #{label}", values(snapshot).to_a) else SuccessfulMatchData.new(description) From ba2922e655066e782d20985f0c6e28bf6b099174 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 May 2021 10:12:40 -0600 Subject: [PATCH 233/399] Implement broadcast formatter methods --- .../formatting/broadcast_formatter.cr | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/src/spectator/formatting/broadcast_formatter.cr b/src/spectator/formatting/broadcast_formatter.cr index 5cc58b0..47a2aae 100644 --- a/src/spectator/formatting/broadcast_formatter.cr +++ b/src/spectator/formatting/broadcast_formatter.cr @@ -8,5 +8,80 @@ module Spectator::Formatting # Takes a collection of formatters to pass events along to. def initialize(@formatters : Enumerable(Formatter)) end + + # Forwards the event to other formatters. + def start(notification) + @formatters.each(&.start(notification)) + end + + # :ditto: + def example_started(notification) + @formatters.each(&.example_started(notification)) + end + + # :ditto: + def example_finished(notification) + @formatters.each(&.example_finished(notification)) + end + + # :ditto: + def example_passed(notification) + @formatters.each(&.example_passed(notification)) + end + + # :ditto: + def example_pending(notification) + @formatters.each(&.example_pending(notification)) + end + + # :ditto: + def example_failed(notification) + @formatters.each(&.example_failed(notification)) + end + + # :ditto: + def example_error(notification) + @formatters.each(&.example_error(notification)) + end + + # :ditto: + def message(notification) + @formatters.each(&.message(notification)) + end + + # :ditto: + def stop(notification) + @formatters.each(&.stop(notification)) + end + + # :ditto: + def start_dump(notification) + @formatters.each(&.start_dump(notification)) + end + + # :ditto: + def dump_pending(notification) + @formatters.each(&.dump_pending(notification)) + end + + # :ditto: + def dump_failures(notification) + @formatters.each(&.dump_failures(notification)) + end + + # :ditto: + def dump_summary(notification) + @formatters.each(&.dump_summary(notification)) + end + + # :ditto: + def dump_profile(notification) + @formatters.each(&.dump_profile(notification)) + end + + # :ditto: + def close(notification) + @formatters.each(&.close(notification)) + end end end From 9a62c1386a612c727a2308d6146a4f35a2a82b20 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 May 2021 10:51:50 -0600 Subject: [PATCH 234/399] Remove parameter from start_dump and close events --- src/spectator/formatting/broadcast_formatter.cr | 8 ++++---- src/spectator/formatting/formatter.cr | 4 ++-- src/spectator/spec/events.cr | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/spectator/formatting/broadcast_formatter.cr b/src/spectator/formatting/broadcast_formatter.cr index 47a2aae..0a9db22 100644 --- a/src/spectator/formatting/broadcast_formatter.cr +++ b/src/spectator/formatting/broadcast_formatter.cr @@ -55,8 +55,8 @@ module Spectator::Formatting end # :ditto: - def start_dump(notification) - @formatters.each(&.start_dump(notification)) + def start_dump + @formatters.each(&.start_dump) end # :ditto: @@ -80,8 +80,8 @@ module Spectator::Formatting end # :ditto: - def close(notification) - @formatters.each(&.close(notification)) + def close + @formatters.each(&.close) end end end diff --git a/src/spectator/formatting/formatter.cr b/src/spectator/formatting/formatter.cr index abdc500..0928de3 100644 --- a/src/spectator/formatting/formatter.cr +++ b/src/spectator/formatting/formatter.cr @@ -90,7 +90,7 @@ module Spectator::Formatting # Invoked after all examples finished. # Indicates that summarized report data is about to be produced. # This method is called after `#stop` and before `#dump_pending`. - def start_dump(_notification) + def start_dump end # Invoked after testing completes with a list of pending examples. @@ -118,7 +118,7 @@ module Spectator::Formatting # Invoked at the end of the program. # Allows the formatter to perform any cleanup and teardown. - def close(_notification) + def close end end end diff --git a/src/spectator/spec/events.cr b/src/spectator/spec/events.cr index 4ef8dff..6b298af 100644 --- a/src/spectator/spec/events.cr +++ b/src/spectator/spec/events.cr @@ -38,7 +38,7 @@ module Spectator # Triggers the 'close' event. # See `Formatting::Formatter#close` private def close - formatter.close(nil) + formatter.close end # Provides methods for the various result types. From 59c67c26a9981c828c9325ecec854fb21e723156 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 May 2021 12:19:16 -0600 Subject: [PATCH 235/399] Result visitor methods should take result as argument --- src/spectator/error_result.cr | 2 +- src/spectator/fail_result.cr | 2 +- src/spectator/pass_result.cr | 2 +- src/spectator/pending_result.cr | 2 +- src/spectator/spec/events.cr | 8 ++++---- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/spectator/error_result.cr b/src/spectator/error_result.cr index 1f19062..989b180 100644 --- a/src/spectator/error_result.cr +++ b/src/spectator/error_result.cr @@ -7,7 +7,7 @@ module Spectator class ErrorResult < FailResult # Calls the `error` method on *visitor*. def accept(visitor) - visitor.error + visitor.error(self) end # Calls the `error` method on *visitor*. diff --git a/src/spectator/fail_result.cr b/src/spectator/fail_result.cr index a0b01e0..5bc0f06 100644 --- a/src/spectator/fail_result.cr +++ b/src/spectator/fail_result.cr @@ -17,7 +17,7 @@ module Spectator # Calls the `failure` method on *visitor*. def accept(visitor) - visitor.fail + visitor.fail(self) end # Calls the `failure` method on *visitor*. diff --git a/src/spectator/pass_result.cr b/src/spectator/pass_result.cr index 1093590..eb2bfcd 100644 --- a/src/spectator/pass_result.cr +++ b/src/spectator/pass_result.cr @@ -5,7 +5,7 @@ module Spectator class PassResult < Result # Calls the `pass` method on *visitor*. def accept(visitor) - visitor.pass + visitor.pass(self) end # Calls the `pass` method on *visitor*. diff --git a/src/spectator/pending_result.cr b/src/spectator/pending_result.cr index 364f2f4..d1d6f2c 100644 --- a/src/spectator/pending_result.cr +++ b/src/spectator/pending_result.cr @@ -13,7 +13,7 @@ module Spectator # Calls the `pending` method on the *visitor*. def accept(visitor) - visitor.pending + visitor.pending(self) end # Calls the `pending` method on the *visitor*. diff --git a/src/spectator/spec/events.cr b/src/spectator/spec/events.cr index 6b298af..e146e1c 100644 --- a/src/spectator/spec/events.cr +++ b/src/spectator/spec/events.cr @@ -49,22 +49,22 @@ module Spectator end # Invokes the example passed method. - def pass + def pass(_result) @formatter.example_passed(@notification) end # Invokes the example failed method. - def fail + def fail(_result) @formatter.example_failed(@notification) end # Invokes the example error method. - def error + def error(_result) @formatter.example_error(@notification) end # Invokes the example pending method. - def pending + def pending(_result) @formatter.example_pending(@notification) end end From 832ffbf403645ea3d23e08eba6ddd8d7d60b5740 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 May 2021 12:22:00 -0600 Subject: [PATCH 236/399] Split to make it obvious run is called --- src/spectator/spec.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/spectator/spec.cr b/src/spectator/spec.cr index df84a54..a1e80ab 100644 --- a/src/spectator/spec.cr +++ b/src/spectator/spec.cr @@ -14,7 +14,8 @@ module Spectator # Runs all selected examples and returns the results. def run - Runner.new(examples, @config.formatter, @config.run_flags).run + runner = Runner.new(examples, @config.formatter, @config.run_flags) + runner.run end # Selects and shuffles the examples that should run. From fb436d2ec47709b4d66362d8557e0af385d5e4fa Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 May 2021 12:31:01 -0600 Subject: [PATCH 237/399] Define a pending result as neither passing nor failing --- src/spectator/result.cr | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/spectator/result.cr b/src/spectator/result.cr index d2ef98b..49ed947 100644 --- a/src/spectator/result.cr +++ b/src/spectator/result.cr @@ -23,6 +23,11 @@ module Spectator # Indicates whether the example failed. abstract def fail? : Bool + # Indicates whether the example was skipped. + def pending? : Bool + !pass? && !fail? + end + # Creates a JSON object from the result information. def to_json(json : ::JSON::Builder, example) json.object do From ceb368a7f43d27c8894ad0262f054ac06588613b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 May 2021 13:19:04 -0600 Subject: [PATCH 238/399] Overhaul Report --- src/spectator/report.cr | 175 +++++++++++++++++++++------------------- 1 file changed, 92 insertions(+), 83 deletions(-) diff --git a/src/spectator/report.cr b/src/spectator/report.cr index 996fb58..9168848 100644 --- a/src/spectator/report.cr +++ b/src/spectator/report.cr @@ -3,118 +3,127 @@ require "./result" module Spectator # Outcome of all tests in a suite. class Report - include Enumerable(Example) + # Records the number of examples that had each type of result. + record Counts, pass = 0, fail = 0, error = 0, pending = 0, remaining = 0 do + # Number of examples that actually ran. + def run + pass + fail + pending + end + + # Total number of examples in the suite that were selected to run. + def total + run + remaining + end + + # Indicates whether there were skipped tests + # because of a failure causing the test suite to abort. + def remaining? + remaining_count > 0 + end + end # Total length of time it took to execute the test suite. # This includes examples, hooks, and framework processes. getter runtime : Time::Span - # Number of passing examples. - getter successful_count = 0 + # Number of examples of each result type. + getter counts : Counts - # Number of failing examples (includes errors). - getter failed_count = 0 - - # Number of examples that had errors. - getter error_count = 0 - - # Number of pending examples. - getter pending_count = 0 - - # Number of remaining tests. - # This will be greater than zero only in fail-fast mode. - getter remaining_count - - # Random seed used to determine test ordering. + # Seed used for random number generation. getter! random_seed : UInt64? # Creates the report. - # The *examples* are all examples in the test suite. + # The *examples* are all examples in the test suite that were selected to run. # The *runtime* is the total time it took to execute the suite. - # The *remaining_count* is the number of tests skipped due to fail-fast. - # The *fail_blank* flag indicates whether it is a failure if there were no tests run. + # The *counts* is the number of examples for each type of result. # The *random_seed* is the seed used for random number generation. - def initialize(@examples : Array(Example), @runtime, @remaining_count = 0, @fail_blank = false, @random_seed = nil) - @examples.each do |example| - case example.result - when PassResult - @successful_count += 1 - when ErrorResult - @error_count += 1 - @failed_count += 1 - when FailResult - @failed_count += 1 - when PendingResult - @pending_count += 1 - when Result - # This case isn't possible, but gets the compiler to stop complaining. - nil + def initialize(@examples : Array(Example), @runtime, @counts : Counts, @random_seed = nil) + end + + # Generates the report from a set of examples. + def self.generate(examples : Enumerable(Example), runtime, random_seed = nil) + counts = count_examples(examples) + new(examples.to_a, runtime, counts, random_seed) + end + + # Counts the number of examples for each result type. + private def self.count_examples(examples) + visitor = CountVisitor.new + + # Number of tests not run. + remaining = 0 + + # Iterate through each example and count the number of each type of result. + # If an example hasn't run (indicated by `Node#finished?`), then count is as "remaining." + # This typically happens in fail-fast mode. + examples.each do |example| + if example.finished? + example.result.accept(visitor) + else + remaining += 1 end end + + visitor.counts(remaining) end - # Creates the report. - # This constructor is intended for reports of subsets of results. - # The *examples* are all examples in the test suite. - # The runtime is calculated from the *results*. - def initialize(examples : Array(Example)) - runtime = examples.sum(&.result.elapsed) - initialize(examples, runtime) - end - - # Yields each example in turn. - def each - @examples.each do |example| - yield example - end - end - - # Retrieves results of all examples. - def results - @examples.each.map(&.result) - end - - # Number of examples. - def example_count - @examples.size - end - - # Number of examples run (not skipped or pending). - def examples_ran - @successful_count + @failed_count - end - - # Indicates whether the test suite failed. - def failed? - failed_count > 0 || (@fail_blank && examples_ran == 0) - end - - # Indicates whether there were skipped tests - # because of a failure causing the test to abort. - def remaining? - remaining_count > 0 - end - - # Returns a set of all failed examples. + # Returns a collection of all failed examples. def failures - @examples.select(&.result.is_a?(FailResult)) + @examples.each.select(&.result.fail?) end - # Returns a set of all errored examples. - def errors - @examples.select(&.result.is_a?(ErrorResult)) + # Returns a collection of all pending (skipped) examples. + def pending + @examples.each.select(&.result.pending?) end # Length of time it took to run just example code. # This does not include hooks, # but it does include pre- and post-conditions. def example_runtime - results.sum(&.elapsed) + @examples.sum(&.result.elapsed) end # Length of time spent in framework processes and hooks. def overhead_time @runtime - example_runtime 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 + @fail = 0 + @error = 0 + @pending = 0 + + # Increments the number of passing examples. + def pass(_result) + @pass += 1 + end + + # Increments the number of failing (non-error) examples. + def fail(_result) + @fail += 1 + end + + # Increments the number of error (and failed) examples. + def error(result) + fail(result) + @error += 1 + end + + # Increments the number of pending (skipped) examples. + def pending(_result) + @pending += 1 + end + + # Produces the total counts. + # The *remaining* number of examples should be provided. + def counts(remaining) + Counts.new(@pass, @fail, @error, @pending, remaining) + end + end end end From 0ed684afbc9d9cddafa41776b481a56846849c4b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 May 2021 13:20:02 -0600 Subject: [PATCH 239/399] Integrate report and summary events --- src/spectator/formatting/formatter.cr | 3 +++ src/spectator/formatting/notifications.cr | 8 ++++++++ src/spectator/spec/events.cr | 16 ++++++++++++++++ src/spectator/spec/runner.cr | 21 ++++++++++----------- 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/spectator/formatting/formatter.cr b/src/spectator/formatting/formatter.cr index 0928de3..bcc7c9c 100644 --- a/src/spectator/formatting/formatter.cr +++ b/src/spectator/formatting/formatter.cr @@ -96,17 +96,20 @@ module Spectator::Formatting # Invoked after testing completes with a list of pending examples. # This method will be called with an empty list if there were no pending (skipped) examples. # Called after `#start_dump` and before `#dump_failures`. + # The *notification* will be an `ExampleSummaryNotification` type of object. def dump_pending(_notification) end # Invoked after testing completes with a list of failed examples. # This method will be called with an empty list if there were no failures. # Called after `#dump_pending` and before `#dump_summary`. + # The *notification* will be an `ExampleSummaryNotification` type of object. def dump_failures(_notification) end # Invoked after testing completes with summarized information from the test suite. # Called after `#dump_failures` and before `#dump_profile`. + # The *notification* will be an `SummaryNotification` type of object. def dump_summary(_notification) end diff --git a/src/spectator/formatting/notifications.cr b/src/spectator/formatting/notifications.cr index afe57e2..7fba582 100644 --- a/src/spectator/formatting/notifications.cr +++ b/src/spectator/formatting/notifications.cr @@ -1,7 +1,15 @@ require "../example" module Spectator::Formatting + # Structure indicating the test suite has started. record StartNotification, example_count : Int32 + # Structure indicating an event occurred with an example. record ExampleNotification, example : Example + + # Structure containing a subset of examples from the test suite. + record ExampleSummaryNotification, examples : Enumerable(Example) + + # Structure containing summarized information from the outcome of the test suite. + record SummaryNotification, report : Report end diff --git a/src/spectator/spec/events.cr b/src/spectator/spec/events.cr index e146e1c..7ef1fe3 100644 --- a/src/spectator/spec/events.cr +++ b/src/spectator/spec/events.cr @@ -35,6 +35,22 @@ module Spectator formatter.stop(nil) end + # Triggers the 'dump' events. + private def summarize(report) + formatter.start_dump + + notification = Formatting::ExampleSummaryNotification.new(report.pending) + formatter.dump_pending(notification) + + notification = Formatting::ExampleSummaryNotification.new(report.failures) + formatter.dump_failures(notification) + + notification = Formatting::SummaryNotification.new(report) + formatter.dump_summary(notification) + + formatter.dump_profile(nil) + end + # Triggers the 'close' event. # See `Formatting::Formatter#close` private def close diff --git a/src/spectator/spec/runner.cr b/src/spectator/spec/runner.cr index 3ffe030..adc8a82 100644 --- a/src/spectator/spec/runner.cr +++ b/src/spectator/spec/runner.cr @@ -1,4 +1,5 @@ require "../example" +require "../report" require "../run_flags" require "./events" @@ -26,11 +27,11 @@ module Spectator # or false if there was at least one failure. def run : Bool start - executed = [] of Example - elapsed = Time.measure { executed = run_examples } + elapsed = Time.measure { run_examples } stop - # TODO: Generate a report and pass it along to the formatter. + report = Report.generate(@examples, elapsed, nil) # TODO: Provide random seed. + summarize(report) false # TODO: Report real result @@ -41,15 +42,12 @@ module Spectator # Attempts to run all examples. # Returns a list of examples that ran. private def run_examples - Array(Example).new(example_count).tap do |executed| - @examples.each do |example| - result = run_example(example) - executed << example + @examples.each do |example| + result = run_example(example) - # Bail out if the example failed - # and configured to stop after the first failure. - break fail_fast if fail_fast? && result.fail? - end + # Bail out if the example failed + # and configured to stop after the first failure. + break fail_fast if fail_fast? && result.fail? end end @@ -58,6 +56,7 @@ module Spectator private def run_example(example) example_started(example) result = if dry_run? + # TODO: Pending examples return a pending result instead of pass in RSpec dry-run. dry_run_result else example.run From ee294a3ec21ced086915204258a09ad7e5e5c617 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 May 2021 13:22:25 -0600 Subject: [PATCH 240/399] Use Array instead of Enumerable --- src/spectator/spec/runner.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/spec/runner.cr b/src/spectator/spec/runner.cr index adc8a82..a77b78f 100644 --- a/src/spectator/spec/runner.cr +++ b/src/spectator/spec/runner.cr @@ -16,7 +16,7 @@ module Spectator # The collection of *examples* should be pre-filtered and shuffled. # This runner will run each example in the order provided. # The *formatter* will be called for various events. - def initialize(@examples : Enumerable(Example), + def initialize(@examples : Array(Example), @formatter : Formatting::Formatter, @run_flags = RunFlags::None) end From 3ecb04e293a8599407df2f7d0bdd7e944459223a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 May 2021 15:03:37 -0600 Subject: [PATCH 241/399] Initial work on summary output --- src/spectator/formatting/components.cr | 8 +++ .../formatting/components/comment.cr | 18 +++++ .../components/example_filter_command.cr | 13 ++++ .../formatting/components/failure_block.cr | 32 +++++++++ .../formatting/components/pending_block.cr | 28 ++++++++ .../formatting/components/runtime.cr | 69 +++++++++++++++++++ .../formatting/components/summary_block.cr | 38 ++++++++++ src/spectator/formatting/components/totals.cr | 35 ++++++++++ .../formatting/progress_formatter.cr | 11 +++ src/spectator/formatting/summary.cr | 55 +++++++++++++++ 10 files changed, 307 insertions(+) create mode 100644 src/spectator/formatting/components.cr create mode 100644 src/spectator/formatting/components/comment.cr create mode 100644 src/spectator/formatting/components/example_filter_command.cr create mode 100644 src/spectator/formatting/components/failure_block.cr create mode 100644 src/spectator/formatting/components/pending_block.cr create mode 100644 src/spectator/formatting/components/runtime.cr create mode 100644 src/spectator/formatting/components/summary_block.cr create mode 100644 src/spectator/formatting/components/totals.cr create mode 100644 src/spectator/formatting/summary.cr diff --git a/src/spectator/formatting/components.cr b/src/spectator/formatting/components.cr new file mode 100644 index 0000000..9c7c7b0 --- /dev/null +++ b/src/spectator/formatting/components.cr @@ -0,0 +1,8 @@ +require "./components/*" + +module Spectator::Formatting + # Namespace for snippets of text displayed in console output. + # These types are typically constructed and have `#to_s` called. + module Components + end +end diff --git a/src/spectator/formatting/components/comment.cr b/src/spectator/formatting/components/comment.cr new file mode 100644 index 0000000..75b67df --- /dev/null +++ b/src/spectator/formatting/components/comment.cr @@ -0,0 +1,18 @@ +module Spectator::Formatting::Components + struct Comment(T) + private COLOR = :cyan + + def initialize(@content : T) + end + + def self.colorize(content) + new(content).colorize(COLOR) + end + + def to_s(io) + io << '#' + io << ' ' + io << @content + end + end +end diff --git a/src/spectator/formatting/components/example_filter_command.cr b/src/spectator/formatting/components/example_filter_command.cr new file mode 100644 index 0000000..450fd63 --- /dev/null +++ b/src/spectator/formatting/components/example_filter_command.cr @@ -0,0 +1,13 @@ +module Spectator::Formatting::Components + struct ExampleFilterCommand + def initialize(@example : Example) + end + + def to_s(io) + io << "crystal spec " + io << @example.location + io << ' ' + io << Comment.colorize(@example.to_s) + end + end +end diff --git a/src/spectator/formatting/components/failure_block.cr b/src/spectator/formatting/components/failure_block.cr new file mode 100644 index 0000000..ca12d92 --- /dev/null +++ b/src/spectator/formatting/components/failure_block.cr @@ -0,0 +1,32 @@ +require "../../example" +require "./comment" + +module Spectator::Formatting::Components + struct FailureBlock + private INDENT = 2 + + def initialize(@example : Example, @index : Int32) + @result = @example.result.as(FailResult) + end + + def to_s(io) + 2.times { io << ' ' } + io << @index + io << ')' + io << ' ' + io.puts @example + indent = INDENT + index_digit_count + 2 + indent.times { io << ' ' } + io << "Failure: ".colorize(:red) + io.puts @result.error.message + io.puts + # TODO: Expectation values + indent.times { io << ' ' } + io.puts Comment.colorize(@example.location) # TODO: Use location of failed expectation. + end + + private def index_digit_count + (Math.log(@index.to_f + 1) / Math.log(10)).ceil.to_i + end + end +end diff --git a/src/spectator/formatting/components/pending_block.cr b/src/spectator/formatting/components/pending_block.cr new file mode 100644 index 0000000..dfc6abc --- /dev/null +++ b/src/spectator/formatting/components/pending_block.cr @@ -0,0 +1,28 @@ +require "../../example" +require "./comment" + +module Spectator::Formatting::Components + struct PendingBlock + private INDENT = 2 + + def initialize(@example : Example, @index : Int32) + end + + def to_s(io) + 2.times { io << ' ' } + io << @index + io << ')' + io << ' ' + io.puts @example + indent = INDENT + index_digit_count + 2 + indent.times { io << ' ' } + io.puts Comment.colorize("No reason given") # TODO: Get reason from result. + indent.times { io << ' ' } + io.puts Comment.colorize(@example.location) # TODO: Pending result could be triggered from another location. + end + + private def index_digit_count + (Math.log(@index.to_f + 1) / Math.log(10)).ceil.to_i + end + end +end diff --git a/src/spectator/formatting/components/runtime.cr b/src/spectator/formatting/components/runtime.cr new file mode 100644 index 0000000..90ef3bf --- /dev/null +++ b/src/spectator/formatting/components/runtime.cr @@ -0,0 +1,69 @@ +module Spectator::Formatting::Components + # Presents a human readable time span. + struct Runtime + # Creates the component. + def initialize(@span : Time::Span) + end + + # Appends the elapsed time to the output. + # The text will be formatted as follows, depending on the magnitude: + # ```text + # ## microseconds + # ## milliseconds + # ## seconds + # #:## + # #:##:## + # # days #:##:## + # ``` + def to_s(io) + millis = @span.total_milliseconds + return format_micro(io, millis * 1000) if millis < 1 + + seconds = @span.total_seconds + return format_millis(io, millis) if seconds < 1 + return format_seconds(io, seconds) if seconds < 60 + + minutes, seconds = seconds.divmod(60) + return format_minutes(io, minutes, seconds) if minutes < 60 + + hours, minutes = minutes.divmod(60) + return format_hours(io, hours, minutes, seconds) if hours < 24 + + days, hours = hours.divmod(24) + format_days(io, days, hours, minutes, seconds) + end + + # Formats for microseconds. + private def format_micro(io, micros) + io << micros.round.to_i + io << " microseconds" + end + + # Formats for milliseconds. + private def format_millis(io, millis) + io << millis.round(2) + io << " milliseconds" + end + + # Formats for seconds. + private def format_seconds(io, seconds) + io << seconds.round(2) + io << " seconds" + end + + # Formats for minutes. + private def format_minutes(io, minutes, seconds) + io.printf("%i:%02i", minutes, seconds) + end + + # Formats for hours. + private def format_hours(io, hours, minutes, seconds) + io.printf("%i:%02i:%02i", hours, minutes, seconds) + end + + # Formats for days. + private def format_days(io, days, hours, minutes, seconds) + io.printf("%i days %i:%02i:%02i", days, hours, minutes, seconds) + end + end +end diff --git a/src/spectator/formatting/components/summary_block.cr b/src/spectator/formatting/components/summary_block.cr new file mode 100644 index 0000000..8dc2c94 --- /dev/null +++ b/src/spectator/formatting/components/summary_block.cr @@ -0,0 +1,38 @@ +require "./example_filter_command" +require "./runtime" +require "./totals" + +module Spectator::Formatting::Components + # Summary information displayed at the end of a run. + struct SummaryBlock + def initialize(@report : Report) + end + + def to_s(io) + timing_line(io) + totals_line(io) + + unless (failures = @report.failures).empty? + io.puts + failures_block(io, failures) + end + end + + private def timing_line(io) + io << "Finished in " + io.puts Runtime.new(@report.runtime) + end + + private def totals_line(io) + io.puts Totals.colorize(@report.counts) + end + + private def failures_block(io, failures) + io.puts "Failed examples:" + io.puts + failures.each do |failure| + io.puts ExampleFilterCommand.new(failure).colorize(:red) + end + end + end +end diff --git a/src/spectator/formatting/components/totals.cr b/src/spectator/formatting/components/totals.cr new file mode 100644 index 0000000..ae18bcf --- /dev/null +++ b/src/spectator/formatting/components/totals.cr @@ -0,0 +1,35 @@ +module Spectator::Formatting::Components + struct Totals + def initialize(@examples : Int32, @failures : Int32, @errors : Int32, @pending : Int32) + end + + def initialize(counts) + @examples = counts.run + @failures = counts.fail + @errors = counts.error + @pending = counts.pending + end + + def self.colorize(counts) + totals = new(counts) + if counts.fail > 0 + totals.colorize(:red) + elsif counts.pending > 0 + totals.colorize(:yellow) + else + totals.colorize(:green) + end + end + + def to_s(io) + io << @examples + io << " examples, " + io << @failures + io << " failures, " + io << @errors + io << " errors, " + io << @pending + io << " pending" + end + end +end diff --git a/src/spectator/formatting/progress_formatter.cr b/src/spectator/formatting/progress_formatter.cr index 8f098fc..dff8956 100644 --- a/src/spectator/formatting/progress_formatter.cr +++ b/src/spectator/formatting/progress_formatter.cr @@ -1,15 +1,21 @@ require "colorize" require "./formatter" +require "./summary" module Spectator::Formatting # Output formatter that produces a single character for each test as it completes. # A '.' indicates a pass, 'F' a failure, 'E' an error, and '*' a skipped or pending test. class ProgressFormatter < Formatter + include Summary + @pass_char : Colorize::Object(Char) = '.'.colorize(:green) @fail_char : Colorize::Object(Char) = 'F'.colorize(:red) @error_char : Colorize::Object(Char) = 'E'.colorize(:red) @skip_char : Colorize::Object(Char) = '*'.colorize(:yellow) + # Output stream to write results to. + private getter io : IO + # Creates the formatter. def initialize(@io : IO = STDOUT) end @@ -33,5 +39,10 @@ module Spectator::Formatting def example_pending(_notification) @skip_char.to_s(@io) end + + # Produces a new line after the tests complete. + def stop(_notification) + @io.puts + end end end diff --git a/src/spectator/formatting/summary.cr b/src/spectator/formatting/summary.cr new file mode 100644 index 0000000..de82f01 --- /dev/null +++ b/src/spectator/formatting/summary.cr @@ -0,0 +1,55 @@ +require "./components" + +module Spectator::Formatting + # Mix-in providing common output for summarized results. + # Implements the following methods: + # `Formatter#start_dump`, `Formatter#dump_pending`, `Formatter#dump_failures`, + # `Formatter#dump_summary`, and `Formatter#dump_profile`. + # Classes including this module must implement `#io`. + module Summary + # Stream to write results to. + private abstract def io : IO + + def start_dump + io.puts + end + + # Invoked after testing completes with a list of pending examples. + # This method will be called with an empty list if there were no pending (skipped) examples. + # Called after `#start_dump` and before `#dump_failures`. + def dump_pending(notification) + return if (examples = notification.examples).empty? + + io.puts "Pending:" + io.puts + examples.each_with_index do |example, index| + io.puts Components::PendingBlock.new(example, index + 1) + end + end + + # Invoked after testing completes with a list of failed examples. + # This method will be called with an empty list if there were no failures. + # Called after `#dump_pending` and before `#dump_summary`. + def dump_failures(notification) + return if (examples = notification.examples).empty? + + io.puts "Failures:" + io.puts + examples.each_with_index do |example, index| + io.puts Components::FailureBlock.new(example, index + 1) + end + end + + # Invoked after testing completes with summarized information from the test suite. + # Called after `#dump_failures` and before `#dump_profile`. + def dump_summary(notification) + io.puts Components::SummaryBlock.new(notification.report) + end + + # Invoked after testing completes with profiling information. + # This method is only called if profiling is enabled. + # Called after `#dump_summary` and before `#close`. + def dump_profile(_notification) + end + end +end From 031e892dad46377e7d5ead6c90351b38ee484346 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 May 2021 16:56:19 -0600 Subject: [PATCH 242/399] Rename SummaryBlock to Stats --- .../formatting/components/{summary_block.cr => stats.cr} | 4 ++-- src/spectator/formatting/summary.cr | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/spectator/formatting/components/{summary_block.cr => stats.cr} (90%) diff --git a/src/spectator/formatting/components/summary_block.cr b/src/spectator/formatting/components/stats.cr similarity index 90% rename from src/spectator/formatting/components/summary_block.cr rename to src/spectator/formatting/components/stats.cr index 8dc2c94..29b1d5c 100644 --- a/src/spectator/formatting/components/summary_block.cr +++ b/src/spectator/formatting/components/stats.cr @@ -3,8 +3,8 @@ require "./runtime" require "./totals" module Spectator::Formatting::Components - # Summary information displayed at the end of a run. - struct SummaryBlock + # Statistics information displayed at the end of a run. + struct Stats def initialize(@report : Report) end diff --git a/src/spectator/formatting/summary.cr b/src/spectator/formatting/summary.cr index de82f01..0364755 100644 --- a/src/spectator/formatting/summary.cr +++ b/src/spectator/formatting/summary.cr @@ -43,7 +43,7 @@ module Spectator::Formatting # Invoked after testing completes with summarized information from the test suite. # Called after `#dump_failures` and before `#dump_profile`. def dump_summary(notification) - io.puts Components::SummaryBlock.new(notification.report) + io.puts Components::Stats.new(notification.report) end # Invoked after testing completes with profiling information. From 2316377c6e565063426eecc038431c48d2495a01 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 May 2021 17:08:15 -0600 Subject: [PATCH 243/399] Rename ExampleFilterCommand to ExampleCommand --- .../{example_filter_command.cr => example_command.cr} | 4 +++- src/spectator/formatting/components/stats.cr | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) rename src/spectator/formatting/components/{example_filter_command.cr => example_command.cr} (84%) diff --git a/src/spectator/formatting/components/example_filter_command.cr b/src/spectator/formatting/components/example_command.cr similarity index 84% rename from src/spectator/formatting/components/example_filter_command.cr rename to src/spectator/formatting/components/example_command.cr index 450fd63..648d8ea 100644 --- a/src/spectator/formatting/components/example_filter_command.cr +++ b/src/spectator/formatting/components/example_command.cr @@ -1,5 +1,7 @@ +require "./comment" + module Spectator::Formatting::Components - struct ExampleFilterCommand + struct ExampleCommand def initialize(@example : Example) end diff --git a/src/spectator/formatting/components/stats.cr b/src/spectator/formatting/components/stats.cr index 29b1d5c..4828c00 100644 --- a/src/spectator/formatting/components/stats.cr +++ b/src/spectator/formatting/components/stats.cr @@ -1,4 +1,4 @@ -require "./example_filter_command" +require "./example_command" require "./runtime" require "./totals" @@ -31,7 +31,7 @@ module Spectator::Formatting::Components io.puts "Failed examples:" io.puts failures.each do |failure| - io.puts ExampleFilterCommand.new(failure).colorize(:red) + io.puts ExampleCommand.new(failure).colorize(:red) end end end From ed3ad662d2da9e0b94dfc944eba9534a8ce7a03a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 May 2021 17:13:06 -0600 Subject: [PATCH 244/399] Move failure command list to its own component --- .../components/failure_command_list.cr | 16 ++++++++++++++ src/spectator/formatting/components/stats.cr | 22 ++++--------------- src/spectator/formatting/summary.cr | 7 +++++- 3 files changed, 26 insertions(+), 19 deletions(-) create mode 100644 src/spectator/formatting/components/failure_command_list.cr diff --git a/src/spectator/formatting/components/failure_command_list.cr b/src/spectator/formatting/components/failure_command_list.cr new file mode 100644 index 0000000..c6f247b --- /dev/null +++ b/src/spectator/formatting/components/failure_command_list.cr @@ -0,0 +1,16 @@ +require "./example_command" + +module Spectator::Formatting::Components + struct FailureCommandList + def initialize(@failures : Enumerable(Example)) + end + + def to_s(io) + io.puts "Failed examples:" + io.puts + @failures.each do |failure| + io.puts ExampleCommand.new(failure).colorize(:red) + end + end + end +end diff --git a/src/spectator/formatting/components/stats.cr b/src/spectator/formatting/components/stats.cr index 4828c00..3daa837 100644 --- a/src/spectator/formatting/components/stats.cr +++ b/src/spectator/formatting/components/stats.cr @@ -1,4 +1,3 @@ -require "./example_command" require "./runtime" require "./totals" @@ -9,30 +8,17 @@ module Spectator::Formatting::Components end def to_s(io) - timing_line(io) - totals_line(io) - - unless (failures = @report.failures).empty? - io.puts - failures_block(io, failures) - end + runtime(io) + totals(io) end - private def timing_line(io) + private def runtime(io) io << "Finished in " io.puts Runtime.new(@report.runtime) end - private def totals_line(io) + private def totals(io) io.puts Totals.colorize(@report.counts) end - - private def failures_block(io, failures) - io.puts "Failed examples:" - io.puts - failures.each do |failure| - io.puts ExampleCommand.new(failure).colorize(:red) - end - end end end diff --git a/src/spectator/formatting/summary.cr b/src/spectator/formatting/summary.cr index 0364755..85d0538 100644 --- a/src/spectator/formatting/summary.cr +++ b/src/spectator/formatting/summary.cr @@ -43,7 +43,12 @@ module Spectator::Formatting # Invoked after testing completes with summarized information from the test suite. # Called after `#dump_failures` and before `#dump_profile`. def dump_summary(notification) - io.puts Components::Stats.new(notification.report) + report = notification.report + io.puts Components::Stats.new(report) + + return if (failures = report.failures).empty? + + io.puts Components::FailureCommandList.new(failures) end # Invoked after testing completes with profiling information. From f81c498aefd4e248e33bd37eeb6ae48eb4f1a6b2 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 May 2021 17:14:09 -0600 Subject: [PATCH 245/399] Add error block component --- .../formatting/components/error_block.cr | 44 +++++++++++++++++++ .../formatting/components/failure_block.cr | 4 +- src/spectator/formatting/summary.cr | 7 ++- 3 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 src/spectator/formatting/components/error_block.cr diff --git a/src/spectator/formatting/components/error_block.cr b/src/spectator/formatting/components/error_block.cr new file mode 100644 index 0000000..64650dc --- /dev/null +++ b/src/spectator/formatting/components/error_block.cr @@ -0,0 +1,44 @@ +require "../../example" +require "../../error_result" +require "./comment" + +module Spectator::Formatting::Components + struct ErrorBlock + private INDENT = 2 + + def initialize(@example : Example, @result : ErrorResult, @index : Int32) + end + + def to_s(io) + 2.times { io << ' ' } + io << @index + io << ')' + io << ' ' + io.puts @example + indent = INDENT + index_digit_count + 2 + indent.times { io << ' ' } + error = @result.error + io << "Error: ".colorize(:red) + io.puts error.message + io.puts + indent.times { io << ' ' } + io << error.class + io.puts ':' + indent += INDENT + error.backtrace?.try do |trace| + trace.each do |entry| + indent.times { io << ' ' } + entry = entry.colorize.dim unless entry.starts_with?(/src\/|spec\//) + io.puts entry + end + end + indent -= INDENT + indent.times { io << ' ' } + io.puts Comment.colorize(@example.location) # TODO: Use location of failed expectation. + end + + private def index_digit_count + (Math.log(@index.to_f + 1) / Math.log(10)).ceil.to_i + end + end +end diff --git a/src/spectator/formatting/components/failure_block.cr b/src/spectator/formatting/components/failure_block.cr index ca12d92..1980ec9 100644 --- a/src/spectator/formatting/components/failure_block.cr +++ b/src/spectator/formatting/components/failure_block.cr @@ -1,12 +1,12 @@ require "../../example" +require "../../fail_result" require "./comment" module Spectator::Formatting::Components struct FailureBlock private INDENT = 2 - def initialize(@example : Example, @index : Int32) - @result = @example.result.as(FailResult) + def initialize(@example : Example, @result : FailResult, @index : Int32) end def to_s(io) diff --git a/src/spectator/formatting/summary.cr b/src/spectator/formatting/summary.cr index 85d0538..7d07353 100644 --- a/src/spectator/formatting/summary.cr +++ b/src/spectator/formatting/summary.cr @@ -1,3 +1,4 @@ +require "../fail_result" require "./components" module Spectator::Formatting @@ -36,7 +37,11 @@ module Spectator::Formatting io.puts "Failures:" io.puts examples.each_with_index do |example, index| - io.puts Components::FailureBlock.new(example, index + 1) + if result = example.result.as?(ErrorResult) + io.puts Components::ErrorBlock.new(example, result, index + 1) + elsif result = example.result.as?(FailResult) + io.puts Components::FailureBlock.new(example, result, index + 1) + end end end From cc09cb1b7767c740d4ac2b085884c547d0795d0c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 May 2021 19:28:59 -0600 Subject: [PATCH 246/399] Cleanup and consolidate common code for result blocks --- src/spectator/formatting/components/block.cr | 20 +++++++ .../formatting/components/comment.cr | 2 + .../formatting/components/error_block.cr | 44 -------------- .../components/error_result_block.cr | 40 +++++++++++++ .../components/fail_result_block.cr | 24 ++++++++ .../formatting/components/failure_block.cr | 32 ---------- .../formatting/components/result_block.cr | 58 +++++++++++++++++++ src/spectator/formatting/summary.cr | 4 +- 8 files changed, 146 insertions(+), 78 deletions(-) create mode 100644 src/spectator/formatting/components/block.cr delete mode 100644 src/spectator/formatting/components/error_block.cr create mode 100644 src/spectator/formatting/components/error_result_block.cr create mode 100644 src/spectator/formatting/components/fail_result_block.cr delete mode 100644 src/spectator/formatting/components/failure_block.cr create mode 100644 src/spectator/formatting/components/result_block.cr diff --git a/src/spectator/formatting/components/block.cr b/src/spectator/formatting/components/block.cr new file mode 100644 index 0000000..19286c8 --- /dev/null +++ b/src/spectator/formatting/components/block.cr @@ -0,0 +1,20 @@ +module Spectator::Formatting::Components + abstract struct Block + private INDENT = 2 + + def initialize(*, @indent : Int32 = INDENT) + end + + private def indent(amount = INDENT) + @indent += amount + yield + @indent -= amount + end + + private def line(io) + @indent.times { io << ' ' } + yield + io.puts + end + end +end diff --git a/src/spectator/formatting/components/comment.cr b/src/spectator/formatting/components/comment.cr index 75b67df..7405c6e 100644 --- a/src/spectator/formatting/components/comment.cr +++ b/src/spectator/formatting/components/comment.cr @@ -1,3 +1,5 @@ +require "colorize" + module Spectator::Formatting::Components struct Comment(T) private COLOR = :cyan diff --git a/src/spectator/formatting/components/error_block.cr b/src/spectator/formatting/components/error_block.cr deleted file mode 100644 index 64650dc..0000000 --- a/src/spectator/formatting/components/error_block.cr +++ /dev/null @@ -1,44 +0,0 @@ -require "../../example" -require "../../error_result" -require "./comment" - -module Spectator::Formatting::Components - struct ErrorBlock - private INDENT = 2 - - def initialize(@example : Example, @result : ErrorResult, @index : Int32) - end - - def to_s(io) - 2.times { io << ' ' } - io << @index - io << ')' - io << ' ' - io.puts @example - indent = INDENT + index_digit_count + 2 - indent.times { io << ' ' } - error = @result.error - io << "Error: ".colorize(:red) - io.puts error.message - io.puts - indent.times { io << ' ' } - io << error.class - io.puts ':' - indent += INDENT - error.backtrace?.try do |trace| - trace.each do |entry| - indent.times { io << ' ' } - entry = entry.colorize.dim unless entry.starts_with?(/src\/|spec\//) - io.puts entry - end - end - indent -= INDENT - indent.times { io << ' ' } - io.puts Comment.colorize(@example.location) # TODO: Use location of failed expectation. - end - - private def index_digit_count - (Math.log(@index.to_f + 1) / Math.log(10)).ceil.to_i - end - end -end diff --git a/src/spectator/formatting/components/error_result_block.cr b/src/spectator/formatting/components/error_result_block.cr new file mode 100644 index 0000000..10d9826 --- /dev/null +++ b/src/spectator/formatting/components/error_result_block.cr @@ -0,0 +1,40 @@ +require "colorize" +require "../../example" +require "../../error_result" +require "./result_block" + +module Spectator::Formatting::Components + struct ErrorResultBlock < ResultBlock + def initialize(index : Int32, example : Example, @result : ErrorResult) + super(index, example) + end + + private def subtitle + @result.error.message + end + + private def subtitle_label + "Error: ".colorize(:red) + end + + private def content(io) + error = @result.error + + line(io) do + io << "#{error.class}: ".colorize(:red) + io << error.message + end + + error.backtrace?.try do |backtrace| + indent { write_backtrace(io, backtrace) } + end + end + + private def write_backtrace(io, backtrace) + backtrace.each do |entry| + entry = entry.colorize.dim unless entry.starts_with?(/(src|spec)\//) + line(io) { io << entry } + end + end + end +end diff --git a/src/spectator/formatting/components/fail_result_block.cr b/src/spectator/formatting/components/fail_result_block.cr new file mode 100644 index 0000000..9b20a1b --- /dev/null +++ b/src/spectator/formatting/components/fail_result_block.cr @@ -0,0 +1,24 @@ +require "colorize" +require "../../example" +require "../../fail_result" +require "./result_block" + +module Spectator::Formatting::Components + struct FailResultBlock < ResultBlock + def initialize(index : Int32, example : Example, @result : FailResult) + super(index, example) + end + + private def subtitle + @result.error.message + end + + private def subtitle_label + "Failure: ".colorize(:red) + end + + private def content(io) + # TODO: Display match data. + end + end +end diff --git a/src/spectator/formatting/components/failure_block.cr b/src/spectator/formatting/components/failure_block.cr deleted file mode 100644 index 1980ec9..0000000 --- a/src/spectator/formatting/components/failure_block.cr +++ /dev/null @@ -1,32 +0,0 @@ -require "../../example" -require "../../fail_result" -require "./comment" - -module Spectator::Formatting::Components - struct FailureBlock - private INDENT = 2 - - def initialize(@example : Example, @result : FailResult, @index : Int32) - end - - def to_s(io) - 2.times { io << ' ' } - io << @index - io << ')' - io << ' ' - io.puts @example - indent = INDENT + index_digit_count + 2 - indent.times { io << ' ' } - io << "Failure: ".colorize(:red) - io.puts @result.error.message - io.puts - # TODO: Expectation values - indent.times { io << ' ' } - io.puts Comment.colorize(@example.location) # TODO: Use location of failed expectation. - end - - private def index_digit_count - (Math.log(@index.to_f + 1) / Math.log(10)).ceil.to_i - end - end -end diff --git a/src/spectator/formatting/components/result_block.cr b/src/spectator/formatting/components/result_block.cr new file mode 100644 index 0000000..1321b13 --- /dev/null +++ b/src/spectator/formatting/components/result_block.cr @@ -0,0 +1,58 @@ +require "../../example" +require "./block" +require "./comment" + +module Spectator::Formatting::Components + abstract struct ResultBlock < Block + def initialize(@index : Int32, @example : Example) + super() + end + + private def title + @example + end + + private abstract def subtitle + + private abstract def subtitle_label + + def to_s(io) + title_line(io) + indent(index_digit_count + 2) do + subtitle_line(io) + io.puts + content(io) + source_line(io) + end + end + + private def title_line(io) + line(io) do + io << @index + io << ')' + io << ' ' + io << title + end + end + + private def subtitle_line(io) + line(io) do + io << subtitle_label + io << subtitle + end + end + + private def source_line(io) + source = if (result = @example.result).responds_to?(:source) + result.source + else + @example.location + end + line(io) { io << Comment.colorize(source) } + end + + private def index_digit_count + (Math.log(@index.to_f + 1) / Math::LOG10).ceil.to_i + end + end +end diff --git a/src/spectator/formatting/summary.cr b/src/spectator/formatting/summary.cr index 7d07353..7dba7f2 100644 --- a/src/spectator/formatting/summary.cr +++ b/src/spectator/formatting/summary.cr @@ -38,9 +38,9 @@ module Spectator::Formatting io.puts examples.each_with_index do |example, index| if result = example.result.as?(ErrorResult) - io.puts Components::ErrorBlock.new(example, result, index + 1) + io.puts Components::ErrorResultBlock.new(index + 1, example, result) elsif result = example.result.as?(FailResult) - io.puts Components::FailureBlock.new(example, result, index + 1) + io.puts Components::FailResultBlock.new(index + 1, example, result) end end end From 84ac41967b4089340af346e915305c4a670b9847 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 May 2021 19:29:20 -0600 Subject: [PATCH 247/399] Formatting --- src/spectator/formatting/components/result_block.cr | 8 ++++---- src/spectator/spec/runner.cr | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/spectator/formatting/components/result_block.cr b/src/spectator/formatting/components/result_block.cr index 1321b13..2025441 100644 --- a/src/spectator/formatting/components/result_block.cr +++ b/src/spectator/formatting/components/result_block.cr @@ -44,10 +44,10 @@ module Spectator::Formatting::Components private def source_line(io) source = if (result = @example.result).responds_to?(:source) - result.source - else - @example.location - end + result.source + else + @example.location + end line(io) { io << Comment.colorize(source) } end diff --git a/src/spectator/spec/runner.cr b/src/spectator/spec/runner.cr index a77b78f..78dcfb0 100644 --- a/src/spectator/spec/runner.cr +++ b/src/spectator/spec/runner.cr @@ -35,6 +35,7 @@ module Spectator false # TODO: Report real result + ensure close end From aee171656ad0e4401930e92d94d193051599a40c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 May 2021 19:37:45 -0600 Subject: [PATCH 248/399] Fix '' showing up for root group --- src/spectator/example_group.cr | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index 8031e20..4c9ae9e 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -121,19 +121,18 @@ module Spectator # Constructs the full name or description of the example group. # This prepends names of groups this group is part of. def to_s(io) + # Prefix with group's full name if the node belongs to a group. + return unless parent = @group + + parent.to_s(io) name = @name - # Prefix with group's full name if the node belongs to a group. - if (parent = @group) - parent.to_s(io) - - # Add padding between the node names - # only if the names don't appear to be symbolic. - # Skip blank group names (like the root group). - io << ' ' unless !parent.name? || # ameba:disable Style/NegatedConditionsInUnless - (parent.name?.is_a?(Symbol) && name.is_a?(String) && - (name.starts_with?('#') || name.starts_with?('.'))) - end + # Add padding between the node names + # only if the names don't appear to be symbolic. + # Skip blank group names (like the root group). + io << ' ' unless !parent.name? || # ameba:disable Style/NegatedConditionsInUnless + (parent.name?.is_a?(Symbol) && name.is_a?(String) && + (name.starts_with?('#') || name.starts_with?('.'))) super end From 1e3e0daa044550ca02405948d135fed8b2ce6c06 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 May 2021 19:42:15 -0600 Subject: [PATCH 249/399] Place error count in parens next to failures --- src/spectator/formatting/components/totals.cr | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/spectator/formatting/components/totals.cr b/src/spectator/formatting/components/totals.cr index ae18bcf..06483e5 100644 --- a/src/spectator/formatting/components/totals.cr +++ b/src/spectator/formatting/components/totals.cr @@ -1,3 +1,5 @@ +require "colorize" + module Spectator::Formatting::Components struct Totals def initialize(@examples : Int32, @failures : Int32, @errors : Int32, @pending : Int32) @@ -25,9 +27,14 @@ module Spectator::Formatting::Components io << @examples io << " examples, " io << @failures - io << " failures, " - io << @errors - io << " errors, " + io << " failures " + + if @errors > 1 + io << '(' + io << @errors + io << " errors), " + end + io << @pending io << " pending" end From aa13b077f3916cf80e447ff633f330b83c23292c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 May 2021 19:47:07 -0600 Subject: [PATCH 250/399] No need to type restrict io method --- src/spectator/formatting/progress_formatter.cr | 2 +- src/spectator/formatting/summary.cr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/formatting/progress_formatter.cr b/src/spectator/formatting/progress_formatter.cr index dff8956..8f3a366 100644 --- a/src/spectator/formatting/progress_formatter.cr +++ b/src/spectator/formatting/progress_formatter.cr @@ -14,7 +14,7 @@ module Spectator::Formatting @skip_char : Colorize::Object(Char) = '*'.colorize(:yellow) # Output stream to write results to. - private getter io : IO + private getter io # Creates the formatter. def initialize(@io : IO = STDOUT) diff --git a/src/spectator/formatting/summary.cr b/src/spectator/formatting/summary.cr index 7dba7f2..1695f34 100644 --- a/src/spectator/formatting/summary.cr +++ b/src/spectator/formatting/summary.cr @@ -9,7 +9,7 @@ module Spectator::Formatting # Classes including this module must implement `#io`. module Summary # Stream to write results to. - private abstract def io : IO + private abstract def io def start_dump io.puts From 4bb4c2f16e69ea7f9e439e891161f03bd0e106e2 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 May 2021 19:52:53 -0600 Subject: [PATCH 251/399] Dump profile before summary --- src/spectator/formatting/formatter.cr | 18 +++++++++--------- src/spectator/formatting/summary.cr | 12 ++++++------ src/spectator/spec/events.cr | 4 ++-- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/spectator/formatting/formatter.cr b/src/spectator/formatting/formatter.cr index bcc7c9c..059c663 100644 --- a/src/spectator/formatting/formatter.cr +++ b/src/spectator/formatting/formatter.cr @@ -18,8 +18,8 @@ module Spectator::Formatting # 8. `#start_dump` # 9. `#dump_pending` # 10. `#dump_failures` - # 11. `#dump_summary` - # 12. `#dump_profile` + # 11. `#dump_profile` + # 12. `#dump_summary` # 13. `#close` # # Only one of the `#example_passed`, `#example_pending`, or `#example_failed` methods @@ -107,18 +107,18 @@ module Spectator::Formatting def dump_failures(_notification) end - # Invoked after testing completes with summarized information from the test suite. - # Called after `#dump_failures` and before `#dump_profile`. - # The *notification* will be an `SummaryNotification` type of object. - def dump_summary(_notification) - end - # Invoked after testing completes with profiling information. # This method is only called if profiling is enabled. - # Called after `#dump_summary` and before `#close`. + # Called after `#dump_failures` and before `#dump_summary`. def dump_profile(_notification) end + # Invoked after testing completes with summarized information from the test suite. + # Called after `#dump_profile` and before `#close`. + # The *notification* will be an `SummaryNotification` type of object. + def dump_summary(_notification) + end + # Invoked at the end of the program. # Allows the formatter to perform any cleanup and teardown. def close diff --git a/src/spectator/formatting/summary.cr b/src/spectator/formatting/summary.cr index 1695f34..9215327 100644 --- a/src/spectator/formatting/summary.cr +++ b/src/spectator/formatting/summary.cr @@ -45,6 +45,12 @@ module Spectator::Formatting end end + # Invoked after testing completes with profiling information. + # This method is only called if profiling is enabled. + # Called after `#dump_failures` and before `#dump_summary`. + def dump_profile(_notification) + end + # Invoked after testing completes with summarized information from the test suite. # Called after `#dump_failures` and before `#dump_profile`. def dump_summary(notification) @@ -55,11 +61,5 @@ module Spectator::Formatting io.puts Components::FailureCommandList.new(failures) end - - # Invoked after testing completes with profiling information. - # This method is only called if profiling is enabled. - # Called after `#dump_summary` and before `#close`. - def dump_profile(_notification) - end end end diff --git a/src/spectator/spec/events.cr b/src/spectator/spec/events.cr index 7ef1fe3..4d3541e 100644 --- a/src/spectator/spec/events.cr +++ b/src/spectator/spec/events.cr @@ -45,10 +45,10 @@ module Spectator notification = Formatting::ExampleSummaryNotification.new(report.failures) formatter.dump_failures(notification) + formatter.dump_profile(nil) + notification = Formatting::SummaryNotification.new(report) formatter.dump_summary(notification) - - formatter.dump_profile(nil) end # Triggers the 'close' event. From ebf04b360e9010fbdbeabf195c6dd2bac93af299 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 May 2021 20:21:04 -0600 Subject: [PATCH 252/399] Add profiling info --- .../formatting/components/profile.cr | 32 +++++++++++++++++++ src/spectator/formatting/notifications.cr | 5 +++ src/spectator/formatting/summary.cr | 3 +- src/spectator/profile.cr | 23 ++++++------- src/spectator/spec/events.cr | 7 ++-- src/spectator/spec/runner.cr | 3 +- 6 files changed, 58 insertions(+), 15 deletions(-) create mode 100644 src/spectator/formatting/components/profile.cr diff --git a/src/spectator/formatting/components/profile.cr b/src/spectator/formatting/components/profile.cr new file mode 100644 index 0000000..d38210c --- /dev/null +++ b/src/spectator/formatting/components/profile.cr @@ -0,0 +1,32 @@ +require "../../profile" +require "./runtime" + +module Spectator::Formatting::Components + struct Profile + def initialize(@profile : Spectator::Profile) + end + + def to_s(io) + io << "Top " + io << @profile.size + io << " slowest examples (" + io << Runtime.new(@profile.time) + io << ", " + io << @profile.percentage.round(2) + io.puts "% of total time):" + + @profile.each do |example| + example_profile(io, example) + end + end + + private def example_profile(io, example) + io << " " + io.puts example + io << " " + io << Runtime.new(example.result.elapsed).colorize.bold + io << ' ' + io.puts example.location + end + end +end diff --git a/src/spectator/formatting/notifications.cr b/src/spectator/formatting/notifications.cr index 7fba582..362f710 100644 --- a/src/spectator/formatting/notifications.cr +++ b/src/spectator/formatting/notifications.cr @@ -1,4 +1,6 @@ require "../example" +require "../profile" +require "../report" module Spectator::Formatting # Structure indicating the test suite has started. @@ -10,6 +12,9 @@ module Spectator::Formatting # Structure containing a subset of examples from the test suite. record ExampleSummaryNotification, examples : Enumerable(Example) + # Structure containing profiling information. + record ProfileNotification, profile : Profile + # Structure containing summarized information from the outcome of the test suite. record SummaryNotification, report : Report end diff --git a/src/spectator/formatting/summary.cr b/src/spectator/formatting/summary.cr index 9215327..eca030c 100644 --- a/src/spectator/formatting/summary.cr +++ b/src/spectator/formatting/summary.cr @@ -48,7 +48,8 @@ module Spectator::Formatting # Invoked after testing completes with profiling information. # This method is only called if profiling is enabled. # Called after `#dump_failures` and before `#dump_summary`. - def dump_profile(_notification) + def dump_profile(notification) + io.puts Components::Profile.new(notification.profile) end # Invoked after testing completes with summarized information from the test suite. diff --git a/src/spectator/profile.cr b/src/spectator/profile.cr index c4021e2..e6bc612 100644 --- a/src/spectator/profile.cr +++ b/src/spectator/profile.cr @@ -8,7 +8,16 @@ module Spectator # Creates the profiling information. # The *slowest* results must already be sorted, longest time first. - private def initialize(@slowest : Array(Example), @total_time) + def initialize(@slowest : Array(Example), @total_time) + end + + # Produces the profile from a report. + def self.generate(examples, size = 10) + finished = examples.select(&.finished?).to_a + total_time = finished.sum(&.result.elapsed) + sorted_examples = finished.sort_by(&.result.elapsed) + slowest = sorted_examples.last(size).reverse + new(slowest, total_time) end # Number of results in the profile. @@ -26,17 +35,9 @@ module Spectator @slowest.sum(&.result.elapsed) end - # Percentage (from 0 to 1) of time the results in this profile took compared to all examples. + # Percentage (from 0 to 100) of time the results in this profile took compared to all examples. def percentage - time / @total_time - end - - # Produces the profile from a report. - def self.generate(report, size = 10) - examples = report.to_a - sorted_examples = examples.sort_by(&.result.elapsed) - slowest = sorted_examples.last(size).reverse - self.new(slowest, report.example_runtime) + time / @total_time * 100 end end end diff --git a/src/spectator/spec/events.cr b/src/spectator/spec/events.cr index 4d3541e..fc09d21 100644 --- a/src/spectator/spec/events.cr +++ b/src/spectator/spec/events.cr @@ -36,7 +36,7 @@ module Spectator end # Triggers the 'dump' events. - private def summarize(report) + private def summarize(report, profile) formatter.start_dump notification = Formatting::ExampleSummaryNotification.new(report.pending) @@ -45,7 +45,10 @@ module Spectator notification = Formatting::ExampleSummaryNotification.new(report.failures) formatter.dump_failures(notification) - formatter.dump_profile(nil) + if profile + notification = Formatting::ProfileNotification.new(profile) + formatter.dump_profile(notification) + end notification = Formatting::SummaryNotification.new(report) formatter.dump_summary(notification) diff --git a/src/spectator/spec/runner.cr b/src/spectator/spec/runner.cr index 78dcfb0..181361d 100644 --- a/src/spectator/spec/runner.cr +++ b/src/spectator/spec/runner.cr @@ -31,7 +31,8 @@ module Spectator stop report = Report.generate(@examples, elapsed, nil) # TODO: Provide random seed. - summarize(report) + profile = Profile.generate(@examples) if @run_flags.profile? && report.counts.run > 0 + summarize(report, profile) false # TODO: Report real result From 36f9f2b4343c9fdfa98bd4ffe9a05d1dda5c7c61 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 May 2021 20:23:16 -0600 Subject: [PATCH 253/399] Return true from run method if successful --- src/spectator/spec.cr | 4 +++- src/spectator/spec/runner.cr | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/spectator/spec.cr b/src/spectator/spec.cr index a1e80ab..e85d5dc 100644 --- a/src/spectator/spec.cr +++ b/src/spectator/spec.cr @@ -13,7 +13,9 @@ module Spectator end # Runs all selected examples and returns the results. - def run + # True will be returned if the spec ran successfully, + # or false if there was at least one failure. + def run : Bool runner = Runner.new(examples, @config.formatter, @config.run_flags) runner.run end diff --git a/src/spectator/spec/runner.cr b/src/spectator/spec/runner.cr index 181361d..d5696ea 100644 --- a/src/spectator/spec/runner.cr +++ b/src/spectator/spec/runner.cr @@ -34,9 +34,7 @@ module Spectator profile = Profile.generate(@examples) if @run_flags.profile? && report.counts.run > 0 summarize(report, profile) - false # TODO: Report real result - - + report.counts.fail.zero? ensure close end From 1525317e2ced4a4e99e07098c560d8c71c25867e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 May 2021 20:38:02 -0600 Subject: [PATCH 254/399] Pass along and output random seed --- src/spectator/formatting/components/stats.cr | 8 ++++++++ src/spectator/spec.cr | 2 +- src/spectator/spec/runner.cr | 6 +++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/spectator/formatting/components/stats.cr b/src/spectator/formatting/components/stats.cr index 3daa837..cd5baea 100644 --- a/src/spectator/formatting/components/stats.cr +++ b/src/spectator/formatting/components/stats.cr @@ -1,3 +1,4 @@ +require "colorize" require "./runtime" require "./totals" @@ -10,6 +11,9 @@ module Spectator::Formatting::Components def to_s(io) runtime(io) totals(io) + if seed = @report.random_seed? + random(io, seed) + end end private def runtime(io) @@ -20,5 +24,9 @@ module Spectator::Formatting::Components private def totals(io) io.puts Totals.colorize(@report.counts) end + + private def random(io, seed) + io.puts "Randomized with seed: #{seed}".colorize(:cyan) + end end end diff --git a/src/spectator/spec.cr b/src/spectator/spec.cr index e85d5dc..1e275d3 100644 --- a/src/spectator/spec.cr +++ b/src/spectator/spec.cr @@ -16,7 +16,7 @@ module Spectator # True will be returned if the spec ran successfully, # or false if there was at least one failure. def run : Bool - runner = Runner.new(examples, @config.formatter, @config.run_flags) + runner = Runner.new(examples, @config.formatter, @config.run_flags, @config.random_seed) runner.run end diff --git a/src/spectator/spec/runner.cr b/src/spectator/spec/runner.cr index d5696ea..81e86c2 100644 --- a/src/spectator/spec/runner.cr +++ b/src/spectator/spec/runner.cr @@ -16,8 +16,8 @@ module Spectator # The collection of *examples* should be pre-filtered and shuffled. # This runner will run each example in the order provided. # The *formatter* will be called for various events. - def initialize(@examples : Array(Example), - @formatter : Formatting::Formatter, @run_flags = RunFlags::None) + def initialize(@examples : Array(Example), @formatter : Formatting::Formatter, + @run_flags = RunFlags::None, @random_seed : UInt64? = nil) end # Runs the spec. @@ -30,7 +30,7 @@ module Spectator elapsed = Time.measure { run_examples } stop - report = Report.generate(@examples, elapsed, nil) # TODO: Provide random seed. + report = Report.generate(@examples, elapsed, @random_seed) profile = Profile.generate(@examples) if @run_flags.profile? && report.counts.run > 0 summarize(report, profile) From b970abd334ccd01e383822865dbcc5bfaf559b30 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 May 2021 20:39:35 -0600 Subject: [PATCH 255/399] Limit generated seed to < 100,000 --- src/spectator/config/builder.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/config/builder.cr b/src/spectator/config/builder.cr index ca086be..4ecb4d9 100644 --- a/src/spectator/config/builder.cr +++ b/src/spectator/config/builder.cr @@ -11,7 +11,7 @@ module Spectator # Then call `#build` to create the final configuration. class Builder # Seed used for random number generation. - property random_seed : UInt64 = Random.rand(UInt64) + property random_seed : UInt64 = Random.rand(100000_u64) # Toggles indicating how the test spec should execute. property run_flags = RunFlags::None From c0befe63e953d763e488471c69c6c3a172b4278e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 18 May 2021 18:50:43 -0600 Subject: [PATCH 256/399] Don't use random seed unless randomized --- src/spectator/spec.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/spectator/spec.cr b/src/spectator/spec.cr index 1e275d3..a3894f4 100644 --- a/src/spectator/spec.cr +++ b/src/spectator/spec.cr @@ -16,7 +16,8 @@ module Spectator # True will be returned if the spec ran successfully, # or false if there was at least one failure. def run : Bool - runner = Runner.new(examples, @config.formatter, @config.run_flags, @config.random_seed) + random_seed = (@config.random_seed if @config.run_flags.randomize?) + runner = Runner.new(examples, @config.formatter, @config.run_flags, random_seed) runner.run end From 1addc46f7eb2578d6d9905181aa30127fc379653 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 18 May 2021 19:03:40 -0600 Subject: [PATCH 257/399] Remove parameter from stop method --- src/spectator/formatting/broadcast_formatter.cr | 4 ++-- src/spectator/formatting/formatter.cr | 2 +- src/spectator/formatting/progress_formatter.cr | 2 +- src/spectator/spec/events.cr | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/spectator/formatting/broadcast_formatter.cr b/src/spectator/formatting/broadcast_formatter.cr index 0a9db22..76ac814 100644 --- a/src/spectator/formatting/broadcast_formatter.cr +++ b/src/spectator/formatting/broadcast_formatter.cr @@ -50,8 +50,8 @@ module Spectator::Formatting end # :ditto: - def stop(notification) - @formatters.each(&.stop(notification)) + def stop + @formatters.each(&.stop) end # :ditto: diff --git a/src/spectator/formatting/formatter.cr b/src/spectator/formatting/formatter.cr index 059c663..a80ea66 100644 --- a/src/spectator/formatting/formatter.cr +++ b/src/spectator/formatting/formatter.cr @@ -84,7 +84,7 @@ module Spectator::Formatting # Invoked after all tests that will run have completed. # When this method is called, it should be considered that the testing is done. # Summary (dump) methods will be called after this. - def stop(_notification) + def stop end # Invoked after all examples finished. diff --git a/src/spectator/formatting/progress_formatter.cr b/src/spectator/formatting/progress_formatter.cr index 8f3a366..20975f4 100644 --- a/src/spectator/formatting/progress_formatter.cr +++ b/src/spectator/formatting/progress_formatter.cr @@ -41,7 +41,7 @@ module Spectator::Formatting end # Produces a new line after the tests complete. - def stop(_notification) + def stop @io.puts end end diff --git a/src/spectator/spec/events.cr b/src/spectator/spec/events.cr index fc09d21..5adb606 100644 --- a/src/spectator/spec/events.cr +++ b/src/spectator/spec/events.cr @@ -32,7 +32,7 @@ module Spectator # Triggers the 'stop' event. # See `Formatting::Formatter#stop` private def stop - formatter.stop(nil) + formatter.stop end # Triggers the 'dump' events. From 1e2f3f7c6683007d863211f7410cfe01dd591bf6 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 18 May 2021 19:38:04 -0600 Subject: [PATCH 258/399] Use index offset argument --- src/spectator/formatting/summary.cr | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/spectator/formatting/summary.cr b/src/spectator/formatting/summary.cr index eca030c..f0b9279 100644 --- a/src/spectator/formatting/summary.cr +++ b/src/spectator/formatting/summary.cr @@ -23,8 +23,8 @@ module Spectator::Formatting io.puts "Pending:" io.puts - examples.each_with_index do |example, index| - io.puts Components::PendingBlock.new(example, index + 1) + examples.each_with_index(1) do |example, index| + io.puts Components::PendingBlock.new(example, index) end end @@ -36,11 +36,11 @@ module Spectator::Formatting io.puts "Failures:" io.puts - examples.each_with_index do |example, index| + examples.each_with_index(1) do |example, index| if result = example.result.as?(ErrorResult) - io.puts Components::ErrorResultBlock.new(index + 1, example, result) + io.puts Components::ErrorResultBlock.new(index, example, result) elsif result = example.result.as?(FailResult) - io.puts Components::FailResultBlock.new(index + 1, example, result) + io.puts Components::FailResultBlock.new(index, example, result) end end end From 867c06bd670c69743075a9fde78b622a290c7b31 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 18 May 2021 20:01:58 -0600 Subject: [PATCH 259/399] Initial working document formatter --- .../formatting/document_formatter.cr | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/src/spectator/formatting/document_formatter.cr b/src/spectator/formatting/document_formatter.cr index 61c298e..89099cd 100644 --- a/src/spectator/formatting/document_formatter.cr +++ b/src/spectator/formatting/document_formatter.cr @@ -1,6 +1,93 @@ +require "../label" require "./formatter" +require "./summary" module Spectator::Formatting + # Produces an indented document-style output. + # Each nested group of examples increases the indent. + # Example names are output in a color based on their result. class DocumentFormatter < Formatter + include Summary + + # Whitespace count per indent. + private INDENT_AMOUNT = 2 + + # Output stream to write results to. + private getter io + + @previous_hierarchy = [] of Label + + # Creates the formatter. + # By default, output is sent to STDOUT. + def initialize(@io : IO = STDOUT) + end + + # Invoked just before an example runs. + # Prints the example group hierarchy if it changed. + def example_started(notification) + hierarchy = group_hierarchy(notification.example) + tuple = hierarchy_diff(@previous_hierarchy, hierarchy) + print_sub_hierarchy(*tuple) + @previous_hierarchy = hierarchy + end + + # Invoked after an example completes successfully. + # Produces a successful example line. + def example_passed(notification) + indent = @previous_hierarchy.size * INDENT_AMOUNT + indent.times { @io << ' ' } + @io.puts notification.example.name.colorize(:green) + end + + # Invoked after an example is skipped or marked as pending. + # Produces a pending example line. + def example_pending(notification) + indent = @previous_hierarchy.size * INDENT_AMOUNT + indent.times { @io << ' ' } + @io.puts notification.example.name.colorize(:yellow) + end + + # Invoked after an example fails. + # Produces a failure example line. + def example_failed(notification) + indent = @previous_hierarchy.size * INDENT_AMOUNT + indent.times { @io << ' ' } + @io.puts notification.example.name.colorize(:red) + end + + # Invoked after an example fails from an unexpected error. + # Produces a failure example line. + def example_error(notification) + example_failed(notification) + end + + # Produces a list of groups making up the hierarchy for an example. + private def group_hierarchy(example) + hierarchy = [] of Label + group = example.group? + while group && (parent = group.group?) + hierarchy << group.name + group = parent + end + hierarchy.reverse + end + + # Generates a difference between two hierarchies. + private def hierarchy_diff(first, second) + index = -1 + diff = second.skip_while do |group| + index += 1 + first.size > index && first[index] == group + end + {index, diff} + end + + # Displays an indented hierarchy starting partially into the whole hierarchy. + private def print_sub_hierarchy(start_index, sub_hierarchy) + sub_hierarchy.each_with_index(start_index) do |name, index| + (index * INDENT_AMOUNT).times { @io << ' ' } + @io.puts name + end + end end end From 453f6a2fcec3b230d7198c5de92bc0ea157f443e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 18 May 2021 20:10:02 -0600 Subject: [PATCH 260/399] Cleanup --- .../formatting/document_formatter.cr | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/spectator/formatting/document_formatter.cr b/src/spectator/formatting/document_formatter.cr index 89099cd..99fb689 100644 --- a/src/spectator/formatting/document_formatter.cr +++ b/src/spectator/formatting/document_formatter.cr @@ -9,8 +9,8 @@ module Spectator::Formatting class DocumentFormatter < Formatter include Summary - # Whitespace count per indent. - private INDENT_AMOUNT = 2 + # Identation string. + private INDENT = " " # Output stream to write results to. private getter io @@ -34,25 +34,19 @@ module Spectator::Formatting # Invoked after an example completes successfully. # Produces a successful example line. def example_passed(notification) - indent = @previous_hierarchy.size * INDENT_AMOUNT - indent.times { @io << ' ' } - @io.puts notification.example.name.colorize(:green) + line(notification.example.name.colorize(:green)) end # Invoked after an example is skipped or marked as pending. # Produces a pending example line. def example_pending(notification) - indent = @previous_hierarchy.size * INDENT_AMOUNT - indent.times { @io << ' ' } - @io.puts notification.example.name.colorize(:yellow) + line(notification.example.name.colorize(:yellow)) end # Invoked after an example fails. # Produces a failure example line. def example_failed(notification) - indent = @previous_hierarchy.size * INDENT_AMOUNT - indent.times { @io << ' ' } - @io.puts notification.example.name.colorize(:red) + line(notification.example.name.colorize(:red)) end # Invoked after an example fails from an unexpected error. @@ -63,13 +57,14 @@ module Spectator::Formatting # Produces a list of groups making up the hierarchy for an example. private def group_hierarchy(example) - hierarchy = [] of Label - group = example.group? - while group && (parent = group.group?) - hierarchy << group.name - group = parent + Array(Label).new.tap do |hierarchy| + group = example.group? + while group && (parent = group.group?) + hierarchy << group.name + group = parent + end + hierarchy.reverse! end - hierarchy.reverse end # Generates a difference between two hierarchies. @@ -83,11 +78,16 @@ module Spectator::Formatting end # Displays an indented hierarchy starting partially into the whole hierarchy. - private def print_sub_hierarchy(start_index, sub_hierarchy) - sub_hierarchy.each_with_index(start_index) do |name, index| - (index * INDENT_AMOUNT).times { @io << ' ' } - @io.puts name + private def print_sub_hierarchy(start_index, hierarchy) + hierarchy.each_with_index(start_index) do |name, index| + line(name, index) end end + + # Displays an indented line of text. + private def line(text, level = @previous_hierarchy.size) + level.times { @io << INDENT } + @io.puts text + end end end From 76dd5603de644b7c14d43f382d6a668f55c4fdf8 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 29 May 2021 17:13:14 -0600 Subject: [PATCH 261/399] Fix harness not updating current --- src/spectator/harness.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 27b2679..f865f4d 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -55,7 +55,7 @@ module Spectator private def self.with_harness previous = @@current begin - harness = new + @@current = harness = new yield harness ensure @@current = previous From fccd55ed0a22e635366ea230b4786e05f68d18df Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 29 May 2021 17:33:45 -0600 Subject: [PATCH 262/399] Improve appearance of multi-line error messages --- .../formatting/components/error_result_block.cr | 11 +++++++++-- .../formatting/components/fail_result_block.cr | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/spectator/formatting/components/error_result_block.cr b/src/spectator/formatting/components/error_result_block.cr index 10d9826..6ebbab5 100644 --- a/src/spectator/formatting/components/error_result_block.cr +++ b/src/spectator/formatting/components/error_result_block.cr @@ -10,7 +10,7 @@ module Spectator::Formatting::Components end private def subtitle - @result.error.message + @result.error.message.try(&.each_line.first) end private def subtitle_label @@ -19,10 +19,17 @@ module Spectator::Formatting::Components private def content(io) error = @result.error + lines = error.message.try(&.lines) || {"".colorize(:purple)} line(io) do io << "#{error.class}: ".colorize(:red) - io << error.message + io << lines.first + end + + lines.skip(1).each do |entry| + line(io) do + io << entry + end end error.backtrace?.try do |backtrace| diff --git a/src/spectator/formatting/components/fail_result_block.cr b/src/spectator/formatting/components/fail_result_block.cr index 9b20a1b..1b22c6a 100644 --- a/src/spectator/formatting/components/fail_result_block.cr +++ b/src/spectator/formatting/components/fail_result_block.cr @@ -10,7 +10,7 @@ module Spectator::Formatting::Components end private def subtitle - @result.error.message + @result.error.message.try(&.each_line.first) end private def subtitle_label From 4a44d038fbf614c2980e19097236b3acb77f6a9a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 29 May 2021 17:45:49 -0600 Subject: [PATCH 263/399] Re-enable desired logging on framework error --- src/spectator.cr | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/spectator.cr b/src/spectator.cr index 4ebfee5..f565f21 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -71,6 +71,9 @@ module Spectator spec = DSL::Builder.build spec.run rescue ex + # Re-enable logger for fatal error. + ::Log.setup_from_env + # Catch all unhandled exceptions here. # Examples are already wrapped, so any exceptions they throw are caught. # But if an exception occurs outside an example, From 6d8d117ec245ae57311da314dc75fd788d84c102 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 29 May 2021 17:50:30 -0600 Subject: [PATCH 264/399] Handle nodes with no name --- src/spectator/formatting/document_formatter.cr | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/spectator/formatting/document_formatter.cr b/src/spectator/formatting/document_formatter.cr index 99fb689..fe6c1ce 100644 --- a/src/spectator/formatting/document_formatter.cr +++ b/src/spectator/formatting/document_formatter.cr @@ -12,6 +12,9 @@ module Spectator::Formatting # Identation string. private INDENT = " " + # String used for groups and examples that don't have a name. + private NO_NAME = "" + # Output stream to write results to. private getter io @@ -34,19 +37,22 @@ module Spectator::Formatting # Invoked after an example completes successfully. # Produces a successful example line. def example_passed(notification) - line(notification.example.name.colorize(:green)) + name = (notification.example.name? || NO_NAME) + line(name.colorize(:green)) end # Invoked after an example is skipped or marked as pending. # Produces a pending example line. def example_pending(notification) - line(notification.example.name.colorize(:yellow)) + name = (notification.example.name? || NO_NAME) + line(name.colorize(:yellow)) end # Invoked after an example fails. # Produces a failure example line. def example_failed(notification) - line(notification.example.name.colorize(:red)) + name = (notification.example.name? || NO_NAME) + line(name.colorize(:red)) end # Invoked after an example fails from an unexpected error. @@ -60,7 +66,7 @@ module Spectator::Formatting Array(Label).new.tap do |hierarchy| group = example.group? while group && (parent = group.group?) - hierarchy << group.name + hierarchy << (group.name? || NO_NAME) group = parent end hierarchy.reverse! From 5e1ca341465ab137213c4ac5dace9db73b9a9a66 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 29 May 2021 17:59:16 -0600 Subject: [PATCH 265/399] Fix pending results not being counted --- src/spectator/example.cr | 1 + 1 file changed, 1 insertion(+) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 08e7858..34b6061 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -75,6 +75,7 @@ module Spectator if pending? Log.debug { "Skipping example #{self} - marked pending" } + @finished = true return @result = PendingResult.new end From f02e1acb3b37422fc0a9d41f1457291a8de32f86 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 29 May 2021 18:35:01 -0600 Subject: [PATCH 266/399] Remove lazy iteration Fixes issue with empty? method eating the first example for failure and pending lists. --- src/spectator/report.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/report.cr b/src/spectator/report.cr index 9168848..fb69dde 100644 --- a/src/spectator/report.cr +++ b/src/spectator/report.cr @@ -69,12 +69,12 @@ module Spectator # Returns a collection of all failed examples. def failures - @examples.each.select(&.result.fail?) + @examples.select(&.result.fail?) end # Returns a collection of all pending (skipped) examples. def pending - @examples.each.select(&.result.pending?) + @examples.select(&.result.pending?) end # Length of time it took to run just example code. From 76c525de528fa77e25852de097490a0b3d804975 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 29 May 2021 22:47:53 -0600 Subject: [PATCH 267/399] Fix call to example_finished --- src/spectator/spec/events.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/spec/events.cr b/src/spectator/spec/events.cr index 5adb606..d6ddb8f 100644 --- a/src/spectator/spec/events.cr +++ b/src/spectator/spec/events.cr @@ -25,7 +25,7 @@ module Spectator private def example_finished(example) notification = Formatting::ExampleNotification.new(example) visitor = ResultVisitor.new(formatter, notification) - formatter.example_started(notification) + formatter.example_finished(notification) example.result.accept(visitor) end From 48fb293ba08683374ea60966833cf65211b28e4c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 29 May 2021 22:59:56 -0600 Subject: [PATCH 268/399] Define notification type for messages --- src/spectator/formatting/formatter.cr | 1 + src/spectator/formatting/notifications.cr | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/spectator/formatting/formatter.cr b/src/spectator/formatting/formatter.cr index a80ea66..7c46e54 100644 --- a/src/spectator/formatting/formatter.cr +++ b/src/spectator/formatting/formatter.cr @@ -78,6 +78,7 @@ module Spectator::Formatting # Called whenever the example or framework produces a message. # This is typically used for logging. + # The *notification* will be a `MessageNotification` type of object. def message(_notification) end diff --git a/src/spectator/formatting/notifications.cr b/src/spectator/formatting/notifications.cr index 362f710..e789514 100644 --- a/src/spectator/formatting/notifications.cr +++ b/src/spectator/formatting/notifications.cr @@ -17,4 +17,7 @@ module Spectator::Formatting # Structure containing summarized information from the outcome of the test suite. record SummaryNotification, report : Report + + # Structure containing a debug or log message from the test suite. + record MessageNotification, message : String end From 40e189a1d191eb5e1ea581508055b35017060cdc Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 29 May 2021 23:09:18 -0600 Subject: [PATCH 269/399] Fix method name --- src/spectator/report.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/report.cr b/src/spectator/report.cr index fb69dde..d565531 100644 --- a/src/spectator/report.cr +++ b/src/spectator/report.cr @@ -18,7 +18,7 @@ module Spectator # Indicates whether there were skipped tests # because of a failure causing the test suite to abort. def remaining? - remaining_count > 0 + remaining > 0 end end From 7215e28d75cf42905413477278fe61aebfbeab5e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 29 May 2021 23:09:28 -0600 Subject: [PATCH 270/399] Implement TAP formatter --- .../formatting/components/tap_profile.cr | 32 +++++++++ src/spectator/formatting/tap_formatter.cr | 67 +++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 src/spectator/formatting/components/tap_profile.cr diff --git a/src/spectator/formatting/components/tap_profile.cr b/src/spectator/formatting/components/tap_profile.cr new file mode 100644 index 0000000..6e3a558 --- /dev/null +++ b/src/spectator/formatting/components/tap_profile.cr @@ -0,0 +1,32 @@ +require "../../profile" +require "./runtime" + +module Spectator::Formatting::Components + struct TAPProfile + def initialize(@profile : Spectator::Profile) + end + + def to_s(io) + io << "# Top " + io << @profile.size + io << " slowest examples (" + io << Runtime.new(@profile.time) + io << ", " + io << @profile.percentage.round(2) + io.puts "% of total time):" + + @profile.each do |example| + example_profile(io, example) + end + end + + private def example_profile(io, example) + io << "# " + io.puts example + io << "# " + io << Runtime.new(example.result.elapsed) + io << ' ' + io.puts example.location + end + end +end diff --git a/src/spectator/formatting/tap_formatter.cr b/src/spectator/formatting/tap_formatter.cr index 3da03db..d060041 100644 --- a/src/spectator/formatting/tap_formatter.cr +++ b/src/spectator/formatting/tap_formatter.cr @@ -1,6 +1,73 @@ require "./formatter" module Spectator::Formatting + # Produces TAP output from test results. + # See: https://testanything.org/ + # Version 12 of the specification is used. class TAPFormatter < Formatter + @counter = 0 + + # Creates the formatter. + def initialize(@io : IO = STDOUT) + end + + # Invoked when the test suite begins. + def start(notification) + @io << "1.." + @io.puts notification.example_count + end + + # Invoked just after an example completes. + def example_finished(_notification) + @counter += 1 + end + + # Invoked after an example completes successfully. + def example_passed(notification) + @io << "ok " + @io << @counter + @io << " - " + @io.puts notification.example + end + + # Invoked after an example is skipped or marked as pending. + def example_pending(notification) + @io << "not ok " # TODO: Skipped tests should report ok. + @io << @counter + @io << " - " + @io << notification.example + @io << " # TODO " + @io.puts "No reason given" # TODO: Get reason from result. + end + + # Invoked after an example fails. + def example_failed(notification) + @io << "not ok " + @io << @counter + @io << " - " + @io.puts notification.example + end + + # Invoked after an example fails from an unexpected error. + def example_error(notification) + example_failed(notification) + end + + # Called whenever the example or framework produces a message. + # This is typically used for logging. + def message(notification) + @io << "# " + @io.puts notification.message + end + + # Invoked after testing completes with profiling information. + def dump_profile(notification) + @io << Components::TAPProfile.new(notification.profile) + end + + # Invoked after testing completes with summarized information from the test suite. + def dump_summary(notification) + @io.puts "Bail out!" if notification.report.counts.remaining? + end end end From a4042a968404b63de746974956b119fb98893289 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 30 May 2021 10:02:25 -0600 Subject: [PATCH 271/399] Extend PendingBlock from ResultBlock and rename to PendingResultBlock --- .../formatting/components/pending_block.cr | 28 ------------------- .../components/pending_result_block.cr | 23 +++++++++++++++ src/spectator/formatting/summary.cr | 3 +- 3 files changed, 25 insertions(+), 29 deletions(-) delete mode 100644 src/spectator/formatting/components/pending_block.cr create mode 100644 src/spectator/formatting/components/pending_result_block.cr diff --git a/src/spectator/formatting/components/pending_block.cr b/src/spectator/formatting/components/pending_block.cr deleted file mode 100644 index dfc6abc..0000000 --- a/src/spectator/formatting/components/pending_block.cr +++ /dev/null @@ -1,28 +0,0 @@ -require "../../example" -require "./comment" - -module Spectator::Formatting::Components - struct PendingBlock - private INDENT = 2 - - def initialize(@example : Example, @index : Int32) - end - - def to_s(io) - 2.times { io << ' ' } - io << @index - io << ')' - io << ' ' - io.puts @example - indent = INDENT + index_digit_count + 2 - indent.times { io << ' ' } - io.puts Comment.colorize("No reason given") # TODO: Get reason from result. - indent.times { io << ' ' } - io.puts Comment.colorize(@example.location) # TODO: Pending result could be triggered from another location. - end - - private def index_digit_count - (Math.log(@index.to_f + 1) / Math.log(10)).ceil.to_i - end - end -end diff --git a/src/spectator/formatting/components/pending_result_block.cr b/src/spectator/formatting/components/pending_result_block.cr new file mode 100644 index 0000000..389eda3 --- /dev/null +++ b/src/spectator/formatting/components/pending_result_block.cr @@ -0,0 +1,23 @@ +require "../../example" +require "../../pending_result" +require "./result_block" + +module Spectator::Formatting::Components + struct PendingResultBlock < ResultBlock + def initialize(index : Int32, example : Example, @result : PendingResult) + super(index, example) + end + + private def subtitle + "No reason given" # TODO: Get reason from result. + end + + private def subtitle_label + # TODO: Could be pending or skipped. + "Pending: ".colorize(:yellow) + end + + private def content(io) + end + end +end diff --git a/src/spectator/formatting/summary.cr b/src/spectator/formatting/summary.cr index f0b9279..e66ced8 100644 --- a/src/spectator/formatting/summary.cr +++ b/src/spectator/formatting/summary.cr @@ -24,7 +24,8 @@ module Spectator::Formatting io.puts "Pending:" io.puts examples.each_with_index(1) do |example, index| - io.puts Components::PendingBlock.new(example, index) + result = example.result.as(PendingResult) + io.puts Components::PendingResultBlock.new(index, example, result) end end From 877831a98b2bbd555adb39a1ad310bd816a508cf Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 30 May 2021 10:17:49 -0600 Subject: [PATCH 272/399] Add docs --- src/spectator/formatting/components/block.cr | 12 ++++++++ .../formatting/components/comment.cr | 5 ++++ .../components/error_result_block.cr | 18 ++++++++--- .../formatting/components/example_command.cr | 4 +++ .../components/fail_result_block.cr | 5 ++++ .../components/failure_command_list.cr | 5 ++++ .../components/pending_result_block.cr | 6 ++++ .../formatting/components/profile.cr | 4 +++ .../formatting/components/result_block.cr | 30 +++++++++++++++++++ src/spectator/formatting/components/stats.cr | 6 ++++ .../formatting/components/tap_profile.cr | 5 ++++ src/spectator/formatting/components/totals.cr | 8 +++++ 12 files changed, 104 insertions(+), 4 deletions(-) diff --git a/src/spectator/formatting/components/block.cr b/src/spectator/formatting/components/block.cr index 19286c8..40cd5a8 100644 --- a/src/spectator/formatting/components/block.cr +++ b/src/spectator/formatting/components/block.cr @@ -1,16 +1,28 @@ module Spectator::Formatting::Components + # Base type for handling indented output. + # Indents are tracked and automatically printed. + # Use `#indent` to increase the indent for the duration of a block. + # Use `#line` to produce a line with an indentation prefixing it. abstract struct Block + # Default indent amount. private INDENT = 2 + # Creates the block. + # A default *indent* size can be specified. def initialize(*, @indent : Int32 = INDENT) end + # Increases the indent by the a specific *amount* for the duration of the block. private def indent(amount = INDENT) @indent += amount yield @indent -= amount end + # Produces a line of output with an indent before it. + # The contents of the line should be generated by a block provided to this method. + # Ensure that _only_ one line is produced by the block, + # otherwise the indent will be lost. private def line(io) @indent.times { io << ' ' } yield diff --git a/src/spectator/formatting/components/comment.cr b/src/spectator/formatting/components/comment.cr index 7405c6e..3ae5341 100644 --- a/src/spectator/formatting/components/comment.cr +++ b/src/spectator/formatting/components/comment.cr @@ -1,16 +1,21 @@ require "colorize" module Spectator::Formatting::Components + # Object that can be stringified pre-pended with a comment mark (#). struct Comment(T) + # Default color for a comment. private COLOR = :cyan + # Creates a comment with the specified content. def initialize(@content : T) end + # Creates a colored comment. def self.colorize(content) new(content).colorize(COLOR) end + # Writes the comment to the output. def to_s(io) io << '#' io << ' ' diff --git a/src/spectator/formatting/components/error_result_block.cr b/src/spectator/formatting/components/error_result_block.cr index 6ebbab5..bbd348c 100644 --- a/src/spectator/formatting/components/error_result_block.cr +++ b/src/spectator/formatting/components/error_result_block.cr @@ -4,41 +4,51 @@ require "../../error_result" require "./result_block" module Spectator::Formatting::Components + # Displays information about an error result. struct ErrorResultBlock < ResultBlock + # Creates the component. def initialize(index : Int32, example : Example, @result : ErrorResult) super(index, example) end + # Content displayed on the second line of the block after the label. private def subtitle @result.error.message.try(&.each_line.first) end + # Prefix for the second line of the block. private def subtitle_label "Error: ".colorize(:red) end + # Display error information. private def content(io) + # Fetch the error and message. + # If there's no message error = @result.error lines = error.message.try(&.lines) || {"".colorize(:purple)} + # Display the error type and first line of the message. line(io) do io << "#{error.class}: ".colorize(:red) io << lines.first end + # Display additional lines after the first if there's any. lines.skip(1).each do |entry| - line(io) do - io << entry - end + line(io) { io << entry } end - error.backtrace?.try do |backtrace| + # Display the backtrace if it's available. + if backtrace = error.backtrace? indent { write_backtrace(io, backtrace) } end end + # Writes the backtrace entries to the output. private def write_backtrace(io, backtrace) backtrace.each do |entry| + # Dim entries that are outside the shard. entry = entry.colorize.dim unless entry.starts_with?(/(src|spec)\//) line(io) { io << entry } end diff --git a/src/spectator/formatting/components/example_command.cr b/src/spectator/formatting/components/example_command.cr index 648d8ea..5dd4c05 100644 --- a/src/spectator/formatting/components/example_command.cr +++ b/src/spectator/formatting/components/example_command.cr @@ -1,10 +1,14 @@ +require "../../example" require "./comment" module Spectator::Formatting::Components + # Provides syntax for running a specific example from the command-line. struct ExampleCommand + # Creates the component with the specified example. def initialize(@example : Example) end + # Produces output for running the previously specified example. def to_s(io) io << "crystal spec " io << @example.location diff --git a/src/spectator/formatting/components/fail_result_block.cr b/src/spectator/formatting/components/fail_result_block.cr index 1b22c6a..dc5780d 100644 --- a/src/spectator/formatting/components/fail_result_block.cr +++ b/src/spectator/formatting/components/fail_result_block.cr @@ -4,19 +4,24 @@ require "../../fail_result" require "./result_block" module Spectator::Formatting::Components + # Displays information about a fail result. struct FailResultBlock < ResultBlock + # Creates the component. def initialize(index : Int32, example : Example, @result : FailResult) super(index, example) end + # Content displayed on the second line of the block after the label. private def subtitle @result.error.message.try(&.each_line.first) end + # Prefix for the second line of the block. private def subtitle_label "Failure: ".colorize(:red) end + # Display expectation match data. private def content(io) # TODO: Display match data. end diff --git a/src/spectator/formatting/components/failure_command_list.cr b/src/spectator/formatting/components/failure_command_list.cr index c6f247b..7da5ed2 100644 --- a/src/spectator/formatting/components/failure_command_list.cr +++ b/src/spectator/formatting/components/failure_command_list.cr @@ -1,10 +1,15 @@ +require "../../example" require "./example_command" module Spectator::Formatting::Components + # Produces a list of commands to run failed examples. struct FailureCommandList + # Creates the component. + # Requires a set of *failures* to display commands for. def initialize(@failures : Enumerable(Example)) end + # Produces the list of commands to run failed examples. def to_s(io) io.puts "Failed examples:" io.puts diff --git a/src/spectator/formatting/components/pending_result_block.cr b/src/spectator/formatting/components/pending_result_block.cr index 389eda3..12ee2a1 100644 --- a/src/spectator/formatting/components/pending_result_block.cr +++ b/src/spectator/formatting/components/pending_result_block.cr @@ -1,22 +1,28 @@ +require "colorize" require "../../example" require "../../pending_result" require "./result_block" module Spectator::Formatting::Components + # Displays information about a pending result. struct PendingResultBlock < ResultBlock + # Creates the component. def initialize(index : Int32, example : Example, @result : PendingResult) super(index, example) end + # Content displayed on the second line of the block after the label. private def subtitle "No reason given" # TODO: Get reason from result. end + # Prefix for the second line of the block. private def subtitle_label # TODO: Could be pending or skipped. "Pending: ".colorize(:yellow) end + # No content for this type of block. private def content(io) end end diff --git a/src/spectator/formatting/components/profile.cr b/src/spectator/formatting/components/profile.cr index d38210c..7e35b30 100644 --- a/src/spectator/formatting/components/profile.cr +++ b/src/spectator/formatting/components/profile.cr @@ -2,10 +2,13 @@ require "../../profile" require "./runtime" module Spectator::Formatting::Components + # Displays profiling information for slow examples. struct Profile + # Creates the component with the specified *profile*. def initialize(@profile : Spectator::Profile) end + # Produces the output containing the profiling information. def to_s(io) io << "Top " io << @profile.size @@ -20,6 +23,7 @@ module Spectator::Formatting::Components end end + # Writes a single example's timing to the output. private def example_profile(io, example) io << " " io.puts example diff --git a/src/spectator/formatting/components/result_block.cr b/src/spectator/formatting/components/result_block.cr index 2025441..fd5eb5f 100644 --- a/src/spectator/formatting/components/result_block.cr +++ b/src/spectator/formatting/components/result_block.cr @@ -3,21 +3,47 @@ require "./block" require "./comment" module Spectator::Formatting::Components + # Base class that displayed indexed results in block form. + # These typically take the form: + # ```text + # 1) Title + # Label: Subtitle + # + # Content + # # Source + # ``` abstract struct ResultBlock < Block + # Creates the block with the specified *index* and for the given *example*. def initialize(@index : Int32, @example : Example) super() end + # Content displayed on the first line of the block. + # Will be stringified. + # By default, uses the example name. + # Can be overridden to use a different value. private def title @example end + # Content displayed on the second line of the block after the label. + # Will be stringified. private abstract def subtitle + # Prefix for the second line of the block. + # Will be stringified. + # This is typically something like "Error:" or "Failure:" private abstract def subtitle_label + # Produces the main content of the block. + # *io* is the stream to write to. + # `#line` and `#indent` (from `Block`) should be used to maintain spacing. + private abstract def content(io) + + # Writes the component's output to the specified stream. def to_s(io) title_line(io) + # Ident over to align with the spacing used by the index. indent(index_digit_count + 2) do subtitle_line(io) io.puts @@ -26,6 +52,7 @@ module Spectator::Formatting::Components end end + # Produces the title line. private def title_line(io) line(io) do io << @index @@ -35,6 +62,7 @@ module Spectator::Formatting::Components end end + # Produces the subtitle line. private def subtitle_line(io) line(io) do io << subtitle_label @@ -42,6 +70,7 @@ module Spectator::Formatting::Components end end + # Produces the (example) source line. private def source_line(io) source = if (result = @example.result).responds_to?(:source) result.source @@ -51,6 +80,7 @@ module Spectator::Formatting::Components line(io) { io << Comment.colorize(source) } end + # Computes the number of spaces the index takes private def index_digit_count (Math.log(@index.to_f + 1) / Math::LOG10).ceil.to_i end diff --git a/src/spectator/formatting/components/stats.cr b/src/spectator/formatting/components/stats.cr index cd5baea..ab76c7c 100644 --- a/src/spectator/formatting/components/stats.cr +++ b/src/spectator/formatting/components/stats.cr @@ -1,13 +1,16 @@ require "colorize" +require "../../report" require "./runtime" require "./totals" module Spectator::Formatting::Components # Statistics information displayed at the end of a run. struct Stats + # Creates the component with stats from *report*. def initialize(@report : Report) end + # Displays the stats. def to_s(io) runtime(io) totals(io) @@ -16,15 +19,18 @@ module Spectator::Formatting::Components end end + # Displays the time it took to run the suite. private def runtime(io) io << "Finished in " io.puts Runtime.new(@report.runtime) end + # Displays the counts for each type of result. private def totals(io) io.puts Totals.colorize(@report.counts) end + # Displays the random seed. private def random(io, seed) io.puts "Randomized with seed: #{seed}".colorize(:cyan) end diff --git a/src/spectator/formatting/components/tap_profile.cr b/src/spectator/formatting/components/tap_profile.cr index 6e3a558..8ae022f 100644 --- a/src/spectator/formatting/components/tap_profile.cr +++ b/src/spectator/formatting/components/tap_profile.cr @@ -2,10 +2,14 @@ require "../../profile" require "./runtime" module Spectator::Formatting::Components + # Displays profiling information for slow examples in a TAP format. + # Produces output similar to `Profile`, but formatted for TAP. struct TAPProfile + # Creates the component with the specified *profile*. def initialize(@profile : Spectator::Profile) end + # Produces the output containing the profiling information. def to_s(io) io << "# Top " io << @profile.size @@ -20,6 +24,7 @@ module Spectator::Formatting::Components end end + # Writes a single example's timing to the output. private def example_profile(io, example) io << "# " io.puts example diff --git a/src/spectator/formatting/components/totals.cr b/src/spectator/formatting/components/totals.cr index 06483e5..db4edab 100644 --- a/src/spectator/formatting/components/totals.cr +++ b/src/spectator/formatting/components/totals.cr @@ -1,10 +1,13 @@ require "colorize" module Spectator::Formatting::Components + # Displays counts for each type of example result (pass, fail, error, pending). struct Totals + # Creates the component with the specified counts. def initialize(@examples : Int32, @failures : Int32, @errors : Int32, @pending : Int32) end + # Creates the component by pulling numbers from *counts*. def initialize(counts) @examples = counts.run @failures = counts.fail @@ -12,6 +15,10 @@ module Spectator::Formatting::Components @pending = counts.pending end + # Creates the component, but colors it whether there were pending or failed results. + # The component will be red if there were failures (or errors), + # yellow if there were pending/skipped tests, + # and green if everything passed. def self.colorize(counts) totals = new(counts) if counts.fail > 0 @@ -23,6 +30,7 @@ module Spectator::Formatting::Components end end + # Writes the counts to the output. def to_s(io) io << @examples io << " examples, " From 0a7909fb7ab81dd384d256ecf4c76694af637b94 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 30 May 2021 10:44:09 -0600 Subject: [PATCH 273/399] Cleanup --- .../components/error_result_block.cr | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/src/spectator/formatting/components/error_result_block.cr b/src/spectator/formatting/components/error_result_block.cr index bbd348c..d27f294 100644 --- a/src/spectator/formatting/components/error_result_block.cr +++ b/src/spectator/formatting/components/error_result_block.cr @@ -24,19 +24,15 @@ module Spectator::Formatting::Components # Display error information. private def content(io) # Fetch the error and message. - # If there's no message error = @result.error - lines = error.message.try(&.lines) || {"".colorize(:purple)} + lines = error.message.try(&.lines) - # Display the error type and first line of the message. - line(io) do - io << "#{error.class}: ".colorize(:red) - io << lines.first - end - - # Display additional lines after the first if there's any. - lines.skip(1).each do |entry| - line(io) { io << entry } + # Write the error and message if available. + case + when lines.nil? then write_error_class(io, error) + when lines.size == 1 then write_error_message(io, error, lines.first) + when lines.size > 1 then write_multiline_error_message(io, error, lines) + else write_error_class(io, error) end # Display the backtrace if it's available. @@ -45,6 +41,32 @@ module Spectator::Formatting::Components end end + # Display just the error type. + private def write_error_class(io, error) + line(io) do + io << error.class.colorize(:red) + end + end + + # Display the error type and first line of the message. + private def write_error_message(io, error, message) + line(io) do + io << "#{error.class}: ".colorize(:red) + io << message + end + end + + # Display the error type and its multi-line message. + private def write_multiline_error_message(io, error, lines) + # Use the normal formatting for the first line. + write_error_message(io, error, lines.first) + + # Display additional lines after the first. + lines.skip(1).each do |entry| + line(io) { io << entry } + end + end + # Writes the backtrace entries to the output. private def write_backtrace(io, backtrace) backtrace.each do |entry| From e30d5c1981f03c5584e45e3033bf7f516dc1cbc9 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 30 May 2021 14:21:42 -0600 Subject: [PATCH 274/399] Use multiple << on a single line --- src/spectator/abstract_expression.cr | 8 ++------ src/spectator/context.cr | 4 +--- src/spectator/example.cr | 3 +-- src/spectator/example_group_hook.cr | 6 ++---- src/spectator/example_hook.cr | 6 ++---- src/spectator/example_procsy_hook.cr | 6 ++---- src/spectator/formatting/components/comment.cr | 4 +--- .../formatting/components/result_block.cr | 8 ++------ src/spectator/formatting/components/runtime.cr | 9 +++------ src/spectator/formatting/components/totals.cr | 12 +++--------- src/spectator/formatting/tap_formatter.cr | 16 +++++----------- src/spectator/location.cr | 4 +--- src/spectator/mocks/double.cr | 4 +--- src/spectator/mocks/generic_arguments.cr | 3 +-- src/spectator/mocks/generic_method_stub.cr | 9 ++------- src/spectator/mocks/method_call.cr | 3 +-- src/spectator/mocks/method_stub.cr | 3 +-- src/spectator/mocks/verifying_double.cr | 4 +--- src/spectator/node.cr | 7 ++----- 19 files changed, 34 insertions(+), 85 deletions(-) diff --git a/src/spectator/abstract_expression.cr b/src/spectator/abstract_expression.cr index dc4ba13..7985c16 100644 --- a/src/spectator/abstract_expression.cr +++ b/src/spectator/abstract_expression.cr @@ -36,9 +36,7 @@ module Spectator # This consists of the label (if one is available) and the value. def to_s(io) if (label = @label) - io << label - io << ':' - io << ' ' + io << label << ": " end raw_value.to_s(io) end @@ -47,9 +45,7 @@ module Spectator # This consists of the label (if one is available) and the value. def inspect(io) if (label = @label) - io << label - io << ':' - io << ' ' + io << label << ": " end raw_value.inspect(io) end diff --git a/src/spectator/context.cr b/src/spectator/context.cr index 475f49c..c3213e3 100644 --- a/src/spectator/context.cr +++ b/src/spectator/context.cr @@ -16,9 +16,7 @@ abstract class SpectatorContext # :ditto: def inspect(io) - io << "Context<" - io << self.class - io << '>' + io << "Context<" << self.class << '>' end end diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 34b6061..3cca701 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -160,8 +160,7 @@ module Spectator # Exposes information about the example useful for debugging. def inspect(io) super - io << ' ' - io << result + io << ' ' << result end # Creates the JSON representation of the example, diff --git a/src/spectator/example_group_hook.cr b/src/spectator/example_group_hook.cr index 85f6898..ef44e8b 100644 --- a/src/spectator/example_group_hook.cr +++ b/src/spectator/example_group_hook.cr @@ -36,13 +36,11 @@ module Spectator io << "example group hook" if (label = @label) - io << ' ' - io << label + io << ' ' << label end if (location = @location) - io << " @ " - io << location + io << " @ " << location end end end diff --git a/src/spectator/example_hook.cr b/src/spectator/example_hook.cr index 1f02445..f57c908 100644 --- a/src/spectator/example_hook.cr +++ b/src/spectator/example_hook.cr @@ -38,13 +38,11 @@ module Spectator io << "example hook" if (label = @label) - io << ' ' - io << label + io << ' ' << label end if (location = @location) - io << " @ " - io << location + io << " @ " << location end end end diff --git a/src/spectator/example_procsy_hook.cr b/src/spectator/example_procsy_hook.cr index 27ccae6..8a64f17 100644 --- a/src/spectator/example_procsy_hook.cr +++ b/src/spectator/example_procsy_hook.cr @@ -43,13 +43,11 @@ module Spectator io << "example hook" if (label = @label) - io << ' ' - io << label + io << ' ' << label end if (location = @location) - io << " @ " - io << location + io << " @ " << location end end end diff --git a/src/spectator/formatting/components/comment.cr b/src/spectator/formatting/components/comment.cr index 3ae5341..b398840 100644 --- a/src/spectator/formatting/components/comment.cr +++ b/src/spectator/formatting/components/comment.cr @@ -17,9 +17,7 @@ module Spectator::Formatting::Components # Writes the comment to the output. def to_s(io) - io << '#' - io << ' ' - io << @content + io << "# " << @content end end end diff --git a/src/spectator/formatting/components/result_block.cr b/src/spectator/formatting/components/result_block.cr index fd5eb5f..8ea7459 100644 --- a/src/spectator/formatting/components/result_block.cr +++ b/src/spectator/formatting/components/result_block.cr @@ -55,18 +55,14 @@ module Spectator::Formatting::Components # Produces the title line. private def title_line(io) line(io) do - io << @index - io << ')' - io << ' ' - io << title + io << @index << ") " << title end end # Produces the subtitle line. private def subtitle_line(io) line(io) do - io << subtitle_label - io << subtitle + io << subtitle_label << subtitle end end diff --git a/src/spectator/formatting/components/runtime.cr b/src/spectator/formatting/components/runtime.cr index 90ef3bf..9f28813 100644 --- a/src/spectator/formatting/components/runtime.cr +++ b/src/spectator/formatting/components/runtime.cr @@ -35,20 +35,17 @@ module Spectator::Formatting::Components # Formats for microseconds. private def format_micro(io, micros) - io << micros.round.to_i - io << " microseconds" + io << micros.round.to_i << " microseconds" end # Formats for milliseconds. private def format_millis(io, millis) - io << millis.round(2) - io << " milliseconds" + io << millis.round(2) << " milliseconds" end # Formats for seconds. private def format_seconds(io, seconds) - io << seconds.round(2) - io << " seconds" + io << seconds.round(2) << " seconds" end # Formats for minutes. diff --git a/src/spectator/formatting/components/totals.cr b/src/spectator/formatting/components/totals.cr index db4edab..5af5c30 100644 --- a/src/spectator/formatting/components/totals.cr +++ b/src/spectator/formatting/components/totals.cr @@ -32,19 +32,13 @@ module Spectator::Formatting::Components # Writes the counts to the output. def to_s(io) - io << @examples - io << " examples, " - io << @failures - io << " failures " + io << @examples << " examples, " << @failures << " failures " if @errors > 1 - io << '(' - io << @errors - io << " errors), " + io << '(' << @errors << " errors), " end - io << @pending - io << " pending" + io << @pending << " pending" end end end diff --git a/src/spectator/formatting/tap_formatter.cr b/src/spectator/formatting/tap_formatter.cr index d060041..fe5392f 100644 --- a/src/spectator/formatting/tap_formatter.cr +++ b/src/spectator/formatting/tap_formatter.cr @@ -24,27 +24,21 @@ module Spectator::Formatting # Invoked after an example completes successfully. def example_passed(notification) - @io << "ok " - @io << @counter - @io << " - " + @io << "ok " << @counter << " - " @io.puts notification.example end # Invoked after an example is skipped or marked as pending. def example_pending(notification) - @io << "not ok " # TODO: Skipped tests should report ok. - @io << @counter - @io << " - " - @io << notification.example - @io << " # TODO " + # TODO: Skipped tests should report ok. + @io << "not ok " << @counter << " - " + @io << notification.example << " # TODO " @io.puts "No reason given" # TODO: Get reason from result. end # Invoked after an example fails. def example_failed(notification) - @io << "not ok " - @io << @counter - @io << " - " + @io << "not ok " << @counter << " - " @io.puts notification.example end diff --git a/src/spectator/location.cr b/src/spectator/location.cr index bf1ac68..986d048 100644 --- a/src/spectator/location.cr +++ b/src/spectator/location.cr @@ -62,9 +62,7 @@ module Spectator # FILE:LINE # ``` def to_s(io) - io << path - io << ':' - io << line + io << path << ':' << line end # Creates the JSON representation of the location. diff --git a/src/spectator/mocks/double.cr b/src/spectator/mocks/double.cr index 9684aed..6768c49 100644 --- a/src/spectator/mocks/double.cr +++ b/src/spectator/mocks/double.cr @@ -124,9 +124,7 @@ module Spectator::Mocks end def to_s(io) - io << "Double(" - io << @spectator_double_name - io << ')' + io << "Double(" << @spectator_double_name << ')' end end end diff --git a/src/spectator/mocks/generic_arguments.cr b/src/spectator/mocks/generic_arguments.cr index 18d4193..156ef27 100644 --- a/src/spectator/mocks/generic_arguments.cr +++ b/src/spectator/mocks/generic_arguments.cr @@ -32,8 +32,7 @@ module Spectator::Mocks end io << ", " unless @args.empty? || @opts.empty? @opts.each_with_index do |key, value, i| - io << key - io << ": " + io << key << ": " value.inspect(io) io << ", " if i < @opts.size - 1 end diff --git a/src/spectator/mocks/generic_method_stub.cr b/src/spectator/mocks/generic_method_stub.cr index db4ff8f..15ef9b4 100644 --- a/src/spectator/mocks/generic_method_stub.cr +++ b/src/spectator/mocks/generic_method_stub.cr @@ -18,14 +18,9 @@ module Spectator::Mocks def to_s(io) super(io) if @args - io << '(' - io << @args - io << ')' + io << '(' << @args << ')' end - io << " : " - io << ReturnType - io << " at " - io << @location + io << " : " << ReturnType << " at " << @location end end end diff --git a/src/spectator/mocks/method_call.cr b/src/spectator/mocks/method_call.cr index f89138f..79b3a8b 100644 --- a/src/spectator/mocks/method_call.cr +++ b/src/spectator/mocks/method_call.cr @@ -7,8 +7,7 @@ module Spectator::Mocks end def to_s(io) - io << '#' - io << @name + io << '#' << @name end end end diff --git a/src/spectator/mocks/method_stub.cr b/src/spectator/mocks/method_stub.cr index 8419501..5dc56b8 100644 --- a/src/spectator/mocks/method_stub.cr +++ b/src/spectator/mocks/method_stub.cr @@ -26,8 +26,7 @@ module Spectator::Mocks end def to_s(io) - io << '#' - io << @name + io << '#' << @name end end end diff --git a/src/spectator/mocks/verifying_double.cr b/src/spectator/mocks/verifying_double.cr index 6c8e157..f7be6e3 100644 --- a/src/spectator/mocks/verifying_double.cr +++ b/src/spectator/mocks/verifying_double.cr @@ -100,9 +100,7 @@ module Spectator::Mocks end def to_s(io) - io << "Double(" - io << T - io << ')' + io << "Double(" << T << ')' end end end diff --git a/src/spectator/node.cr b/src/spectator/node.cr index 7a562e1..d1560c8 100644 --- a/src/spectator/node.cr +++ b/src/spectator/node.cr @@ -55,14 +55,11 @@ module Spectator # Exposes information about the node useful for debugging. def inspect(io) # Full node name. - io << '"' - to_s(io) - io << '"' + io << '"' << self << '"' # Add location if it's available. if (location = self.location) - io << " @ " - io << location + io << " @ " << location end end end From fa3e9dd34d0c95a273c9c76b558cf3413edcae2f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 30 May 2021 15:02:30 -0600 Subject: [PATCH 275/399] Implement JUnit formatter --- src/spectator/formatting/components.cr | 2 +- .../formatting/components/junit/root.cr | 39 +++++++ .../formatting/components/junit/test_case.cr | 86 +++++++++++++++ .../formatting/components/junit/test_suite.cr | 104 ++++++++++++++++++ src/spectator/formatting/junit_formatter.cr | 45 +++++++- src/spectator/report.cr | 3 + 6 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 src/spectator/formatting/components/junit/root.cr create mode 100644 src/spectator/formatting/components/junit/test_case.cr create mode 100644 src/spectator/formatting/components/junit/test_suite.cr diff --git a/src/spectator/formatting/components.cr b/src/spectator/formatting/components.cr index 9c7c7b0..9d888f3 100644 --- a/src/spectator/formatting/components.cr +++ b/src/spectator/formatting/components.cr @@ -1,4 +1,4 @@ -require "./components/*" +require "./components/**" module Spectator::Formatting # Namespace for snippets of text displayed in console output. diff --git a/src/spectator/formatting/components/junit/root.cr b/src/spectator/formatting/components/junit/root.cr new file mode 100644 index 0000000..f38f968 --- /dev/null +++ b/src/spectator/formatting/components/junit/root.cr @@ -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 diff --git a/src/spectator/formatting/components/junit/test_case.cr b/src/spectator/formatting/components/junit/test_case.cr new file mode 100644 index 0000000..f57cd5f --- /dev/null +++ b/src/spectator/formatting/components/junit/test_case.cr @@ -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 diff --git a/src/spectator/formatting/components/junit/test_suite.cr b/src/spectator/formatting/components/junit/test_suite.cr new file mode 100644 index 0000000..0c16809 --- /dev/null +++ b/src/spectator/formatting/components/junit/test_suite.cr @@ -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 diff --git a/src/spectator/formatting/junit_formatter.cr b/src/spectator/formatting/junit_formatter.cr index 32cec45..a777371 100644 --- a/src/spectator/formatting/junit_formatter.cr +++ b/src/spectator/formatting/junit_formatter.cr @@ -1,8 +1,51 @@ +require "xml" require "./formatter" module Spectator::Formatting + # Produces a JUnit compatible XML file containing the test results. 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 diff --git a/src/spectator/report.cr b/src/spectator/report.cr index d565531..93a88ea 100644 --- a/src/spectator/report.cr +++ b/src/spectator/report.cr @@ -22,6 +22,9 @@ module Spectator 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. # This includes examples, hooks, and framework processes. getter runtime : Time::Span From 8f3a7c0a5a42e9cf47c1696a6f8dc4bfb325202f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 2 Jun 2021 22:48:48 -0600 Subject: [PATCH 276/399] Basically done JSON formatter --- src/spectator/error_result.cr | 25 +--- src/spectator/example.cr | 10 +- src/spectator/expectation.cr | 5 +- src/spectator/fail_result.cr | 20 ++- src/spectator/formatting/json_formatter.cr | 135 +++++++++++++++++++++ src/spectator/pass_result.cr | 6 + src/spectator/pending_result.cr | 7 ++ src/spectator/result.cr | 21 ++-- 8 files changed, 188 insertions(+), 41 deletions(-) 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 From 39e917ce5737c6cedbf810dff3ed6849367c792f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 2 Jun 2021 23:09:30 -0600 Subject: [PATCH 277/399] Consistent location fields --- src/spectator/expectation.cr | 3 ++- src/spectator/location.cr | 7 ------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/spectator/expectation.cr b/src/spectator/expectation.cr index 55623d5..35710cb 100644 --- a/src/spectator/expectation.cr +++ b/src/spectator/expectation.cr @@ -56,7 +56,8 @@ module Spectator # Creates the JSON representation of the expectation. def to_json(json : JSON::Builder) json.object do - json.field("location") { @location.to_json(json) } + json.field("file_path", @location.path) + json.field("line_number", @location.line) json.field("satisfied", satisfied?) if (failed = @match_data.as?(Matchers::FailedMatchData)) failed_to_json(failed, json) diff --git a/src/spectator/location.cr b/src/spectator/location.cr index 986d048..688fdb6 100644 --- a/src/spectator/location.cr +++ b/src/spectator/location.cr @@ -1,5 +1,3 @@ -require "json" - module Spectator # Defines the file and line number a piece of code originated from. struct Location @@ -64,10 +62,5 @@ module Spectator def to_s(io) io << path << ':' << line end - - # Creates the JSON representation of the location. - def to_json(json : ::JSON::Builder) - json.string(to_s) - end end end From 835fa4077312de015b0844e1f1a27e8d21702fe8 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 2 Jun 2021 23:35:41 -0600 Subject: [PATCH 278/399] Handle nil location --- src/spectator/example.cr | 6 ++++-- src/spectator/expectation.cr | 6 ++++-- .../formatting/components/example_command.cr | 13 ++++++++++--- src/spectator/formatting/components/junit/root.cr | 2 +- .../formatting/components/junit/test_suite.cr | 2 +- src/spectator/formatting/components/profile.cr | 9 +++++++-- src/spectator/formatting/components/result_block.cr | 4 +++- src/spectator/formatting/components/tap_profile.cr | 9 +++++++-- src/spectator/formatting/json_formatter.cr | 7 +++++-- src/spectator/line_example_filter.cr | 6 ++++-- src/spectator/location_example_filter.cr | 2 +- 11 files changed, 47 insertions(+), 19 deletions(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 040b2ca..bdef6a3 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -169,8 +169,10 @@ module Spectator 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) + if location = location? + json.field("file_path", location.path) + json.field("line_number", location.line) + end @result.to_json(json) if @finished end end diff --git a/src/spectator/expectation.cr b/src/spectator/expectation.cr index 35710cb..1b19562 100644 --- a/src/spectator/expectation.cr +++ b/src/spectator/expectation.cr @@ -56,8 +56,10 @@ module Spectator # Creates the JSON representation of the expectation. def to_json(json : JSON::Builder) json.object do - json.field("file_path", @location.path) - json.field("line_number", @location.line) + if location = @location + json.field("file_path", location.path) + json.field("line_number", location.line) + end json.field("satisfied", satisfied?) if (failed = @match_data.as?(Matchers::FailedMatchData)) failed_to_json(failed, json) diff --git a/src/spectator/formatting/components/example_command.cr b/src/spectator/formatting/components/example_command.cr index 5dd4c05..8b3c0b9 100644 --- a/src/spectator/formatting/components/example_command.cr +++ b/src/spectator/formatting/components/example_command.cr @@ -11,9 +11,16 @@ module Spectator::Formatting::Components # Produces output for running the previously specified example. def to_s(io) io << "crystal spec " - io << @example.location - io << ' ' - io << Comment.colorize(@example.to_s) + + # Use location for argument if it's available, since it's simpler. + # Otherwise, use the example name filter argument. + if location = @example.location? + io << location + else + io << "-e " << @example + end + + io << ' ' << Comment.colorize(@example.to_s) end end end diff --git a/src/spectator/formatting/components/junit/root.cr b/src/spectator/formatting/components/junit/root.cr index f38f968..3c6f440 100644 --- a/src/spectator/formatting/components/junit/root.cr +++ b/src/spectator/formatting/components/junit/root.cr @@ -13,7 +13,7 @@ module Spectator::Formatting::Components::JUnit def self.from_report(report) hostname = System.hostname counts = report.counts - suites = report.examples.group_by(&.location.file) + suites = report.examples.group_by { |example| example.location?.try(&.path) || "anonymous" } suites = suites.map do |file, examples| TestSuite.from_examples(file, examples, hostname) end diff --git a/src/spectator/formatting/components/junit/test_suite.cr b/src/spectator/formatting/components/junit/test_suite.cr index 0c16809..28b5adc 100644 --- a/src/spectator/formatting/components/junit/test_suite.cr +++ b/src/spectator/formatting/components/junit/test_suite.cr @@ -26,7 +26,7 @@ module Spectator::Formatting::Components::JUnit private def self.package_name_from_file(file) path = Path.new(file.to_s) name = path.stem - directory = path.relative_to(Dir.current).dirname + directory = path.dirname package = directory.gsub(File::SEPARATOR, '.') {package, name} end diff --git a/src/spectator/formatting/components/profile.cr b/src/spectator/formatting/components/profile.cr index 7e35b30..2a56e0d 100644 --- a/src/spectator/formatting/components/profile.cr +++ b/src/spectator/formatting/components/profile.cr @@ -29,8 +29,13 @@ module Spectator::Formatting::Components io.puts example io << " " io << Runtime.new(example.result.elapsed).colorize.bold - io << ' ' - io.puts example.location + + if location = example.location + io << ' ' + io.puts location + else + io.puts + end end end end diff --git a/src/spectator/formatting/components/result_block.cr b/src/spectator/formatting/components/result_block.cr index 8ea7459..af7f841 100644 --- a/src/spectator/formatting/components/result_block.cr +++ b/src/spectator/formatting/components/result_block.cr @@ -71,8 +71,10 @@ module Spectator::Formatting::Components source = if (result = @example.result).responds_to?(:source) result.source else - @example.location + @example.location? end + return unless source + line(io) { io << Comment.colorize(source) } end diff --git a/src/spectator/formatting/components/tap_profile.cr b/src/spectator/formatting/components/tap_profile.cr index 8ae022f..64c36b0 100644 --- a/src/spectator/formatting/components/tap_profile.cr +++ b/src/spectator/formatting/components/tap_profile.cr @@ -30,8 +30,13 @@ module Spectator::Formatting::Components io.puts example io << "# " io << Runtime.new(example.result.elapsed) - io << ' ' - io.puts example.location + + if location = example.location? + io << ' ' + io.puts location + else + io.puts + end end end end diff --git a/src/spectator/formatting/json_formatter.cr b/src/spectator/formatting/json_formatter.cr index 46c8a88..0b80d0f 100644 --- a/src/spectator/formatting/json_formatter.cr +++ b/src/spectator/formatting/json_formatter.cr @@ -28,8 +28,11 @@ module Spectator::Formatting @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) + + if location = example.location? + @json.field("file_path", location.path) + @json.field("line_number", location.line) + end end # Adds fields to the example object for all result types known after the example completes. diff --git a/src/spectator/line_example_filter.cr b/src/spectator/line_example_filter.cr index 25b5fc8..22a7c38 100644 --- a/src/spectator/line_example_filter.cr +++ b/src/spectator/line_example_filter.cr @@ -7,8 +7,10 @@ module Spectator # Checks whether the example satisfies the filter. def includes?(example) : Bool - start_line = example.location.line - end_line = example.location.end_line + return false unless location = example.location? + + start_line = location.line + end_line = location.end_line (start_line..end_line).covers?(@line) end end diff --git a/src/spectator/location_example_filter.cr b/src/spectator/location_example_filter.cr index 116d1d9..0cae2bb 100644 --- a/src/spectator/location_example_filter.cr +++ b/src/spectator/location_example_filter.cr @@ -8,7 +8,7 @@ module Spectator # Checks whether the example satisfies the filter. def includes?(example) : Bool - @location === example.location + @location === example.location? end end end From 98ba607583e827dee1a93f50d12fef1ccf1fefc6 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 2 Jun 2021 23:37:01 -0600 Subject: [PATCH 279/399] Dumb whitespace --- src/spectator/formatting/json_formatter.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/formatting/json_formatter.cr b/src/spectator/formatting/json_formatter.cr index 0b80d0f..8709d35 100644 --- a/src/spectator/formatting/json_formatter.cr +++ b/src/spectator/formatting/json_formatter.cr @@ -58,7 +58,7 @@ module Spectator::Formatting 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. + @json.end_object # End example object. end # Adds failure-specific fields to an example object and closes it. From 8536fcf58ca306dc3045b7348ca1db8e2b307214 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 2 Jun 2021 23:41:23 -0600 Subject: [PATCH 280/399] Fix full_description being an object --- src/spectator/formatting/json_formatter.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/formatting/json_formatter.cr b/src/spectator/formatting/json_formatter.cr index 8709d35..850809b 100644 --- a/src/spectator/formatting/json_formatter.cr +++ b/src/spectator/formatting/json_formatter.cr @@ -27,7 +27,7 @@ module Spectator::Formatting @json.start_object @json.field("description", example.name? || "") - @json.field("full_description", example) + @json.field("full_description", example.to_s) if location = example.location? @json.field("file_path", location.path) From bd34b87e22b4fb76c8cc18f6c43cd65ee019716e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 2 Jun 2021 23:44:36 -0600 Subject: [PATCH 281/399] Simplify JSON formatter by reusing Example's to_json --- src/spectator/formatting/json_formatter.cr | 68 +--------------------- 1 file changed, 1 insertion(+), 67 deletions(-) diff --git a/src/spectator/formatting/json_formatter.cr b/src/spectator/formatting/json_formatter.cr index 850809b..d7f87a0 100644 --- a/src/spectator/formatting/json_formatter.cr +++ b/src/spectator/formatting/json_formatter.cr @@ -21,75 +21,9 @@ module Spectator::Formatting @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.to_s) - - if location = example.location? - @json.field("file_path", location.path) - @json.field("line_number", location.line) - end - 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 + notification.example.to_json(@json) end # Marks the end of the examples array. From 12f06abf1126697f61359f1e8e82ba1f20f7daa6 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 2 Jun 2021 23:51:53 -0600 Subject: [PATCH 282/399] Move profile JSON formatting into Profile class --- src/spectator/formatting/json_formatter.cr | 14 +------------- src/spectator/profile.cr | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/spectator/formatting/json_formatter.cr b/src/spectator/formatting/json_formatter.cr index d7f87a0..a0de70f 100644 --- a/src/spectator/formatting/json_formatter.cr +++ b/src/spectator/formatting/json_formatter.cr @@ -33,20 +33,8 @@ module Spectator::Formatting # 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 + notification.profile.to_json(@json) end end diff --git a/src/spectator/profile.cr b/src/spectator/profile.cr index e6bc612..382b85a 100644 --- a/src/spectator/profile.cr +++ b/src/spectator/profile.cr @@ -1,3 +1,6 @@ +require "json" +require "./example" + module Spectator # Information about the runtimes of examples. class Profile @@ -39,5 +42,20 @@ module Spectator def percentage time / @total_time * 100 end + + # Produces a JSON fragment containing the profiling information. + def to_json(json : JSON::Builder) + json.object do + json.field("examples") do + json.array do + @slowest.each(&.to_json(json)) + end + end + + json.field("slowest", @slowest.max_of(&.result.elapsed).total_seconds) + json.field("total", time.total_seconds) + json.field("percentage", percentage) + end + end end end From 15c5b0991d9905f09fe922afcef50412f983db12 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 2 Jun 2021 23:54:38 -0600 Subject: [PATCH 283/399] Fix doc --- src/spectator/formatting/json_formatter.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/formatting/json_formatter.cr b/src/spectator/formatting/json_formatter.cr index a0de70f..90bac6a 100644 --- a/src/spectator/formatting/json_formatter.cr +++ b/src/spectator/formatting/json_formatter.cr @@ -21,7 +21,7 @@ module Spectator::Formatting @json.start_array end - # Adds fields to the example object for all result types known after the example completes. + # Adds an object containing fields about the example. def example_finished(notification) notification.example.to_json(@json) end From 103597a7be3b6695975f598f91c35612a0711f60 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 2 Jun 2021 23:59:42 -0600 Subject: [PATCH 284/399] Update runtime JSON parsing to use new structure --- spec/helpers/result.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/helpers/result.cr b/spec/helpers/result.cr index 1f99b2e..edafd75 100644 --- a/spec/helpers/result.cr +++ b/spec/helpers/result.cr @@ -44,8 +44,8 @@ module Spectator::SpecHelpers # Extracts the result information from a `JSON::Any` object. def self.from_json_any(object : JSON::Any) - name = object["name"].as_s - outcome = parse_outcome_string(object["result"].as_s) + name = object["description"].as_s + outcome = parse_outcome_string(object["status"].as_s) expectations = if (list = object["expectations"].as_a?) list.map { |e| Expectation.from_json_any(e) } else From b738b6b3ff8ea3bf17d723104b054d6ecd1437b9 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 3 Jun 2021 22:04:11 -0600 Subject: [PATCH 285/399] Add item regarding reporting and formatting --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc0b4b1..67e3e3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Short-hand "should" syntax must be included by using `require "spectator/should"` - `it { should eq("foo") }` - Overhaul example creation and handling. - Overhaul storage of test values. +- Overhaul reporting and formatting. Cleaner output for failures and pending tests. - Cleanup and simplify DSL implementation. - Better error messages and detection when DSL methods are used when they shouldn't (i.e. `describe` inside `it`). - Prevent usage of reserved keywords in DSL (such as `initialize`). From a08d5202fe50b9d7567a7bdfecee428728c0c49f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 5 Jun 2021 12:51:46 -0600 Subject: [PATCH 286/399] Implement pending examples as lighweight examples Drop test code block if a pending, skip, or x-prefix macro is used. --- src/spectator/dsl/builder.cr | 8 +++++ src/spectator/dsl/examples.cr | 56 +++++++++++++++++++++++++++++++---- src/spectator/example.cr | 12 ++++++++ src/spectator/spec/builder.cr | 19 ++++++++++++ 4 files changed, 89 insertions(+), 6 deletions(-) diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr index 3d9ec47..3054f18 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -37,6 +37,14 @@ module Spectator::DSL @@builder.add_example(*args, &block) end + # Defines a new pending example. + # The example is added to the group currently on the top of the stack. + # + # See `Spec::Builder#add_pending_example` for usage details. + def add_pending_example(*args) + @@builder.add_pending_example(*args) + end + # Defines a block of code to execute before any and all examples in the current group. def before_all(location = nil, label = "before_all", &block) hook = ExampleGroupHook.new(location: location, label: label, &block) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 9cc9c85..53b01b5 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -11,6 +11,10 @@ module Spectator::DSL # Defines a macro to generate code for an example. # The *name* is the name given to the macro. # + # In addition, another macro is defined that marks the example as pending. + # The pending macro is prefixed with 'x'. + # For instance, `define_example :it` defines `it` and `xit`. + # # Default tags can be provided with *tags* and *metadata*. # The tags are merged with parent groups. # Any items with falsey values from *metadata* remove the corresponding tag. @@ -60,6 +64,50 @@ module Spectator::DSL end end end + + define_pending_example :x{{name.id}} + end + + # Defines a macro to generate code for a pending example. + # The *name* is the name given to the macro. + # + # The block for the example's content is discarded at compilation time. + # This prevents issues with undefined methods, signature differences, etc. + # + # Default tags can be provided with *tags* and *metadata*. + # The tags are merged with parent groups. + # Any items with falsey values from *metadata* remove the corresponding tag. + macro define_pending_example(name, *tags, **metadata) + # Defines a pending example. + # + # If a block is given, it is treated as the code to test. + # The block is provided the current example instance as an argument. + # + # The first argument names the example (test). + # Typically, this specifies what is being tested. + # It has no effect on the test and is purely used for output. + # If omitted, a name is generated from the first assertion in the test. + # + # The example will be marked as pending if the block is omitted. + # A block or name must be provided. + # + # Tags can be specified by adding symbols (keywords) after the first argument. + # Key-value pairs can also be specified. + # Any falsey items will remove a previously defined tag. + macro {{name.id}}(what = nil, *tags, **metadata, &block) + \{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %} + \{% raise "A description or block must be provided. Cannot use '{{name.id}}' alone." unless what || block %} + \{% raise "Block argument count '{{name.id}}' hook must be 0..1" if block.args.size > 1 %} + + _spectator_tags(%tags, :tags, {{tags.splat(",")}} {{metadata.double_splat}}) + _spectator_tags(\%tags, %tags, \{{tags.splat(",")}} \{{metadata.double_splat}}) + + ::Spectator::DSL::Builder.add_pending_example( + _spectator_example_name(\{{what}}), + ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}, \{{block.end_line_number}}), + \%tags + ) + end end # Inserts the correct representation of a example's name. @@ -82,12 +130,8 @@ module Spectator::DSL define_example :specify - define_example :xexample, :pending + define_pending_example :pending - define_example :xspecify, :pending - - define_example :xit, :pending - - define_example :skip, :pending + define_pending_example :skip end end diff --git a/src/spectator/example.cr b/src/spectator/example.cr index bdef6a3..d3ac530 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -66,6 +66,18 @@ module Spectator group << self if group end + # Creates a pending example. + # The *name* describes the purpose of the example. + # It can be a `Symbol` to describe a type. + # The *location* tracks where the example exists in source code. + # The example will be assigned to *group* if it is provided. + # A set of *tags* can be used for filtering and modifying example behavior. + # Note: The tags will not be merged with the parent tags. + def self.pending(name : String? = nil, location : Location? = nil, + group : ExampleGroup? = nil, tags = Tags.new) + new(name, location, group, tags.add(:pending)) { nil } + end + # Executes the test case. # Returns the result of the execution. # The result will also be stored in `#result`. diff --git a/src/spectator/spec/builder.cr b/src/spectator/spec/builder.cr index 6aa6188..090de03 100644 --- a/src/spectator/spec/builder.cr +++ b/src/spectator/spec/builder.cr @@ -105,6 +105,25 @@ module Spectator # The example is added to the current group by `Example` initializer. end + # Defines a new pending example. + # The example is added to the group currently on the top of the stack. + # + # The *name* is the name or brief description of the example. + # This should be a string or nil. + # When nil, the example's name will be an anonymous example reference. + # + # The *location* optionally defined where the example originates in source code. + # + # A set of *tags* can be used for filtering and modifying example behavior. + # For instance, adding a "pending" tag will mark the test as pending and skip execution. + # + # The newly created example is returned. + def add_pending_example(name, location, tags = Tags.new) : Example + Log.trace { "Add pending example: #{name} @ #{location}; tags: #{tags}" } + Example.pending(name, location, current_group, tags) + # The example is added to the current group by `Example` initializer. + end + # Attaches a hook to be invoked before any and all examples in the current group. def before_all(hook) Log.trace { "Add before_all hook #{hook}" } From b9d77321b3a9000fa691c947e4670233286e78a8 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 5 Jun 2021 22:44:34 -0600 Subject: [PATCH 287/399] Fix premature call of after_all hook --- src/spectator/example_group.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index 4c9ae9e..7acf234 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -64,7 +64,7 @@ module Spectator Log.trace { "Processing after_all hooks for #{self}" } call_hooks(hooks) - call_parent_hooks(:call_once_after_all) + call_parent_hooks(:call_once_after_all) if @group.try(&.finished?) end example_event before_each do |hooks, example| From 8d73434e0bb5d7c771faebcd0f32fd95aaf38fea Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 9 Jun 2021 21:57:17 -0600 Subject: [PATCH 288/399] Add ability to mark example skipped/pending mid-test --- src/spectator/dsl/expectations.cr | 13 +++++++++++++ src/spectator/example_pending.cr | 6 ++++++ src/spectator/harness.cr | 3 +++ 3 files changed, 22 insertions(+) create mode 100644 src/spectator/example_pending.cr diff --git a/src/spectator/dsl/expectations.cr b/src/spectator/dsl/expectations.cr index 137b363..92aaa1a 100644 --- a/src/spectator/dsl/expectations.cr +++ b/src/spectator/dsl/expectations.cr @@ -1,4 +1,5 @@ require "../block" +require "../example_pending" require "../expectation" require "../expectation_failed" require "../location" @@ -13,6 +14,18 @@ module Spectator::DSL raise ExpectationFailed.new(Location.new(_file, _line), message) end + # Mark the current test as pending and immediately abort. + # A reason can be specified with *message*. + def pending(message = "No reason given") + raise ExamplePending.new(message) + end + + # Mark the current test as skipped and immediately abort. + # A reason can be specified with *message*. + def skip(message = "No reason given") + raise ExamplePending.new(message) + end + # Starts an expectation. # This should be followed up with `Assertion::Target#to` or `Assertion::Target#to_not`. # The value passed in will be checked to see if it satisfies the conditions of the specified matcher. diff --git a/src/spectator/example_pending.cr b/src/spectator/example_pending.cr new file mode 100644 index 0000000..6099fed --- /dev/null +++ b/src/spectator/example_pending.cr @@ -0,0 +1,6 @@ +module Spectator + # Exception that indicates an example is pending and should be skipped. + # When raised within a test, the test should abort. + class ExamplePending < Exception + end +end diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index f865f4d..f5ffd96 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -1,4 +1,5 @@ require "./error_result" +require "./example_pending" require "./expectation" require "./mocks" require "./pass_result" @@ -119,6 +120,8 @@ module Spectator PassResult.new(elapsed, @expectations) when ExpectationFailed FailResult.new(elapsed, error, @expectations) + when ExamplePending + PendingResult.new(elapsed, @expectations) else ErrorResult.new(elapsed, error, @expectations) end From 5a2a71ffe894cdc9f40b0424e142c83aa05df9d7 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 9 Jun 2021 22:15:15 -0600 Subject: [PATCH 289/399] Pass and output along reason for pending/skip result --- src/spectator/example.cr | 2 +- src/spectator/formatting/components/junit/test_case.cr | 4 ++-- .../formatting/components/pending_result_block.cr | 2 +- src/spectator/formatting/tap_formatter.cr | 6 +++++- src/spectator/harness.cr | 2 +- src/spectator/pending_result.cr | 10 +++++++--- 6 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index d3ac530..cc9805c 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -26,7 +26,7 @@ module Spectator # Result of the last time the example ran. # Is pending if the example hasn't run. - getter result : Result = PendingResult.new + getter result : Result = PendingResult.new(Time::Span::ZERO, "Example not run") # Creates the example. # An instance to run the test code in is given by *context*. diff --git a/src/spectator/formatting/components/junit/test_case.cr b/src/spectator/formatting/components/junit/test_case.cr index f57cd5f..5545fd4 100644 --- a/src/spectator/formatting/components/junit/test_case.cr +++ b/src/spectator/formatting/components/junit/test_case.cr @@ -78,8 +78,8 @@ module Spectator::Formatting::Components::JUnit end # Adds a skipped element to the test case node. - def pending(_result) - @xml.element("skipped") # TODO: Populate message attribute with reason from result. + def pending(result) + @xml.element("skipped", message: result.reason) end end end diff --git a/src/spectator/formatting/components/pending_result_block.cr b/src/spectator/formatting/components/pending_result_block.cr index 12ee2a1..ea916c6 100644 --- a/src/spectator/formatting/components/pending_result_block.cr +++ b/src/spectator/formatting/components/pending_result_block.cr @@ -13,7 +13,7 @@ module Spectator::Formatting::Components # Content displayed on the second line of the block after the label. private def subtitle - "No reason given" # TODO: Get reason from result. + @result.reason end # Prefix for the second line of the block. diff --git a/src/spectator/formatting/tap_formatter.cr b/src/spectator/formatting/tap_formatter.cr index fe5392f..e5991a3 100644 --- a/src/spectator/formatting/tap_formatter.cr +++ b/src/spectator/formatting/tap_formatter.cr @@ -33,7 +33,11 @@ module Spectator::Formatting # TODO: Skipped tests should report ok. @io << "not ok " << @counter << " - " @io << notification.example << " # TODO " - @io.puts "No reason given" # TODO: Get reason from result. + + # This should never be false. + if (result = notification.example.result).responds_to?(:reason) + @io.puts result.reason + end end # Invoked after an example fails. diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index f5ffd96..13ee409 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -121,7 +121,7 @@ module Spectator when ExpectationFailed FailResult.new(elapsed, error, @expectations) when ExamplePending - PendingResult.new(elapsed, @expectations) + PendingResult.new(elapsed, error.message || "No reason given", @expectations) else ErrorResult.new(elapsed, error, @expectations) end diff --git a/src/spectator/pending_result.cr b/src/spectator/pending_result.cr index 11ae93e..5cd61ef 100644 --- a/src/spectator/pending_result.cr +++ b/src/spectator/pending_result.cr @@ -5,10 +5,14 @@ module Spectator # A pending result means the example is not ready to run yet. # This can happen when the functionality to be tested is not implemented yet. class PendingResult < Result + # Reason the example was skipped or marked pending. + getter reason : String + # Creates the result. # *elapsed* is the length of time it took to run the example. - def initialize(elapsed = Time::Span::ZERO, expectations = [] of Expectation) - super + # A *reason* for the skip/pending result can be specified. + def initialize(elapsed = Time::Span::ZERO, @reason = "No reason given", expectations = [] of Expectation) + super(elapsed, expectations) end # Calls the `pending` method on the *visitor*. @@ -40,7 +44,7 @@ module Spectator def to_json(json : JSON::Builder) super json.field("status", "pending") - json.field("pending_message", "Not implemented") # TODO: Provide pending message. + json.field("pending_message", @reason) end end end From d9088b39ca57dabc106cb1306a22d6965ec28b5d Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 11 Jun 2021 18:11:46 -0600 Subject: [PATCH 290/399] Add skip message when using xit (and variants) --- src/spectator/dsl/examples.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 53b01b5..17bfb96 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -65,7 +65,7 @@ module Spectator::DSL end end - define_pending_example :x{{name.id}} + define_pending_example :x{{name.id}}, pending: "Temporarily skipped with x{{name.id}}" end # Defines a macro to generate code for a pending example. From 4a9ec3df4a394a69ecd9acc219a78e97dafe4403 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 11 Jun 2021 18:26:55 -0600 Subject: [PATCH 291/399] Store tags with an optional string value --- src/spectator/dsl/tags.cr | 6 +++--- src/spectator/example.cr | 3 ++- src/spectator/node.cr | 2 +- src/spectator/tags.cr | 6 +++++- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/spectator/dsl/tags.cr b/src/spectator/dsl/tags.cr index f2dc268..44fbf66 100644 --- a/src/spectator/dsl/tags.cr +++ b/src/spectator/dsl/tags.cr @@ -6,15 +6,15 @@ module Spectator::DSL private macro _spectator_tags(name, source, *tags, **metadata) private def self.{{name.id}} %tags = {{source.id}} - {% unless tags.empty? %} - %tags.concat({ {{tags.map(&.id.symbolize).splat}} }) + {% for k in tags %} + %tags[{{k.id.symbolize}}] = nil {% end %} {% for k, v in metadata %} %cond = begin {{v}} end if %cond - %tags.add({{k.id.symbolize}}) + %tags[{{k.id.symbolize}}] = %cond else %tags.delete({{k.id.symbolize}}) end diff --git a/src/spectator/example.cr b/src/spectator/example.cr index cc9805c..7d9fbd1 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -75,7 +75,8 @@ module Spectator # Note: The tags will not be merged with the parent tags. def self.pending(name : String? = nil, location : Location? = nil, group : ExampleGroup? = nil, tags = Tags.new) - new(name, location, group, tags.add(:pending)) { nil } + tags = tags.merge({:pending => nil}) { |_, v, _| v } # Add pending tag if it doesn't exist. + new(name, location, group, tags) { nil } end # Executes the test case. diff --git a/src/spectator/node.cr b/src/spectator/node.cr index d1560c8..12db776 100644 --- a/src/spectator/node.cr +++ b/src/spectator/node.cr @@ -43,7 +43,7 @@ module Spectator # Checks if the node has been marked as pending. # Pending items should be skipped during execution. def pending? - tags.includes?(:pending) + tags.has_key?(:pending) end # Constructs the full name or description of the node. diff --git a/src/spectator/tags.cr b/src/spectator/tags.cr index e7c1065..e37a473 100644 --- a/src/spectator/tags.cr +++ b/src/spectator/tags.cr @@ -1,4 +1,8 @@ module Spectator # User-defined keywords used for filtering and behavior modification. - alias Tags = Set(Symbol) + # The value of a tag is optional, but may contain useful information. + # If the value is nil, the tag exists, but has no data. + # However, when tags are given on examples and example groups, + # if the value is falsey (false or nil), then the tag should be removed from the overall collection. + alias Tags = Hash(Symbol, String?) end From 3b1db7b7723189d43f606b9b3ff13abaa97731b1 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 11 Jun 2021 18:30:12 -0600 Subject: [PATCH 292/399] Pass along pending tag value if available --- src/spectator/example.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 7d9fbd1..e59c328 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -89,7 +89,7 @@ module Spectator if pending? Log.debug { "Skipping example #{self} - marked pending" } @finished = true - return @result = PendingResult.new + return @result = PendingResult.new(Time::Span::ZERO, tags[:pending] || "No reason given") end previous_example = @@current From b43b09f46dc618d5fa6bd7e12cf560782bee2842 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 11 Jun 2021 18:31:41 -0600 Subject: [PATCH 293/399] Change order of pending result parameters --- src/spectator/example.cr | 4 ++-- src/spectator/harness.cr | 2 +- src/spectator/pending_result.cr | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index e59c328..8cb921e 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -26,7 +26,7 @@ module Spectator # Result of the last time the example ran. # Is pending if the example hasn't run. - getter result : Result = PendingResult.new(Time::Span::ZERO, "Example not run") + getter result : Result = PendingResult.new("Example not run") # Creates the example. # An instance to run the test code in is given by *context*. @@ -89,7 +89,7 @@ module Spectator if pending? Log.debug { "Skipping example #{self} - marked pending" } @finished = true - return @result = PendingResult.new(Time::Span::ZERO, tags[:pending] || "No reason given") + return @result = PendingResult.new(tags[:pending] || "No reason given") end previous_example = @@current diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 13ee409..0813758 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -121,7 +121,7 @@ module Spectator when ExpectationFailed FailResult.new(elapsed, error, @expectations) when ExamplePending - PendingResult.new(elapsed, error.message || "No reason given", @expectations) + PendingResult.new(error.message || "No reason given", elapsed, @expectations) else ErrorResult.new(elapsed, error, @expectations) end diff --git a/src/spectator/pending_result.cr b/src/spectator/pending_result.cr index 5cd61ef..83fc689 100644 --- a/src/spectator/pending_result.cr +++ b/src/spectator/pending_result.cr @@ -11,7 +11,7 @@ module Spectator # Creates the result. # *elapsed* is the length of time it took to run the example. # A *reason* for the skip/pending result can be specified. - def initialize(elapsed = Time::Span::ZERO, @reason = "No reason given", expectations = [] of Expectation) + def initialize(@reason = "No reason given", elapsed = Time::Span::ZERO, expectations = [] of Expectation) super(elapsed, expectations) end From 420f69f56b9c435ddfee7deb9899364e4c3b6cfd Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 11 Jun 2021 18:45:05 -0600 Subject: [PATCH 294/399] Add pending changes to changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67e3e3c..ee0dd40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Capture and log information for hooks. - Tags can be added to examples and example groups. - Add matcher to check compiled type of values. +- Examples can be skipped by using a `:pending` tag. A reason method can be specified: `pending: "Some excuse"` +- Examples can be skipped during execution by using `skip` or `pending` in the example block. ### Changed - Simplify and reduce defined types and generics. Should speed up compilation times. From 4f2df78c34eff568e8f1e573afcfd577a1c9f808 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 11 Jun 2021 18:55:56 -0600 Subject: [PATCH 295/399] Deprecate current behavior of pending blocks --- CHANGELOG.md | 3 +++ src/spectator/dsl/examples.cr | 1 + 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee0dd40..955e152 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Prevent usage of reserved keywords in DSL (such as `initialize`). - Other minor internal improvements and cleanup. +### Deprecated +- `pending` blocks will behave differently in v0.11.0. They will mimic RSpec in that they _compile and run_ the block expecting it to fail. Use a `skip` (or `xit`) block instead to prevent compiling the example. + ### Removed - Removed one-liner it syntax without braces (block). diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 17bfb96..17619b7 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -130,6 +130,7 @@ module Spectator::DSL define_example :specify + @[Deprecated("Behavior of pending blocks will change in Spectator v0.11.0. Use `skip` instead.")] define_pending_example :pending define_pending_example :skip From 14d45756e9ffca6637a2fcf5695882691d39cb6c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 11 Jun 2021 18:59:10 -0600 Subject: [PATCH 296/399] Consolidate default pending reason --- src/spectator/dsl/expectations.cr | 5 +++-- src/spectator/example.cr | 2 +- src/spectator/harness.cr | 2 +- src/spectator/pending_result.cr | 4 +++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/spectator/dsl/expectations.cr b/src/spectator/dsl/expectations.cr index 92aaa1a..290c716 100644 --- a/src/spectator/dsl/expectations.cr +++ b/src/spectator/dsl/expectations.cr @@ -3,6 +3,7 @@ require "../example_pending" require "../expectation" require "../expectation_failed" require "../location" +require "../pending_result" require "../value" module Spectator::DSL @@ -16,13 +17,13 @@ module Spectator::DSL # Mark the current test as pending and immediately abort. # A reason can be specified with *message*. - def pending(message = "No reason given") + def pending(message = PendingResult::DEFAULT_REASON) raise ExamplePending.new(message) end # Mark the current test as skipped and immediately abort. # A reason can be specified with *message*. - def skip(message = "No reason given") + def skip(message = PendingResult::DEFAULT_REASON) raise ExamplePending.new(message) end diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 8cb921e..9c06135 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -89,7 +89,7 @@ module Spectator if pending? Log.debug { "Skipping example #{self} - marked pending" } @finished = true - return @result = PendingResult.new(tags[:pending] || "No reason given") + return @result = PendingResult.new(tags[:pending] || PendingResult::DEFAULT_REASON) end previous_example = @@current diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 0813758..08ccb2e 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -121,7 +121,7 @@ module Spectator when ExpectationFailed FailResult.new(elapsed, error, @expectations) when ExamplePending - PendingResult.new(error.message || "No reason given", elapsed, @expectations) + PendingResult.new(error.message || PendingResult::DEFAULT_REASON, elapsed, @expectations) else ErrorResult.new(elapsed, error, @expectations) end diff --git a/src/spectator/pending_result.cr b/src/spectator/pending_result.cr index 83fc689..0854fdc 100644 --- a/src/spectator/pending_result.cr +++ b/src/spectator/pending_result.cr @@ -5,13 +5,15 @@ module Spectator # A pending result means the example is not ready to run yet. # This can happen when the functionality to be tested is not implemented yet. class PendingResult < Result + DEFAULT_REASON = "No reason given" + # Reason the example was skipped or marked pending. getter reason : String # Creates the result. # *elapsed* is the length of time it took to run the example. # A *reason* for the skip/pending result can be specified. - def initialize(@reason = "No reason given", elapsed = Time::Span::ZERO, expectations = [] of Expectation) + def initialize(@reason = DEFAULT_REASON, elapsed = Time::Span::ZERO, expectations = [] of Expectation) super(elapsed, expectations) end From 12cba23fa37bfd157d6a2f7722b73a6f12b44374 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 11 Jun 2021 19:03:16 -0600 Subject: [PATCH 297/399] Treat skip tag as pending --- src/spectator/node.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/node.cr b/src/spectator/node.cr index 12db776..a4da16f 100644 --- a/src/spectator/node.cr +++ b/src/spectator/node.cr @@ -43,7 +43,7 @@ module Spectator # Checks if the node has been marked as pending. # Pending items should be skipped during execution. def pending? - tags.has_key?(:pending) + tags.has_key?(:pending) || tags.has_key?(:skip) end # Constructs the full name or description of the node. From a061bd20446011a6b7b7eaf71304f714ee9461ba Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 11 Jun 2021 19:29:29 -0600 Subject: [PATCH 298/399] Check pending, skip, and reason tags for reason --- src/spectator/example.cr | 2 +- src/spectator/node.cr | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 9c06135..c1757d6 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -89,7 +89,7 @@ module Spectator if pending? Log.debug { "Skipping example #{self} - marked pending" } @finished = true - return @result = PendingResult.new(tags[:pending] || PendingResult::DEFAULT_REASON) + return @result = PendingResult.new(pending_reason) end previous_example = @@current diff --git a/src/spectator/node.cr b/src/spectator/node.cr index a4da16f..63153c4 100644 --- a/src/spectator/node.cr +++ b/src/spectator/node.cr @@ -7,6 +7,9 @@ module Spectator # This is commonly an `Example` or `ExampleGroup`, # but can be anything that should be iterated over when running the spec. abstract class Node + # Default text used if none was given by the user for skipping a node. + DEFAULT_PENDING_REASON = "No reason given" + # Location of the node in source code. getter! location : Location @@ -46,6 +49,11 @@ module Spectator tags.has_key?(:pending) || tags.has_key?(:skip) end + # Gets the reason the node has been marked as pending. + def pending_reason + tags[:pending]? || tags[:skip]? || tags[:reason]? || DEFAULT_PENDING_REASON + end + # Constructs the full name or description of the node. # This prepends names of groups this node is part of. def to_s(io) From dcdc64e134fa2071279185522b63cbebd26f7afd Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 11 Jun 2021 19:30:23 -0600 Subject: [PATCH 299/399] Specify skip reason --- spec/rspec/core/explicit_subject_spec.cr | 8 +-- spec/rspec/core/let_spec.cr | 2 +- spec/rspec/expectations/all_matcher_spec.cr | 14 +++-- .../expectations/contain_matcher_spec.cr | 54 +++++++++---------- .../expectations/end_with_matcher_spec.cr | 5 +- .../have_attributes_matcher_spec.cr | 8 +-- .../expectations/raise_error_matcher_spec.cr | 15 +++--- .../expectations/start_with_matcher_spec.cr | 5 +- 8 files changed, 50 insertions(+), 61 deletions(-) diff --git a/spec/rspec/core/explicit_subject_spec.cr b/spec/rspec/core/explicit_subject_spec.cr index bc102e8..825f6bf 100644 --- a/spec/rspec/core/explicit_subject_spec.cr +++ b/spec/rspec/core/explicit_subject_spec.cr @@ -34,16 +34,16 @@ Spectator.describe "Explicit Subject" do subject { @@element_list.pop } - # TODO: RSpec calls the "actual" block after the "change block". - xit "is memoized across calls (i.e. the block is invoked once)" do + skip "is memoized across calls (i.e. the block is invoked once)", + reason: "RSpec calls the \"actual\" block after the \"change block\"." do expect do 3.times { subject } end.to change { @@element_list }.from([1, 2, 3]).to([1, 2]) expect(subject).to eq(3) end - # TODO: RSpec calls the "actual" block after the "change block". - xit "is not memoized across examples" do + skip "is not memoized across examples", + reason: "RSpec calls the \"actual\" block after the \"change block\"." do expect { subject }.to change { @@element_list }.from([1, 2]).to([1]) expect(subject).to eq(2) end diff --git a/spec/rspec/core/let_spec.cr b/spec/rspec/core/let_spec.cr index 815306d..a711fa9 100644 --- a/spec/rspec/core/let_spec.cr +++ b/spec/rspec/core/let_spec.cr @@ -11,7 +11,7 @@ Spectator.describe "Let and let!" do describe "let" do let(:count) { @@count += 1 } - it "memoizes thte value" do + it "memoizes the value" do expect(count).to eq(1) expect(count).to eq(1) end diff --git a/spec/rspec/expectations/all_matcher_spec.cr b/spec/rspec/expectations/all_matcher_spec.cr index 1d910e9..92d31d3 100644 --- a/spec/rspec/expectations/all_matcher_spec.cr +++ b/spec/rspec/expectations/all_matcher_spec.cr @@ -21,17 +21,15 @@ Spectator.describe "`all` matcher" do # Changed `include` to `contain` to match our own. # `include` is a keyword and can't be used as a method name in Crystal. - # TODO: Add support for compound matchers. describe ["anything", "everything", "something"] do - xit { is_expected.to all(be_a(String)) } # .and contain("thing") ) } - xit { is_expected.to all(be_a(String)) } # .and end_with("g") ) } - xit { is_expected.to all(start_with("s")) } # .or contain("y") ) } + skip reason: "Add support for compound matchers." { is_expected.to all(be_a(String).and contain("thing")) } + skip reason: "Add support for compound matchers." { is_expected.to all(be_a(String).and end_with("g")) } + skip reason: "Add support for compound matchers." { is_expected.to all(start_with("s").or contain("y")) } # deliberate failures - # TODO: Add support for compound matchers. - xit { is_expected.to all(contain("foo")) } # .and contain("bar") ) } - xit { is_expected.to all(be_a(String)) } # .and start_with("a") ) } - xit { is_expected.to all(start_with("a")) } # .or contain("z") ) } + skip reason: "Add support for compound matchers." { is_expected.to all(contain("foo").and contain("bar")) } + skip reason: "Add support for compound matchers." { is_expected.to all(be_a(String).and start_with("a")) } + skip reason: "Add support for compound matchers." { is_expected.to all(start_with("a").or contain("z")) } end end end diff --git a/spec/rspec/expectations/contain_matcher_spec.cr b/spec/rspec/expectations/contain_matcher_spec.cr index 6c8e558..b95d961 100644 --- a/spec/rspec/expectations/contain_matcher_spec.cr +++ b/spec/rspec/expectations/contain_matcher_spec.cr @@ -12,16 +12,14 @@ Spectator.describe "`contain` matcher" do it { is_expected.to contain(1, 7) } it { is_expected.to contain(1, 3, 7) } - # Utility matcher method `a_kind_of` is not supported. - # it { is_expected.to contain(a_kind_of(Int)) } + skip reason: "Utility matcher method `a_kind_of` is not supported." { is_expected.to contain(a_kind_of(Int)) } - # TODO: Compound matchers aren't supported. - # it { is_expected.to contain(be_odd.and be < 10) } + skip reason: "Compound matchers aren't supported." { is_expected.to contain(be_odd.and be < 10) } # TODO: Fix behavior and cleanup output. # This syntax is allowed, but produces a wrong result and bad output. - xit { is_expected.to contain(be_odd) } - xit { is_expected.not_to contain(be_even) } + skip reason: "Fix behavior and cleanup output." { is_expected.to contain(be_odd) } + skip reason: "Fix behavior and cleanup output." { is_expected.not_to contain(be_even) } it { is_expected.not_to contain(17) } it { is_expected.not_to contain(43, 100) } @@ -62,35 +60,31 @@ Spectator.describe "`contain` matcher" do subject { {:a => 7, :b => 5} } # Hash syntax is changed here from `:a => 7` to `a: 7`. - # it { is_expected.to contain(:a) } - # it { is_expected.to contain(:b, :a) } - - # TODO: This hash-like syntax isn't supported. - # it { is_expected.to contain(a: 7) } - # it { is_expected.to contain(b: 5, a: 7) } - # it { is_expected.not_to contain(:c) } - # it { is_expected.not_to contain(:c, :d) } - # it { is_expected.not_to contain(d: 2) } - # it { is_expected.not_to contain(a: 5) } - # it { is_expected.not_to contain(b: 7, a: 5) } + skip reason: "This hash-like syntax isn't supported." { is_expected.to contain(a: 7) } + skip reason: "This hash-like syntax isn't supported." { is_expected.to contain(b: 5, a: 7) } + skip reason: "This hash-like syntax isn't supported." { is_expected.not_to contain(:c) } + skip reason: "This hash-like syntax isn't supported." { is_expected.not_to contain(:c, :d) } + skip reason: "This hash-like syntax isn't supported." { is_expected.not_to contain(d: 2) } + skip reason: "This hash-like syntax isn't supported." { is_expected.not_to contain(a: 5) } + skip reason: "This hash-like syntax isn't supported." { is_expected.not_to contain(b: 7, a: 5) } # deliberate failures - # it { is_expected.not_to contain(:a) } - # it { is_expected.not_to contain(:b, :a) } - # it { is_expected.not_to contain(a: 7) } - # it { is_expected.not_to contain(a: 7, b: 5) } - # it { is_expected.to contain(:c) } - # it { is_expected.to contain(:c, :d) } - # it { is_expected.to contain(d: 2) } - # it { is_expected.to contain(a: 5) } - # it { is_expected.to contain(a: 5, b: 7) } + skip reason: "This hash-like syntax isn't supported." { is_expected.not_to contain(:a) } + skip reason: "This hash-like syntax isn't supported." { is_expected.not_to contain(:b, :a) } + skip reason: "This hash-like syntax isn't supported." { is_expected.not_to contain(a: 7) } + skip reason: "This hash-like syntax isn't supported." { is_expected.not_to contain(a: 7, b: 5) } + skip reason: "This hash-like syntax isn't supported." { is_expected.to contain(:c) } + skip reason: "This hash-like syntax isn't supported." { is_expected.to contain(:c, :d) } + skip reason: "This hash-like syntax isn't supported." { is_expected.to contain(d: 2) } + skip reason: "This hash-like syntax isn't supported." { is_expected.to contain(a: 5) } + skip reason: "This hash-like syntax isn't supported." { is_expected.to contain(a: 5, b: 7) } # Mixed cases--the hash contains one but not the other. # All 4 of these cases should fail. - # it { is_expected.to contain(:a, :d) } - # it { is_expected.not_to contain(:a, :d) } - # it { is_expected.to contain(a: 7, d: 3) } - # it { is_expected.not_to contain(a: 7, d: 3) } + skip reason: "This hash-like syntax isn't supported." { is_expected.to contain(:a, :d) } + skip reason: "This hash-like syntax isn't supported." { is_expected.not_to contain(:a, :d) } + skip reason: "This hash-like syntax isn't supported." { is_expected.to contain(a: 7, d: 3) } + skip reason: "This hash-like syntax isn't supported." { is_expected.not_to contain(a: 7, d: 3) } end end end diff --git a/spec/rspec/expectations/end_with_matcher_spec.cr b/spec/rspec/expectations/end_with_matcher_spec.cr index 75e6fd1..92d927a 100644 --- a/spec/rspec/expectations/end_with_matcher_spec.cr +++ b/spec/rspec/expectations/end_with_matcher_spec.cr @@ -18,10 +18,9 @@ Spectator.describe "`end_with` matcher" do context "array usage" do describe [0, 1, 2, 3, 4] do it { is_expected.to end_with 4 } - # TODO: Add support for multiple items at the end of an array. - # it { is_expected.to end_with 3, 4 } + skip reason: "Add support for multiple items at the end of an array." { is_expected.to end_with 3, 4 } it { is_expected.not_to end_with 3 } - # it { is_expected.not_to end_with 0, 1, 2, 3, 4, 5 } + skip reason: "Add support for multiple items at the end of an array." { is_expected.not_to end_with 0, 1, 2, 3, 4, 5 } # deliberate failures it_fails { is_expected.not_to end_with 4 } diff --git a/spec/rspec/expectations/have_attributes_matcher_spec.cr b/spec/rspec/expectations/have_attributes_matcher_spec.cr index 81c11dc..7bcb441 100644 --- a/spec/rspec/expectations/have_attributes_matcher_spec.cr +++ b/spec/rspec/expectations/have_attributes_matcher_spec.cr @@ -14,14 +14,14 @@ Spectator.describe "`have_attributes` matcher" do # Spectator doesn't support helper matchers like `a_string_starting_with` and `a_value <`. # But maybe in the future it will. it { is_expected.to have_attributes(name: "Jim") } - # it { is_expected.to have_attributes(name: a_string_starting_with("J") ) } + skip reason: "Add support for fuzzy matchers." { is_expected.to have_attributes(name: a_string_starting_with("J")) } it { is_expected.to have_attributes(age: 32) } - # it { is_expected.to have_attributes(age: (a_value > 30) ) } + skip reason: "Add support for fuzzy matchers." { is_expected.to have_attributes(age: (a_value > 30)) } it { is_expected.to have_attributes(name: "Jim", age: 32) } - # it { is_expected.to have_attributes(name: a_string_starting_with("J"), age: (a_value > 30) ) } + skip reason: "Add support for fuzzy matchers." { is_expected.to have_attributes(name: a_string_starting_with("J"), age: (a_value > 30)) } it { is_expected.not_to have_attributes(name: "Bob") } it { is_expected.not_to have_attributes(age: 10) } - # it { is_expected.not_to have_attributes(age: (a_value < 30) ) } + skip reason: "Add support for fuzzy matchers." { is_expected.not_to have_attributes(age: (a_value < 30)) } # deliberate failures it_fails { is_expected.to have_attributes(name: "Bob") } diff --git a/spec/rspec/expectations/raise_error_matcher_spec.cr b/spec/rspec/expectations/raise_error_matcher_spec.cr index 85678a7..1d262ce 100644 --- a/spec/rspec/expectations/raise_error_matcher_spec.cr +++ b/spec/rspec/expectations/raise_error_matcher_spec.cr @@ -75,14 +75,13 @@ Spectator.describe "`raise_error` matcher" do end end - # TODO: Support passing a block to `raise_error` matcher. - # context "set expectations on error object passed to block" do - # it "raises DivisionByZeroError" do - # expect { 42 // 0 }.to raise_error do |error| - # expect(error).to be_a(DivisionByZeroError) - # end - # end - # end + context "set expectations on error object passed to block" do + skip "raises DivisionByZeroError", reason: "Support passing a block to `raise_error` matcher." do + expect { 42 // 0 }.to raise_error do |error| + expect(error).to be_a(DivisionByZeroError) + end + end + end context "expect no error at all" do describe "#to_s" do diff --git a/spec/rspec/expectations/start_with_matcher_spec.cr b/spec/rspec/expectations/start_with_matcher_spec.cr index 471f1d6..74f8f8a 100644 --- a/spec/rspec/expectations/start_with_matcher_spec.cr +++ b/spec/rspec/expectations/start_with_matcher_spec.cr @@ -18,10 +18,9 @@ Spectator.describe "`start_with` matcher" do context "with an array" do describe [0, 1, 2, 3, 4] do it { is_expected.to start_with 0 } - # TODO: Add support for multiple items at the beginning of an array. - # it { is_expected.to start_with(0, 1) } + skip reason: "Add support for multiple items at the beginning of an array." { is_expected.to start_with(0, 1) } it { is_expected.not_to start_with(2) } - # it { is_expected.not_to start_with(0, 1, 2, 3, 4, 5) } + skip reason: "Add support for multiple items at the beginning of an array." { is_expected.not_to start_with(0, 1, 2, 3, 4, 5) } # deliberate failures it_fails { is_expected.not_to start_with 0 } From 117ed901852a2e48c8786c3a21f25cda56b0195e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 11 Jun 2021 20:22:08 -0600 Subject: [PATCH 300/399] Fix DSL usage of tags Ensure parent tags don't get modified by duplicating the hash. Force tag value to string. --- src/spectator/dsl/tags.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/dsl/tags.cr b/src/spectator/dsl/tags.cr index 44fbf66..0368cda 100644 --- a/src/spectator/dsl/tags.cr +++ b/src/spectator/dsl/tags.cr @@ -5,7 +5,7 @@ module Spectator::DSL # Any falsey items from *metadata* are removed. private macro _spectator_tags(name, source, *tags, **metadata) private def self.{{name.id}} - %tags = {{source.id}} + %tags = {{source.id}}.dup {% for k in tags %} %tags[{{k.id.symbolize}}] = nil {% end %} @@ -14,7 +14,7 @@ module Spectator::DSL {{v}} end if %cond - %tags[{{k.id.symbolize}}] = %cond + %tags[{{k.id.symbolize}}] = %cond.to_s else %tags.delete({{k.id.symbolize}}) end From 6f4cc12dfd022b53899e4909ec8b7aafee2d0b6a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 11 Jun 2021 21:03:46 -0600 Subject: [PATCH 301/399] Mark example pending if block is omitted --- src/spectator/dsl/examples.cr | 53 +++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 17619b7..399494f 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -18,8 +18,6 @@ module Spectator::DSL # Default tags can be provided with *tags* and *metadata*. # The tags are merged with parent groups. # Any items with falsey values from *metadata* remove the corresponding tag. - # - # TODO: Mark example as pending if block is omitted. macro define_example(name, *tags, **metadata) # Defines an example. # @@ -40,29 +38,39 @@ module Spectator::DSL macro {{name.id}}(what = nil, *tags, **metadata, &block) \{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %} \{% raise "A description or block must be provided. Cannot use '{{name.id}}' alone." unless what || block %} - \{% raise "Block argument count '{{name.id}}' hook must be 0..1" if block.args.size > 1 %} _spectator_tags(%tags, :tags, {{tags.splat(",")}} {{metadata.double_splat}}) _spectator_tags(\%tags, %tags, \{{tags.splat(",")}} \{{metadata.double_splat}}) - private def \%test(\{{block.args.splat}}) : Nil - \{{block.body}} - end + \{% if block %} + \{% raise "Block argument count '{{name.id}}' hook must be 0..1" if block.args.size > 1 %} - ::Spectator::DSL::Builder.add_example( - _spectator_example_name(\{{what}}), - ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}, \{{block.end_line_number}}), - new.as(::Spectator::Context), - \%tags - ) do |example| - example.with_context(\{{@type.name}}) do - \{% if block.args.empty? %} - \%test - \{% else %} - \%test(example) - \{% end %} + private def \%test(\{{block.args.splat}}) : Nil + \{{block.body}} end - end + + ::Spectator::DSL::Builder.add_example( + _spectator_example_name(\{{what}}), + ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}, \{{block.end_line_number}}), + new.as(::Spectator::Context), + \%tags + ) do |example| + example.with_context(\{{@type.name}}) do + \{% if block.args.empty? %} + \%test + \{% else %} + \%test(example) + \{% end %} + end + end + + \{% else %} + ::Spectator::DSL::Builder.add_pending_example( + _spectator_example_name(\{{what}}), + ::Spectator::Location.new(\{{what.filename}}, \{{what.line_number}}), + \%tags + ) + \{% end %} end define_pending_example :x{{name.id}}, pending: "Temporarily skipped with x{{name.id}}" @@ -88,23 +96,20 @@ module Spectator::DSL # It has no effect on the test and is purely used for output. # If omitted, a name is generated from the first assertion in the test. # - # The example will be marked as pending if the block is omitted. - # A block or name must be provided. - # # Tags can be specified by adding symbols (keywords) after the first argument. # Key-value pairs can also be specified. # Any falsey items will remove a previously defined tag. macro {{name.id}}(what = nil, *tags, **metadata, &block) \{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %} \{% raise "A description or block must be provided. Cannot use '{{name.id}}' alone." unless what || block %} - \{% raise "Block argument count '{{name.id}}' hook must be 0..1" if block.args.size > 1 %} + \{% raise "Block argument count '{{name.id}}' hook must be 0..1" if block && block.args.size > 1 %} _spectator_tags(%tags, :tags, {{tags.splat(",")}} {{metadata.double_splat}}) _spectator_tags(\%tags, %tags, \{{tags.splat(",")}} \{{metadata.double_splat}}) ::Spectator::DSL::Builder.add_pending_example( _spectator_example_name(\{{what}}), - ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}, \{{block.end_line_number}}), + ::Spectator::Location.new(\{{(what || block).filename}}, \{{(what || block).line_number}}, \{{(what || block).end_line_number}}), \%tags ) end From bfbeaf7454a5fdb2ca1e2a44540c788b794fe341 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 11 Jun 2021 21:16:46 -0600 Subject: [PATCH 302/399] Default reason for missing example block "Not yet implemented" --- src/spectator/dsl/examples.cr | 6 ++++-- src/spectator/example.cr | 5 +++-- src/spectator/spec/builder.cr | 5 +++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 399494f..21bf257 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -68,7 +68,8 @@ module Spectator::DSL ::Spectator::DSL::Builder.add_pending_example( _spectator_example_name(\{{what}}), ::Spectator::Location.new(\{{what.filename}}, \{{what.line_number}}), - \%tags + \%tags, + "Not yet implemented" ) \{% end %} end @@ -110,7 +111,8 @@ module Spectator::DSL ::Spectator::DSL::Builder.add_pending_example( _spectator_example_name(\{{what}}), ::Spectator::Location.new(\{{(what || block).filename}}, \{{(what || block).line_number}}, \{{(what || block).end_line_number}}), - \%tags + \%tags, + "Not yet implemented" ) end end diff --git a/src/spectator/example.cr b/src/spectator/example.cr index c1757d6..651aee7 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -74,8 +74,9 @@ module Spectator # A set of *tags* can be used for filtering and modifying example behavior. # Note: The tags will not be merged with the parent tags. def self.pending(name : String? = nil, location : Location? = nil, - group : ExampleGroup? = nil, tags = Tags.new) - tags = tags.merge({:pending => nil}) { |_, v, _| v } # Add pending tag if it doesn't exist. + group : ExampleGroup? = nil, tags = Tags.new, reason = nil) + # Add pending tag and reason if they don't exist. + tags = tags.merge({:pending => nil, :reason => reason}) { |_, v, _| v } new(name, location, group, tags) { nil } end diff --git a/src/spectator/spec/builder.cr b/src/spectator/spec/builder.cr index 090de03..ca938c2 100644 --- a/src/spectator/spec/builder.cr +++ b/src/spectator/spec/builder.cr @@ -116,11 +116,12 @@ module Spectator # # A set of *tags* can be used for filtering and modifying example behavior. # For instance, adding a "pending" tag will mark the test as pending and skip execution. + # A default *reason* can be given in case the user didn't provide one. # # The newly created example is returned. - def add_pending_example(name, location, tags = Tags.new) : Example + def add_pending_example(name, location, tags = Tags.new, reason = nil) : Example Log.trace { "Add pending example: #{name} @ #{location}; tags: #{tags}" } - Example.pending(name, location, current_group, tags) + Example.pending(name, location, current_group, tags, reason) # The example is added to the current group by `Example` initializer. end From 2407c43132754a3bc6e64006e82185d142617eab Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Jun 2021 09:53:42 -0600 Subject: [PATCH 303/399] Only show "Not yet implemented" for missing test block --- src/spectator/dsl/examples.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 21bf257..ac61711 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -112,7 +112,7 @@ module Spectator::DSL _spectator_example_name(\{{what}}), ::Spectator::Location.new(\{{(what || block).filename}}, \{{(what || block).line_number}}, \{{(what || block).end_line_number}}), \%tags, - "Not yet implemented" + \{% if !block %}"Not yet implemented"\{% end %} ) end end From a5ed5d0fb15ec6c7216aac3dae35b0084981551f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Jun 2021 10:33:28 -0600 Subject: [PATCH 304/399] Pass exception failure message to error --- src/spectator/harness.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 08ccb2e..7a4df88 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -81,7 +81,7 @@ module Spectator # TODO: Move this out of harness, maybe to `Example`. Example.current.name = expectation.description unless Example.current.name? - raise ExpectationFailed.new(expectation) if expectation.failed? + raise ExpectationFailed.new(expectation, expectation.failure_message) if expectation.failed? end # Stores a block of code to be executed later. From ba3a03736e17002b71ea7023cfbd47c453224db0 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Jun 2021 11:08:46 -0600 Subject: [PATCH 305/399] Show match data in failure block --- .../components/fail_result_block.cr | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/spectator/formatting/components/fail_result_block.cr b/src/spectator/formatting/components/fail_result_block.cr index dc5780d..2371bf3 100644 --- a/src/spectator/formatting/components/fail_result_block.cr +++ b/src/spectator/formatting/components/fail_result_block.cr @@ -1,19 +1,25 @@ require "colorize" require "../../example" +require "../../expectation" require "../../fail_result" require "./result_block" module Spectator::Formatting::Components # Displays information about a fail result. struct FailResultBlock < ResultBlock + @expectation : Expectation + @longest_key : Int32 + # Creates the component. - def initialize(index : Int32, example : Example, @result : FailResult) + def initialize(index : Int32, example : Example, result : FailResult) super(index, example) + @expectation = result.expectations.find(&.failed?).not_nil! + @longest_key = @expectation.values.max_of { |(key, _value)| key.to_s.size } end # Content displayed on the second line of the block after the label. private def subtitle - @result.error.message.try(&.each_line.first) + @expectation.failure_message end # Prefix for the second line of the block. @@ -23,7 +29,23 @@ module Spectator::Formatting::Components # Display expectation match data. private def content(io) - # TODO: Display match data. + indent do + @expectation.values.each do |(key, value)| + value_line(io, key, value) + end + end + + io.puts + end + + # Display a single line for a match data value. + private def value_line(io, key, value) + key = key.to_s + padding = " " * (@longest_key - key.size) + + line(io) do + io << padding << key.colorize(:red) << ": ".colorize(:red) << value + end end end end From dcdb87e31a99d6e27732c0a2c87abccb01d1d235 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Jun 2021 11:24:21 -0600 Subject: [PATCH 306/399] Output match data to XML --- .../formatting/components/junit/test_case.cr | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/spectator/formatting/components/junit/test_case.cr b/src/spectator/formatting/components/junit/test_case.cr index 5545fd4..be5ae45 100644 --- a/src/spectator/formatting/components/junit/test_case.cr +++ b/src/spectator/formatting/components/junit/test_case.cr @@ -62,8 +62,12 @@ module Spectator::Formatting::Components::JUnit # 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. + result.expectations.each do |expectation| + next unless expectation.failed? + + @xml.element("failure", message: expectation.failure_message, type: error.class) do + match_data(expectation.values) + end end end @@ -81,6 +85,17 @@ module Spectator::Formatting::Components::JUnit def pending(result) @xml.element("skipped", message: result.reason) end + + # Writes match data for a failed expectation. + private def match_data(values) + values.each do |(key, value)| + @xml.text("\n") + @xml.text(key.to_s) + @xml.text(": ") + @xml.text(value) + end + @xml.text("\n") + end end end end From 88f0c23a3e674ab01c67bb60c9365c1182344090 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Jun 2021 11:40:21 -0600 Subject: [PATCH 307/399] Add support for sub-index in result blocks --- .../formatting/components/error_result_block.cr | 4 ++-- .../formatting/components/fail_result_block.cr | 4 ++-- .../formatting/components/pending_result_block.cr | 4 ++-- .../formatting/components/result_block.cr | 15 ++++++++++++--- src/spectator/formatting/summary.cr | 6 +++--- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/spectator/formatting/components/error_result_block.cr b/src/spectator/formatting/components/error_result_block.cr index d27f294..990c501 100644 --- a/src/spectator/formatting/components/error_result_block.cr +++ b/src/spectator/formatting/components/error_result_block.cr @@ -7,8 +7,8 @@ module Spectator::Formatting::Components # Displays information about an error result. struct ErrorResultBlock < ResultBlock # Creates the component. - def initialize(index : Int32, example : Example, @result : ErrorResult) - super(index, example) + def initialize(example : Example, index : Int32, @result : ErrorResult) + super(example, index) end # Content displayed on the second line of the block after the label. diff --git a/src/spectator/formatting/components/fail_result_block.cr b/src/spectator/formatting/components/fail_result_block.cr index 2371bf3..77f536d 100644 --- a/src/spectator/formatting/components/fail_result_block.cr +++ b/src/spectator/formatting/components/fail_result_block.cr @@ -11,8 +11,8 @@ module Spectator::Formatting::Components @longest_key : Int32 # Creates the component. - def initialize(index : Int32, example : Example, result : FailResult) - super(index, example) + def initialize(example : Example, index : Int32, result : FailResult, subindex = 0) + super(example, index, subindex) @expectation = result.expectations.find(&.failed?).not_nil! @longest_key = @expectation.values.max_of { |(key, _value)| key.to_s.size } end diff --git a/src/spectator/formatting/components/pending_result_block.cr b/src/spectator/formatting/components/pending_result_block.cr index ea916c6..95c2481 100644 --- a/src/spectator/formatting/components/pending_result_block.cr +++ b/src/spectator/formatting/components/pending_result_block.cr @@ -7,8 +7,8 @@ module Spectator::Formatting::Components # Displays information about a pending result. struct PendingResultBlock < ResultBlock # Creates the component. - def initialize(index : Int32, example : Example, @result : PendingResult) - super(index, example) + def initialize(example : Example, index : Int32, @result : PendingResult) + super(example, index) end # Content displayed on the second line of the block after the label. diff --git a/src/spectator/formatting/components/result_block.cr b/src/spectator/formatting/components/result_block.cr index af7f841..189bfbe 100644 --- a/src/spectator/formatting/components/result_block.cr +++ b/src/spectator/formatting/components/result_block.cr @@ -14,7 +14,7 @@ module Spectator::Formatting::Components # ``` abstract struct ResultBlock < Block # Creates the block with the specified *index* and for the given *example*. - def initialize(@index : Int32, @example : Example) + def initialize(@example : Example, @index : Int32, @subindex : Int32 = 0) super() end @@ -55,7 +55,9 @@ module Spectator::Formatting::Components # Produces the title line. private def title_line(io) line(io) do - io << @index << ") " << title + io << @index + io << '.' << @subindex if @subindex > 0 + io << ") " << title end end @@ -80,7 +82,14 @@ module Spectator::Formatting::Components # Computes the number of spaces the index takes private def index_digit_count - (Math.log(@index.to_f + 1) / Math::LOG10).ceil.to_i + count = digit_count(@index) + count += 1 + digit_count(@subindex) if @subindex > 0 + count + end + + # Computes the number of spaces an integer takes. + private def digit_count(integer) + (Math.log(integer.to_f + 1) / Math::LOG10).ceil.to_i end end end diff --git a/src/spectator/formatting/summary.cr b/src/spectator/formatting/summary.cr index e66ced8..7007b88 100644 --- a/src/spectator/formatting/summary.cr +++ b/src/spectator/formatting/summary.cr @@ -25,7 +25,7 @@ module Spectator::Formatting io.puts examples.each_with_index(1) do |example, index| result = example.result.as(PendingResult) - io.puts Components::PendingResultBlock.new(index, example, result) + io.puts Components::PendingResultBlock.new(example, index, result) end end @@ -39,9 +39,9 @@ module Spectator::Formatting io.puts examples.each_with_index(1) do |example, index| if result = example.result.as?(ErrorResult) - io.puts Components::ErrorResultBlock.new(index, example, result) + io.puts Components::ErrorResultBlock.new(example, index, result) elsif result = example.result.as?(FailResult) - io.puts Components::FailResultBlock.new(index, example, result) + io.puts Components::FailResultBlock.new(example, index, result) end end end From 621ddb466fbbbf47d69556fd0412d45eb383fc99 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Jun 2021 11:59:57 -0600 Subject: [PATCH 308/399] Support output of multiple failed expectations --- src/spectator/formatting/components/fail_result_block.cr | 6 ++---- src/spectator/formatting/summary.cr | 9 ++++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/spectator/formatting/components/fail_result_block.cr b/src/spectator/formatting/components/fail_result_block.cr index 77f536d..74b2f60 100644 --- a/src/spectator/formatting/components/fail_result_block.cr +++ b/src/spectator/formatting/components/fail_result_block.cr @@ -7,14 +7,12 @@ require "./result_block" module Spectator::Formatting::Components # Displays information about a fail result. struct FailResultBlock < ResultBlock - @expectation : Expectation @longest_key : Int32 # Creates the component. - def initialize(example : Example, index : Int32, result : FailResult, subindex = 0) + def initialize(example : Example, index : Int32, @expectation : Expectation, subindex = 0) super(example, index, subindex) - @expectation = result.expectations.find(&.failed?).not_nil! - @longest_key = @expectation.values.max_of { |(key, _value)| key.to_s.size } + @longest_key = expectation.values.max_of { |(key, _value)| key.to_s.size } end # Content displayed on the second line of the block after the label. diff --git a/src/spectator/formatting/summary.cr b/src/spectator/formatting/summary.cr index 7007b88..29dccb2 100644 --- a/src/spectator/formatting/summary.cr +++ b/src/spectator/formatting/summary.cr @@ -41,7 +41,14 @@ module Spectator::Formatting if result = example.result.as?(ErrorResult) io.puts Components::ErrorResultBlock.new(example, index, result) elsif result = example.result.as?(FailResult) - io.puts Components::FailResultBlock.new(example, index, result) + failed_expectations = result.expectations.select(&.failed?) + if failed_expectations.size == 1 + io.puts Components::FailResultBlock.new(example, index, failed_expectations.first) + else + failed_expectations.each_with_index(1) do |expectation, subindex| + io.puts Components::FailResultBlock.new(example, index, expectation, subindex) + end + end end end end From 02a4b2946e49085d87366e6ce38b11aaa8872d21 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Jun 2021 12:10:44 -0600 Subject: [PATCH 309/399] Display failed expectations and error if an example had both --- .../components/error_result_block.cr | 4 +-- .../formatting/components/junit/test_case.cr | 1 + src/spectator/formatting/summary.cr | 36 ++++++++++++------- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/spectator/formatting/components/error_result_block.cr b/src/spectator/formatting/components/error_result_block.cr index 990c501..56d25e2 100644 --- a/src/spectator/formatting/components/error_result_block.cr +++ b/src/spectator/formatting/components/error_result_block.cr @@ -7,8 +7,8 @@ module Spectator::Formatting::Components # Displays information about an error result. struct ErrorResultBlock < ResultBlock # Creates the component. - def initialize(example : Example, index : Int32, @result : ErrorResult) - super(example, index) + def initialize(example : Example, index : Int32, @result : ErrorResult, subindex = 0) + super(example, index, subindex) end # Content displayed on the second line of the block after the label. diff --git a/src/spectator/formatting/components/junit/test_case.cr b/src/spectator/formatting/components/junit/test_case.cr index be5ae45..796c97f 100644 --- a/src/spectator/formatting/components/junit/test_case.cr +++ b/src/spectator/formatting/components/junit/test_case.cr @@ -74,6 +74,7 @@ module Spectator::Formatting::Components::JUnit # Adds an error element to the test case node. def error(result) error = result.error + fail(result) # Include failed expectations. @xml.element("error", message: error.message, type: error.class) do if backtrace = error.backtrace @xml.text(backtrace.join("\n")) diff --git a/src/spectator/formatting/summary.cr b/src/spectator/formatting/summary.cr index 29dccb2..50a2b1b 100644 --- a/src/spectator/formatting/summary.cr +++ b/src/spectator/formatting/summary.cr @@ -38,18 +38,7 @@ module Spectator::Formatting io.puts "Failures:" io.puts examples.each_with_index(1) do |example, index| - if result = example.result.as?(ErrorResult) - io.puts Components::ErrorResultBlock.new(example, index, result) - elsif result = example.result.as?(FailResult) - failed_expectations = result.expectations.select(&.failed?) - if failed_expectations.size == 1 - io.puts Components::FailResultBlock.new(example, index, failed_expectations.first) - else - failed_expectations.each_with_index(1) do |expectation, subindex| - io.puts Components::FailResultBlock.new(example, index, expectation, subindex) - end - end - end + dump_failed_example(example, index) end end @@ -70,5 +59,28 @@ module Spectator::Formatting io.puts Components::FailureCommandList.new(failures) end + + # Displays one or more blocks for a failed example. + # Each block is a failed expectation or error raised in the example. + private def dump_failed_example(example, index) + result = example.result.as?(ErrorResult) + failed_expectations = example.result.expectations.select(&.failed?) + block_count = failed_expectations.size + block_count += 1 if result + + # Don't use sub-index if there was only one problem. + if block_count == 1 + if result + io.puts Components::ErrorResultBlock.new(example, index, result) + else + io.puts Components::FailResultBlock.new(example, index, failed_expectations.first) + end + else + failed_expectations.each_with_index(1) do |expectation, subindex| + io.puts Components::FailResultBlock.new(example, index, expectation, subindex) + end + io.puts Components::ErrorResultBlock.new(example, index, result, block_count) if result + end + end end end From 71a5c39f6caa800ca6c2fdc7cd20e8f764372278 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Jun 2021 12:14:19 -0600 Subject: [PATCH 310/399] Use skip instead of pending tag Specify default reason for skipping groups. --- src/spectator/dsl/examples.cr | 2 +- src/spectator/dsl/groups.cr | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index ac61711..3310362 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -74,7 +74,7 @@ module Spectator::DSL \{% end %} end - define_pending_example :x{{name.id}}, pending: "Temporarily skipped with x{{name.id}}" + define_pending_example :x{{name.id}}, skip: "Temporarily skipped with x{{name.id}}" end # Defines a macro to generate code for a pending example. diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index fa78ade..4eee462 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -110,11 +110,11 @@ module Spectator::DSL define_example_group :context - define_example_group :xexample_group, :pending + define_example_group :xexample_group, skip: "Temporarily skipped with xexample_group" - define_example_group :xdescribe, :pending + define_example_group :xdescribe, skip: "Temporarily skipped with xdescribe" - define_example_group :xcontext, :pending + define_example_group :xcontext, skip: "Temporarily skipped with xcontext" # TODO: sample, random_sample, and given end From 704c28e8221e6377ab175f7ce26bcd9374f5570b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Jun 2021 16:23:38 -0600 Subject: [PATCH 311/399] Reimplement `given` as `provided` and deprecate The behavior is slightly different now. Nested example blocks aren't allowed in `provided`. The block produces one example, not multiple. --- CHANGELOG.md | 4 +++- src/spectator/dsl/concise.cr | 41 +++++++++++++++++++++++++++++++++++ src/spectator/dsl/groups.cr | 2 +- src/spectator/test_context.cr | 1 + 4 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 src/spectator/dsl/concise.cr diff --git a/CHANGELOG.md b/CHANGELOG.md index 955e152..3b22104 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Simplify and reduce defined types and generics. Should speed up compilation times. - `around_each` hooks wrap `before_all` and `after_all` hooks. [#12](https://github.com/icy-arctic-fox/spectator/issues/12) +- `given` (now `provided`) blocks changed to produce a single example. `it` can no longer be nested in a `provided` block. - The "should" syntax no longer reports the source as inside Spectator. - Short-hand "should" syntax must be included by using `require "spectator/should"` - `it { should eq("foo") }` - Overhaul example creation and handling. @@ -31,9 +32,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Deprecated - `pending` blocks will behave differently in v0.11.0. They will mimic RSpec in that they _compile and run_ the block expecting it to fail. Use a `skip` (or `xit`) block instead to prevent compiling the example. +- `given` has been renamed to `provided`. The `given` keyword may be reused later for memoization. ### Removed -- Removed one-liner it syntax without braces (block). +- Removed one-liner `it`-syntax without braces (block). ## [0.9.38] - 2021-05-27 ### Fixed diff --git a/src/spectator/dsl/concise.cr b/src/spectator/dsl/concise.cr new file mode 100644 index 0000000..0fb13d2 --- /dev/null +++ b/src/spectator/dsl/concise.cr @@ -0,0 +1,41 @@ +require "./examples" +require "./groups" +require "./memoize" + +module Spectator::DSL + # DSL methods and macros for shorter syntax. + module Concise + # Defines an example and input values in a shorter syntax. + # The only arguments given to this macro are one or more assignments. + # The names in the assigments will be available in the example code. + # + # If the code block is omitted, then the example is skipped (marked as not implemented). + # + # Tags and metadata cannot be used with this macro. + # + # ``` + # given x = 42 do + # expect(x).to eq(42) + # end + # ``` + macro provided(*assignments, &block) + class Given%given < {{@type.id}} + {% for assignment in assignments %} + let({{assignment.target}}) { {{assignment.value}} } + {% end %} + + {% if block %} + example {{block}} + {% else %} + example {{assignments.splat.stringify}} + {% end %} + end + end + + # :ditto: + @[Deprecated("Use `provided` instead.")] + macro given(*assignments, &block) + provided({{assignments.splat}}) {{block}} + end + end +end diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 4eee462..d1ec249 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -116,6 +116,6 @@ module Spectator::DSL define_example_group :xcontext, skip: "Temporarily skipped with xcontext" - # TODO: sample, random_sample, and given + # TODO: sample, random_sample end end diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr index 5559473..f02f334 100644 --- a/src/spectator/test_context.cr +++ b/src/spectator/test_context.cr @@ -8,6 +8,7 @@ require "./tags" # This type is intentionally outside the `Spectator` module. # The reason for this is to prevent name collision when using the DSL to define a spec. class SpectatorTestContext < SpectatorContext + include ::Spectator::DSL::Concise include ::Spectator::DSL::Examples include ::Spectator::DSL::Expectations include ::Spectator::DSL::Groups From 04d6c70f5942f13a4eb8ce94c5ae9a29c793ec2e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Jun 2021 16:45:45 -0600 Subject: [PATCH 312/399] Cleaner distinction between metadata and tags --- src/spectator/dsl/examples.cr | 18 +++++++------- src/spectator/dsl/groups.cr | 10 ++++---- src/spectator/dsl/{tags.cr => metadata.cr} | 16 ++++++------- src/spectator/example.cr | 28 +++++++++++----------- src/spectator/example_group.cr | 4 ++-- src/spectator/includes.cr | 2 +- src/spectator/{tags.cr => metadata.cr} | 5 +++- src/spectator/node.cr | 19 +++++++++------ src/spectator/spec/builder.cr | 26 ++++++++++---------- src/spectator/test_context.cr | 8 +++---- 10 files changed, 72 insertions(+), 64 deletions(-) rename src/spectator/dsl/{tags.cr => metadata.cr} (52%) rename src/spectator/{tags.cr => metadata.cr} (73%) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 3310362..27e6b60 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -1,12 +1,12 @@ require "../context" require "../location" require "./builder" -require "./tags" +require "./metadata" module Spectator::DSL # DSL methods for defining examples and test code. module Examples - include Tags + include Metadata # Defines a macro to generate code for an example. # The *name* is the name given to the macro. @@ -39,8 +39,8 @@ module Spectator::DSL \{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %} \{% raise "A description or block must be provided. Cannot use '{{name.id}}' alone." unless what || block %} - _spectator_tags(%tags, :tags, {{tags.splat(",")}} {{metadata.double_splat}}) - _spectator_tags(\%tags, %tags, \{{tags.splat(",")}} \{{metadata.double_splat}}) + _spectator_metadata(%metadata, :metadata, {{tags.splat(",")}} {{metadata.double_splat}}) + _spectator_metadata(\%metadata, %metadata, \{{tags.splat(",")}} \{{metadata.double_splat}}) \{% if block %} \{% raise "Block argument count '{{name.id}}' hook must be 0..1" if block.args.size > 1 %} @@ -53,7 +53,7 @@ module Spectator::DSL _spectator_example_name(\{{what}}), ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}, \{{block.end_line_number}}), new.as(::Spectator::Context), - \%tags + \%metadata ) do |example| example.with_context(\{{@type.name}}) do \{% if block.args.empty? %} @@ -68,7 +68,7 @@ module Spectator::DSL ::Spectator::DSL::Builder.add_pending_example( _spectator_example_name(\{{what}}), ::Spectator::Location.new(\{{what.filename}}, \{{what.line_number}}), - \%tags, + \%metadata, "Not yet implemented" ) \{% end %} @@ -105,13 +105,13 @@ module Spectator::DSL \{% raise "A description or block must be provided. Cannot use '{{name.id}}' alone." unless what || block %} \{% raise "Block argument count '{{name.id}}' hook must be 0..1" if block && block.args.size > 1 %} - _spectator_tags(%tags, :tags, {{tags.splat(",")}} {{metadata.double_splat}}) - _spectator_tags(\%tags, %tags, \{{tags.splat(",")}} \{{metadata.double_splat}}) + _spectator_metadata(%metadata, :metadata, {{tags.splat(",")}} {{metadata.double_splat}}) + _spectator_metadata(\%metadata, %metadata, \{{tags.splat(",")}} \{{metadata.double_splat}}) ::Spectator::DSL::Builder.add_pending_example( _spectator_example_name(\{{what}}), ::Spectator::Location.new(\{{(what || block).filename}}, \{{(what || block).line_number}}, \{{(what || block).end_line_number}}), - \%tags, + \%metadata, \{% if !block %}"Not yet implemented"\{% end %} ) end diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index d1ec249..9eb0091 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -1,13 +1,13 @@ require "../location" require "./builder" -require "./tags" require "./memoize" +require "./metadata" module Spectator::DSL # DSL methods and macros for creating example groups. # This module should be included as a mix-in. module Groups - include Tags + include Metadata # Defines a macro to generate code for an example group. # The *name* is the name given to the macro. @@ -36,13 +36,13 @@ module Spectator::DSL class Group\%group < \{{@type.id}} _spectator_group_subject(\{{what}}) - _spectator_tags(:tags, :super, {{tags.splat(", ")}} {{metadata.double_splat}}) - _spectator_tags(:tags, :previous_def, \{{tags.splat(", ")}} \{{metadata.double_splat}}) + _spectator_metadata(:metadata, :super, {{tags.splat(", ")}} {{metadata.double_splat}}) + _spectator_metadata(:metadata, :previous_def, \{{tags.splat(", ")}} \{{metadata.double_splat}}) ::Spectator::DSL::Builder.start_group( _spectator_group_name(\{{what}}), ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}), - tags + metadata ) \{{block.body}} diff --git a/src/spectator/dsl/tags.cr b/src/spectator/dsl/metadata.cr similarity index 52% rename from src/spectator/dsl/tags.cr rename to src/spectator/dsl/metadata.cr index 0368cda..308bcbd 100644 --- a/src/spectator/dsl/tags.cr +++ b/src/spectator/dsl/metadata.cr @@ -1,25 +1,25 @@ module Spectator::DSL - module Tags - # Defines a class method named *name* that combines tags + module Metadata + # Defines a class method named *name* that combines metadata # returned by *source* with *tags* and *metadata*. # Any falsey items from *metadata* are removed. - private macro _spectator_tags(name, source, *tags, **metadata) + private macro _spectator_metadata(name, source, *tags, **metadata) private def self.{{name.id}} - %tags = {{source.id}}.dup + %metadata = {{source.id}}.dup {% for k in tags %} - %tags[{{k.id.symbolize}}] = nil + %metadata[{{k.id.symbolize}}] = nil {% end %} {% for k, v in metadata %} %cond = begin {{v}} end if %cond - %tags[{{k.id.symbolize}}] = %cond.to_s + %metadata[{{k.id.symbolize}}] = %cond.to_s else - %tags.delete({{k.id.symbolize}}) + %metadata.delete({{k.id.symbolize}}) end {% end %} - %tags + %metadata end end end diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 651aee7..513cca8 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -5,7 +5,7 @@ require "./location" require "./node" require "./pending_result" require "./result" -require "./tags" +require "./metadata" module Spectator # Standard example that runs a test case. @@ -35,12 +35,12 @@ module Spectator # It can be a `Symbol` to describe a type. # The *location* tracks where the example exists in source code. # The example will be assigned to *group* if it is provided. - # A set of *tags* can be used for filtering and modifying example behavior. - # Note: The tags will not be merged with the parent tags. + # A set of *metadata* can be used for filtering and modifying example behavior. + # Note: The metadata will not be merged with the parent metadata. def initialize(@context : Context, @entrypoint : self ->, name : String? = nil, location : Location? = nil, - @group : ExampleGroup? = nil, tags = Tags.new) - super(name, location, tags) + @group : ExampleGroup? = nil, metadata = Metadata.new) + super(name, location, metadata) # Ensure group is linked. group << self if group @@ -53,11 +53,11 @@ module Spectator # It can be a `Symbol` to describe a type. # The *location* tracks where the example exists in source code. # The example will be assigned to *group* if it is provided. - # A set of *tags* can be used for filtering and modifying example behavior. - # Note: The tags will not be merged with the parent tags. + # A set of *metadata* can be used for filtering and modifying example behavior. + # Note: The metadata will not be merged with the parent metadata. def initialize(name : String? = nil, location : Location? = nil, - @group : ExampleGroup? = nil, tags = Tags.new, &block : self ->) - super(name, location, tags) + @group : ExampleGroup? = nil, metadata = Metadata.new, &block : self ->) + super(name, location, metadata) @context = NullContext.new @entrypoint = block @@ -71,13 +71,13 @@ module Spectator # It can be a `Symbol` to describe a type. # The *location* tracks where the example exists in source code. # The example will be assigned to *group* if it is provided. - # A set of *tags* can be used for filtering and modifying example behavior. - # Note: The tags will not be merged with the parent tags. + # A set of *metadata* can be used for filtering and modifying example behavior. + # Note: The metadata will not be merged with the parent metadata. def self.pending(name : String? = nil, location : Location? = nil, - group : ExampleGroup? = nil, tags = Tags.new, reason = nil) + group : ExampleGroup? = nil, metadata = Metadata.new, reason = nil) # Add pending tag and reason if they don't exist. - tags = tags.merge({:pending => nil, :reason => reason}) { |_, v, _| v } - new(name, location, group, tags) { nil } + metadata = metadata.merge({:pending => nil, :reason => reason}) { |_, v, _| v } + new(name, location, group, metadata) { nil } end # Executes the test case. diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index 7acf234..23e0f75 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -86,9 +86,9 @@ module Spectator # It can be a `Symbol` to describe a type. # The *location* tracks where the group exists in source code. # This group will be assigned to the parent *group* if it is provided. - # A set of *tags* can be used for filtering and modifying example behavior. + # A set of *metadata* can be used for filtering and modifying example behavior. def initialize(@name : Label = nil, @location : Location? = nil, - @group : ExampleGroup? = nil, @tags : Tags = Tags.new) + @group : ExampleGroup? = nil, @metadata : Metadata = Metadata.new) # Ensure group is linked. group << self if group end diff --git a/src/spectator/includes.cr b/src/spectator/includes.cr index 9e9dd5f..c611475 100644 --- a/src/spectator/includes.cr +++ b/src/spectator/includes.cr @@ -37,6 +37,7 @@ require "./line_example_filter" require "./location" require "./location_example_filter" require "./matchers" +require "./metadata" require "./mocks" require "./name_example_filter" require "./null_context" @@ -47,7 +48,6 @@ require "./profile" require "./report" require "./result" require "./spec" -require "./tags" require "./test_context" require "./value" require "./wrapper" diff --git a/src/spectator/tags.cr b/src/spectator/metadata.cr similarity index 73% rename from src/spectator/tags.cr rename to src/spectator/metadata.cr index e37a473..9baaab6 100644 --- a/src/spectator/tags.cr +++ b/src/spectator/metadata.cr @@ -1,8 +1,11 @@ module Spectator + # User-defined keywords used for filtering and behavior modification. + alias Tags = Set(Symbol) + # User-defined keywords used for filtering and behavior modification. # The value of a tag is optional, but may contain useful information. # If the value is nil, the tag exists, but has no data. # However, when tags are given on examples and example groups, # if the value is falsey (false or nil), then the tag should be removed from the overall collection. - alias Tags = Hash(Symbol, String?) + alias Metadata = Hash(Symbol, String?) end diff --git a/src/spectator/node.cr b/src/spectator/node.cr index 63153c4..4979f6e 100644 --- a/src/spectator/node.cr +++ b/src/spectator/node.cr @@ -1,6 +1,6 @@ require "./label" require "./location" -require "./tags" +require "./metadata" module Spectator # A single item in a test spec. @@ -29,15 +29,15 @@ module Spectator protected def name=(@name : String) end - # User-defined keywords used for filtering and behavior modification. - getter tags : Tags + # User-defined tags and values used for filtering and behavior modification. + getter metadata : Metadata # Creates the node. # The *name* describes the purpose of the node. # It can be a `Symbol` to describe a type. # The *location* tracks where the node exists in source code. - # A set of *tags* can be used for filtering and modifying example behavior. - def initialize(@name : Label = nil, @location : Location? = nil, @tags : Tags = Tags.new) + # A set of *metadata* can be used for filtering and modifying example behavior. + def initialize(@name : Label = nil, @location : Location? = nil, @metadata : Metadata = Metadata.new) end # Indicates whether the node has completed. @@ -46,12 +46,17 @@ module Spectator # Checks if the node has been marked as pending. # Pending items should be skipped during execution. def pending? - tags.has_key?(:pending) || tags.has_key?(:skip) + metadata.has_key?(:pending) || metadata.has_key?(:skip) end # Gets the reason the node has been marked as pending. def pending_reason - tags[:pending]? || tags[:skip]? || tags[:reason]? || DEFAULT_PENDING_REASON + metadata[:pending]? || metadata[:skip]? || metadata[:reason]? || DEFAULT_PENDING_REASON + end + + # Retrieves just the tag names applied to the node. + def tags + Tags.new(metadata.keys) end # Constructs the full name or description of the node. diff --git a/src/spectator/spec/builder.cr b/src/spectator/spec/builder.cr index ca938c2..f1b401d 100644 --- a/src/spectator/spec/builder.cr +++ b/src/spectator/spec/builder.cr @@ -3,7 +3,7 @@ require "../example" require "../example_context_method" require "../example_group" require "../spec" -require "../tags" +require "../metadata" module Spectator class Spec @@ -53,14 +53,14 @@ module Spectator # # The *location* optionally defined where the group originates in source code. # - # A set of *tags* can be used for filtering and modifying example behavior. + # A set of *metadata* can be used for filtering and modifying example behavior. # For instance, adding a "pending" tag will mark tests as pending and skip execution. # # The newly created group is returned. # It shouldn't be used outside of this class until a matching `#end_group` is called. - def start_group(name, location = nil, tags = Tags.new) : ExampleGroup - Log.trace { "Start group: #{name.inspect} @ #{location}; tags: #{tags}" } - ExampleGroup.new(name, location, current_group, tags).tap do |group| + def start_group(name, location = nil, metadata = Metadata.new) : ExampleGroup + Log.trace { "Start group: #{name.inspect} @ #{location}; metadata: #{metadata}" } + ExampleGroup.new(name, location, current_group, metadata).tap do |group| @group_stack << group end end @@ -90,7 +90,7 @@ module Spectator # The *context* is an instance of the context the test code should run in. # See `Context` for more information. # - # A set of *tags* can be used for filtering and modifying example behavior. + # A set of *metadata* can be used for filtering and modifying example behavior. # For instance, adding a "pending" tag will mark the test as pending and skip execution. # # A block must be provided. @@ -99,9 +99,9 @@ module Spectator # It is expected that the test code runs when the block is called. # # The newly created example is returned. - def add_example(name, location, context, tags = Tags.new, &block : Example -> _) : Example - Log.trace { "Add example: #{name} @ #{location}; tags: #{tags}" } - Example.new(context, block, name, location, current_group, tags) + def add_example(name, location, context, metadata = Metadata.new, &block : Example -> _) : Example + Log.trace { "Add example: #{name} @ #{location}; metadata: #{metadata}" } + Example.new(context, block, name, location, current_group, metadata) # The example is added to the current group by `Example` initializer. end @@ -114,14 +114,14 @@ module Spectator # # The *location* optionally defined where the example originates in source code. # - # A set of *tags* can be used for filtering and modifying example behavior. + # A set of *metadata* can be used for filtering and modifying example behavior. # For instance, adding a "pending" tag will mark the test as pending and skip execution. # A default *reason* can be given in case the user didn't provide one. # # The newly created example is returned. - def add_pending_example(name, location, tags = Tags.new, reason = nil) : Example - Log.trace { "Add pending example: #{name} @ #{location}; tags: #{tags}" } - Example.pending(name, location, current_group, tags, reason) + def add_pending_example(name, location, metadata = Metadata.new, reason = nil) : Example + Log.trace { "Add pending example: #{name} @ #{location}; metadata: #{metadata}" } + Example.pending(name, location, current_group, metadata, reason) # The example is added to the current group by `Example` initializer. end diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr index f02f334..a68c5b9 100644 --- a/src/spectator/test_context.cr +++ b/src/spectator/test_context.cr @@ -1,7 +1,7 @@ require "./context" require "./dsl" require "./lazy_wrapper" -require "./tags" +require "./metadata" # Class used as the base for all specs using the DSL. # It adds methods and macros necessary to use the DSL from the spec. @@ -32,9 +32,9 @@ class SpectatorTestContext < SpectatorContext @subject.get { _spectator_implicit_subject } end - # Initial tags for tests. + # Initial metadata for tests. # This method should be overridden by example groups and examples. - private def self.tags - ::Spectator::Tags.new + private def self.metadata + ::Spectator::Metadata.new end end From e51ad6d504d02603e9c5b6588e9dcd4ca9e851d7 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Jun 2021 17:06:43 -0600 Subject: [PATCH 313/399] Check if `provided` and `given` are used in a method --- src/spectator/dsl/concise.cr | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/spectator/dsl/concise.cr b/src/spectator/dsl/concise.cr index 0fb13d2..bba29f2 100644 --- a/src/spectator/dsl/concise.cr +++ b/src/spectator/dsl/concise.cr @@ -19,6 +19,8 @@ module Spectator::DSL # end # ``` macro provided(*assignments, &block) + {% raise "Cannot use 'provided' inside of a test block" if @def %} + class Given%given < {{@type.id}} {% for assignment in assignments %} let({{assignment.target}}) { {{assignment.value}} } @@ -35,6 +37,7 @@ module Spectator::DSL # :ditto: @[Deprecated("Use `provided` instead.")] macro given(*assignments, &block) + {% raise "Cannot use 'given' inside of a test block" if @def %} provided({{assignments.splat}}) {{block}} end end From 4ff27defff5ce1d6cb8c0eeaa422a7ed57bc940c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 13 Jun 2021 13:16:31 -0600 Subject: [PATCH 314/399] Initial code for iterative (sample) groups --- src/spectator/dsl/builder.cr | 9 ++++++ src/spectator/iterative_example_group.cr | 37 ++++++++++++++++++++++++ src/spectator/spec/builder.cr | 22 ++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 src/spectator/iterative_example_group.cr diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr index 3054f18..9ebedaf 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -21,6 +21,15 @@ module Spectator::DSL @@builder.start_group(*args) end + # Defines a new iterative example group and pushes it onto the group stack. + # Examples and groups defined after calling this method will be nested under the new group. + # The group will be finished and popped off the stack when `#end_example` is called. + # + # See `Spec::Builder#start_iterative_group` for usage details. + def start_iterative_group(*args) + @@builder.start_iterative_group(*args) + end + # Completes a previously defined example group and pops it off the group stack. # Be sure to call `#start_group` and `#end_group` symmetically. # diff --git a/src/spectator/iterative_example_group.cr b/src/spectator/iterative_example_group.cr new file mode 100644 index 0000000..cf384a0 --- /dev/null +++ b/src/spectator/iterative_example_group.cr @@ -0,0 +1,37 @@ +require "./example_group" +require "./node" + +module Spectator + # Collection of examples and sub-groups executed multiple times. + # Each sub-node is executed once for each item in a given collection. + class IterativeExampleGroup(T) < ExampleGroup + # Creates the iterative example group. + # The *collection* is a list of items to iterative over each sub-node over. + # The *location* tracks where the group exists in source code. + # This group will be assigned to the parent *group* if it is provided. + # A set of *metadata* can be used for filtering and modifying example behavior. + def initialize(collection : Enumerable(T), location : Location? = nil, + group : ExampleGroup? = nil, metadata : Metadata = Metadata.new) + super() + + @nodes = collection.map do |item| + Iteration.new(item, location, group, metadata).as(Node) + end + end + + # Adds the specified *node* to the group. + # Assigns the node to this group. + # If the node already belongs to a group, + # it will be removed from the previous group before adding it to this group. + def <<(node : Node) + @nodes.each { |child| child.as(Iteration(T)) << node.dup } + end + + private class Iteration(T) < ExampleGroup + def initialize(@item : T, location : Location? = nil, + group : ExampleGroup? = nil, metadata : Metadata = Metadata.new) + super(@item.inspect, location, group, metadata) + end + end + end +end diff --git a/src/spectator/spec/builder.cr b/src/spectator/spec/builder.cr index f1b401d..8bee550 100644 --- a/src/spectator/spec/builder.cr +++ b/src/spectator/spec/builder.cr @@ -2,6 +2,7 @@ require "../config" require "../example" require "../example_context_method" require "../example_group" +require "../iterative_example_group" require "../spec" require "../metadata" @@ -65,6 +66,27 @@ module Spectator end end + # Defines a new iterative example group and pushes it onto the group stack. + # Examples and groups defined after calling this method will be nested under the new group. + # The group will be finished and popped off the stack when `#end_example` is called. + # + # The *collection* is the set of items to iterate over. + # Child nodes in this group will be executed once for every item in the collection. + # + # The *location* optionally defined where the group originates in source code. + # + # A set of *metadata* can be used for filtering and modifying example behavior. + # For instance, adding a "pending" tag will mark tests as pending and skip execution. + # + # The newly created group is returned. + # It shouldn't be used outside of this class until a matching `#end_group` is called. + def start_iterative_group(collection, location = nil, metadata = Metadata.new) : ExampleGroup + Log.trace { "Start iterative group: #{typeof(collection)} @ #{location}; metadata: #{metadata}" } + IterativeExampleGroup.new(collection, location, current_group, metadata).tap do |group| + @group_stack << group + end + end + # Completes a previously defined example group and pops it off the group stack. # Be sure to call `#start_group` and `#end_group` symmetically. # From 1f91836de1067a765fffda2baaffb2639b6075ee Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 13 Jun 2021 14:45:01 -0600 Subject: [PATCH 315/399] Use block to create examples Seems that nodes can't be duped/cloned easily. --- src/spectator/example_group.cr | 7 +++++++ src/spectator/iterative_example_group.cr | 7 +++++++ src/spectator/spec/builder.cr | 14 +++++++------- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index 23e0f75..01f0d0e 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -93,6 +93,13 @@ module Spectator group << self if group end + # Creates a child that is attched to the group. + # Yields zero or more times to create the child. + # The group the child should be attached to is provided as a block argument. + def create_child + yield self + end + # Removes the specified *node* from the group. # The node will be unassigned from this group. def delete(node : Node) diff --git a/src/spectator/iterative_example_group.cr b/src/spectator/iterative_example_group.cr index cf384a0..ca050b9 100644 --- a/src/spectator/iterative_example_group.cr +++ b/src/spectator/iterative_example_group.cr @@ -19,6 +19,13 @@ module Spectator end end + # Creates a child that is attched to the group. + # Yields zero or more times to create the child. + # The group the child should be attached to is provided as a block argument. + def create_child + @nodes.each { |child| yield child.as(Iteration(T)) } + end + # Adds the specified *node* to the group. # Assigns the node to this group. # If the node already belongs to a group, diff --git a/src/spectator/spec/builder.cr b/src/spectator/spec/builder.cr index 8bee550..f6ecee9 100644 --- a/src/spectator/spec/builder.cr +++ b/src/spectator/spec/builder.cr @@ -119,12 +119,11 @@ module Spectator # It will be yielded two arguments - the example created by this method, and the *context* argument. # The return value of the block is ignored. # It is expected that the test code runs when the block is called. - # - # The newly created example is returned. - def add_example(name, location, context, metadata = Metadata.new, &block : Example -> _) : Example + def add_example(name, location, context, metadata = Metadata.new, &block : Example -> _) Log.trace { "Add example: #{name} @ #{location}; metadata: #{metadata}" } - Example.new(context, block, name, location, current_group, metadata) - # The example is added to the current group by `Example` initializer. + current_group.create_child do |group| + Example.new(context, block, name, location, group, metadata) + end end # Defines a new pending example. @@ -143,8 +142,9 @@ module Spectator # The newly created example is returned. def add_pending_example(name, location, metadata = Metadata.new, reason = nil) : Example Log.trace { "Add pending example: #{name} @ #{location}; metadata: #{metadata}" } - Example.pending(name, location, current_group, metadata, reason) - # The example is added to the current group by `Example` initializer. + current_group.create_child do |group| + Example.pending(name, location, group, metadata, reason) + end end # Attaches a hook to be invoked before any and all examples in the current group. From 4b8d28c916f504ed3a6a7c4e94865c7d01ae9eb0 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 19 Jun 2021 10:54:31 -0600 Subject: [PATCH 316/399] Wording on compiler error for block args --- src/spectator/dsl/examples.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 27e6b60..ddfb51c 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -43,7 +43,7 @@ module Spectator::DSL _spectator_metadata(\%metadata, %metadata, \{{tags.splat(",")}} \{{metadata.double_splat}}) \{% if block %} - \{% raise "Block argument count '{{name.id}}' hook must be 0..1" if block.args.size > 1 %} + \{% raise "Block argument count '{{name.id}}' must be 0..1" if block.args.size > 1 %} private def \%test(\{{block.args.splat}}) : Nil \{{block.body}} @@ -103,7 +103,7 @@ module Spectator::DSL macro {{name.id}}(what = nil, *tags, **metadata, &block) \{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %} \{% raise "A description or block must be provided. Cannot use '{{name.id}}' alone." unless what || block %} - \{% raise "Block argument count '{{name.id}}' hook must be 0..1" if block && block.args.size > 1 %} + \{% raise "Block argument count '{{name.id}}' must be 0..1" if block && block.args.size > 1 %} _spectator_metadata(%metadata, :metadata, {{tags.splat(",")}} {{metadata.double_splat}}) _spectator_metadata(\%metadata, %metadata, \{{tags.splat(",")}} \{{metadata.double_splat}}) From 44ade24fb734272640afb62cf1231b1ce3fa581f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 19 Jun 2021 11:33:26 -0600 Subject: [PATCH 317/399] Generate context one or more times This is necessary for iterative (sample) groups so they don't share a context. --- src/spectator/dsl/examples.cr | 2 +- src/spectator/spec/builder.cr | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index ddfb51c..38c62f0 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -52,7 +52,7 @@ module Spectator::DSL ::Spectator::DSL::Builder.add_example( _spectator_example_name(\{{what}}), ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}, \{{block.end_line_number}}), - new.as(::Spectator::Context), + -> { new.as(::Spectator::Context) }, \%metadata ) do |example| example.with_context(\{{@type.name}}) do diff --git a/src/spectator/spec/builder.cr b/src/spectator/spec/builder.cr index f6ecee9..fe4eacd 100644 --- a/src/spectator/spec/builder.cr +++ b/src/spectator/spec/builder.cr @@ -109,7 +109,7 @@ module Spectator # # The *location* optionally defined where the example originates in source code. # - # The *context* is an instance of the context the test code should run in. + # The *context_builder* is a proc that creates a context the test code should run in. # See `Context` for more information. # # A set of *metadata* can be used for filtering and modifying example behavior. @@ -119,9 +119,10 @@ module Spectator # It will be yielded two arguments - the example created by this method, and the *context* argument. # The return value of the block is ignored. # It is expected that the test code runs when the block is called. - def add_example(name, location, context, metadata = Metadata.new, &block : Example -> _) + def add_example(name, location, context_builder, metadata = Metadata.new, &block : Example -> _) Log.trace { "Add example: #{name} @ #{location}; metadata: #{metadata}" } current_group.create_child do |group| + context = context_builder.call Example.new(context, block, name, location, group, metadata) end end From 989f53c6d6221c5de9ac5a0a6576bfb4cc3704c9 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 19 Jun 2021 11:57:56 -0600 Subject: [PATCH 318/399] Initial code for sample groups --- src/spectator/dsl/groups.cr | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 9eb0091..453b195 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -116,6 +116,36 @@ module Spectator::DSL define_example_group :xcontext, skip: "Temporarily skipped with xcontext" - # TODO: sample, random_sample + # Defines a new iterative example group. + # This type of group duplicates its contents for each element in *collection*. + # + # The first argument is the collection of elements to iterate over. + # + # Tags can be specified by adding symbols (keywords) after the first argument. + # Key-value pairs can also be specified. + # Any falsey items will remove a previously defined tag. + # + # TODO: Handle string interpolation in example and group names. + macro sample(collection, *tags, **metadata, &block) + {% raise "Cannot use 'sample' inside of a test block" if @def %} + + class Group%group < {{@type.id}} + _spectator_metadata(:metadata, :super, {{tags.splat(", ")}} {{metadata.double_splat}}) + + def self.%collection + {{collection}} + end + + ::Spectator::DSL::Builder.start_iterative_group( + %collection, + ::Spectator::Location.new({{block.filename}}, {{block.line_number}}), + metadata + ) + + {{block.body}} + + ::Spectator::DSL::Builder.end_group + end + end end end From 9b01771c678e285be8539e4da6ef7c054159883c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 19 Jun 2021 12:05:45 -0600 Subject: [PATCH 319/399] Fix missing setup log messages --- src/spectator.cr | 1 + 1 file changed, 1 insertion(+) diff --git a/src/spectator.cr b/src/spectator.cr index f565f21..638e9e6 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -11,6 +11,7 @@ module Spectator VERSION = {{ `shards version #{__DIR__}`.stringify.chomp }} # Logger for Spectator internals. + ::Log.setup_from_env Log = ::Log.for(self) # Flag indicating whether Spectator should automatically run tests. From 8d32984eba2cb32c63b1bcea8e74a0d0af638046 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 2 Jul 2021 18:11:40 -0600 Subject: [PATCH 320/399] Mark issue with scope of types as resolved https://github.com/icy-arctic-fox/spectator/issues/31 appears to be fixed in v0.10. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b22104..7b21354 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Fixed +- Fix resolution of types with the same name in nested scopes. [#31](https://github.com/icy-arctic-fox/spectator/issues/31) + ### Added - Hooks are yielded the current example as a block argument. - The `let` and `subject` blocks are yielded the current example as a block argument. From 6a01ab3531d6a5bf2218293a7e731fb0d10676aa Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 5 Jul 2021 11:32:45 -0600 Subject: [PATCH 321/399] Merge master into release/0.10 --- CHANGELOG.md | 3 ++- spec/issues/github_issue_28_spec.cr | 19 ++++++++++++++++ spec/issues/github_issue_29_spec.cr | 20 +++++++++++++++++ spec/issues/github_issue_30_spec.cr | 9 ++++++++ src/spectator/dsl/mocks.cr | 34 ++++++++++++++++++----------- src/spectator/mocks/no_arguments.cr | 21 ++++++++++++++++++ src/spectator/mocks/registry.cr | 8 +++++-- src/spectator/mocks/stubs.cr | 16 +++++++++++--- 8 files changed, 111 insertions(+), 19 deletions(-) create mode 100644 spec/issues/github_issue_28_spec.cr create mode 100644 spec/issues/github_issue_29_spec.cr create mode 100644 spec/issues/github_issue_30_spec.cr create mode 100644 src/spectator/mocks/no_arguments.cr diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b21354..ad65fb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -301,7 +301,8 @@ This has been changed so that it compiles and raises an error at runtime with a First version ready for public use. -[Unreleased]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.38...release%2F0.10 +[Unreleased]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.39...release%2F0.10 +[0.9.39]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.38...v0.9.39 [0.9.38]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.37...v0.9.38 [0.9.37]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.36...v0.9.37 [0.9.36]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.9.35...v0.9.36 diff --git a/spec/issues/github_issue_28_spec.cr b/spec/issues/github_issue_28_spec.cr new file mode 100644 index 0000000..6dd935c --- /dev/null +++ b/spec/issues/github_issue_28_spec.cr @@ -0,0 +1,19 @@ +require "../spec_helper" + +Spectator.describe "GitHub Issue #28" do + class Test + def foo + 42 + end + end + + mock Test do + stub foo + end + + it "matches method stubs with no_args" do + test = Test.new + expect(test).to receive(:foo).with(no_args).and_return(42) + test.foo + end +end diff --git a/spec/issues/github_issue_29_spec.cr b/spec/issues/github_issue_29_spec.cr new file mode 100644 index 0000000..6cedf58 --- /dev/null +++ b/spec/issues/github_issue_29_spec.cr @@ -0,0 +1,20 @@ +require "../spec_helper" + +Spectator.describe "GitHub Issue #29" do + class SomeClass + def goodbye + exit 0 + end + end + + mock SomeClass do + stub exit(code) + end + + describe SomeClass do + it "captures exit" do + expect(subject).to receive(:exit).with(0) + subject.goodbye + end + end +end diff --git a/spec/issues/github_issue_30_spec.cr b/spec/issues/github_issue_30_spec.cr new file mode 100644 index 0000000..2720803 --- /dev/null +++ b/spec/issues/github_issue_30_spec.cr @@ -0,0 +1,9 @@ +require "../spec_helper" + +Spectator.describe "GitHub Issue #30" do + let(dbl) { double(:foo) } + + it "supports block-less symbol doubles" do + expect(dbl).to_not be_nil + end +end diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 7ee1ffc..7e75738 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -11,22 +11,26 @@ module Spectator::DSL type_name = "Double#{safe_name}".id %} - {% if block.is_a?(Nop) %} - create_double({{type_name}}, {{name}}, {{stubs.double_splat}}) - {% else %} + {% if block %} define_double({{type_name}}, {{name}}, {{stubs.double_splat}}) {{block}} + {% else %} + create_double({{type_name}}, {{name}}, {{stubs.double_splat}}) {% end %} {% end %} end macro create_double(type_name, name, **stubs) - {% type_name.resolve? || raise("Could not find a double labeled #{name}") %} - - {{type_name}}.new.tap do |%double| - {% for name, value in stubs %} - allow(%double).to receive({{name.id}}).and_return({{value}}) - {% end %} - end + {% if type_name.resolve? %} + {{type_name}}.new.tap do |%double| + {% for name, value in stubs %} + allow(%double).to receive({{name.id}}).and_return({{value}}) + {% end %} + end + {% elsif @def %} + anonymous_double({{name ? name.stringify : "Anonymous"}}, {{stubs.double_splat}}) + {% else %} + {% raise "Block required for double definition" %} + {% end %} end macro define_double(type_name, name, **stubs, &block) @@ -156,10 +160,10 @@ module Spectator::DSL macro receive(method_name, _source_file = __FILE__, _source_line = __LINE__, &block) %location = ::Spectator::Location.new({{_source_file}}, {{_source_line}}) - {% if block.is_a?(Nop) %} - ::Spectator::Mocks::NilMethodStub.new({{method_name.id.symbolize}}, %location) - {% else %} + {% if block %} ::Spectator::Mocks::ProcMethodStub.create({{method_name.id.symbolize}}, %location) { {{block.body}} } + {% else %} + ::Spectator::Mocks::NilMethodStub.new({{method_name.id.symbolize}}, %location) {% end %} end @@ -171,5 +175,9 @@ module Spectator::DSL {% end %} %stubs end + + def no_args + ::Spectator::Mocks::NoArguments.new + end end end diff --git a/src/spectator/mocks/no_arguments.cr b/src/spectator/mocks/no_arguments.cr new file mode 100644 index 0000000..d99f2bf --- /dev/null +++ b/src/spectator/mocks/no_arguments.cr @@ -0,0 +1,21 @@ +require "./arguments" + +module Spectator::Mocks + class NoArguments < Arguments + def args + Tuple.new + end + + def opts + NamedTuple.new + end + + def ===(other : Arguments) : Bool + other.args.empty? && other.opts.empty? + end + + def ===(other) : Bool + false + end + end +end diff --git a/src/spectator/mocks/registry.cr b/src/spectator/mocks/registry.cr index 545a70c..3b53e5b 100644 --- a/src/spectator/mocks/registry.cr +++ b/src/spectator/mocks/registry.cr @@ -54,11 +54,15 @@ module Spectator::Mocks end def expect(object, stub : MethodStub) : Nil - fetch_instance(object).expected.add(stub) + entry = fetch_instance(object) + entry.expected.add(stub) + entry.stubs.unshift(stub) end def expect(type : T.class, stub : MethodStub) : Nil forall T - fetch_type(type).expected.add(stub) + entry = fetch_type(type) + entry.expected.add(stub) + entry.stubs.unshift(stub) end private def fetch_instance(object) diff --git a/src/spectator/mocks/stubs.cr b/src/spectator/mocks/stubs.cr index 8783057..4e7cd10 100644 --- a/src/spectator/mocks/stubs.cr +++ b/src/spectator/mocks/stubs.cr @@ -59,8 +59,10 @@ module Spectator::Mocks original = if (name == :new.id && receiver == "self.".id) || (t.superclass && t.superclass.has_method?(name) && !t.overrides?(t.superclass, name)) :super - else + elsif t.has_method?(name) :previous_def + else + "::#{name}" end.id %} @@ -80,7 +82,11 @@ module Spectator::Mocks %call = ::Spectator::Mocks::MethodCall.new({{name.symbolize}}, %args) %harness.mocks.record_call(self, %call) if (%stub = %harness.mocks.find_stub(self, %call)) - return %stub.call!(%args) { {{original}} } + if typeof({{original}}) == NoReturn + return %stub.call!(%args) { nil } + else + return %stub.call!(%args) { {{original}} } + end end {% if body && !body.is_a?(Nop) || return_type.is_a?(ArrayLiteral) %} @@ -99,7 +105,11 @@ module Spectator::Mocks %call = ::Spectator::Mocks::MethodCall.new({{name.symbolize}}, %args) %harness.mocks.record_call(self, %call) if (%stub = %harness.mocks.find_stub(self, %call)) - return %stub.call!(%args) { {{original}} { |*%ya| yield *%ya } } + if typeof({{original}}) == NoReturn + return %stub.call!(%args) { nil } + else + return %stub.call!(%args) { {{original}} { |*%ya| yield *%ya } } + end end {% if body && !body.is_a?(Nop) || return_type.is_a?(ArrayLiteral) %} From 3e4079d408442d77ed0cf3795a96c5bfcef6da09 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 5 Jul 2021 11:49:16 -0600 Subject: [PATCH 322/399] Remove Spec namespace --- src/spectator/dsl/builder.cr | 4 +- src/spectator/includes.cr | 3 + src/spectator/runner.cr | 98 +++++++++++++ src/spectator/runner_events.cr | 93 ++++++++++++ src/spectator/spec.cr | 2 +- src/spectator/spec/builder.cr | 255 --------------------------------- src/spectator/spec/events.cr | 92 ------------ src/spectator/spec/runner.cr | 98 ------------- src/spectator/spec_builder.cr | 253 ++++++++++++++++++++++++++++++++ 9 files changed, 450 insertions(+), 448 deletions(-) create mode 100644 src/spectator/runner.cr create mode 100644 src/spectator/runner_events.cr delete mode 100644 src/spectator/spec/builder.cr delete mode 100644 src/spectator/spec/events.cr delete mode 100644 src/spectator/spec/runner.cr create mode 100644 src/spectator/spec_builder.cr diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr index 9ebedaf..e0b724d 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -1,7 +1,7 @@ require "../example_group_hook" require "../example_hook" require "../example_procsy_hook" -require "../spec/builder" +require "../spec_builder" module Spectator::DSL # Incrementally builds up a test spec from the DSL. @@ -10,7 +10,7 @@ module Spectator::DSL extend self # Underlying spec builder. - @@builder = Spec::Builder.new + @@builder = SpecBuilder.new # Defines a new example group and pushes it onto the group stack. # Examples and groups defined after calling this method will be nested under the new group. diff --git a/src/spectator/includes.cr b/src/spectator/includes.cr index c611475..7f87649 100644 --- a/src/spectator/includes.cr +++ b/src/spectator/includes.cr @@ -47,6 +47,9 @@ require "./pending_result" require "./profile" require "./report" require "./result" +require "./runner_events" +require "./runner" +require "./spec_builder" require "./spec" require "./test_context" require "./value" diff --git a/src/spectator/runner.cr b/src/spectator/runner.cr new file mode 100644 index 0000000..49b6065 --- /dev/null +++ b/src/spectator/runner.cr @@ -0,0 +1,98 @@ +require "./example" +require "./formatting/formatter" +require "./profile" +require "./report" +require "./run_flags" +require "./runner_events" + +module Spectator + # Logic for executing examples and collecting results. + struct Runner + include RunnerEvents + + # Formatter to send events to. + private getter formatter : Formatting::Formatter + + # Creates the runner. + # The collection of *examples* should be pre-filtered and shuffled. + # This runner will run each example in the order provided. + # The *formatter* will be called for various events. + def initialize(@examples : Array(Example), @formatter : Formatting::Formatter, + @run_flags = RunFlags::None, @random_seed : UInt64? = nil) + end + + # Runs the spec. + # This will run the provided examples + # and invoke the reporters to communicate results. + # True will be returned if the spec ran successfully, + # or false if there was at least one failure. + def run : Bool + start + elapsed = Time.measure { run_examples } + stop + + report = Report.generate(@examples, elapsed, @random_seed) + profile = Profile.generate(@examples) if @run_flags.profile? && report.counts.run > 0 + summarize(report, profile) + + report.counts.fail.zero? + ensure + close + end + + # Attempts to run all examples. + # Returns a list of examples that ran. + private def run_examples + @examples.each do |example| + result = run_example(example) + + # Bail out if the example failed + # and configured to stop after the first failure. + break fail_fast if fail_fast? && result.fail? + end + end + + # Runs a single example and returns the result. + # The formatter is given the example and result information. + private def run_example(example) + example_started(example) + result = if dry_run? + # TODO: Pending examples return a pending result instead of pass in RSpec dry-run. + dry_run_result + else + example.run + end + example_finished(example) + result + end + + # Creates a fake result. + private def dry_run_result + expectations = [] of Expectation + PassResult.new(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 + + # Indicates whether examples should be simulated, but not run. + private def dry_run? + @run_flags.dry_run? + end + + # Indicates whether test execution should stop after the first failure. + private def fail_fast? + @run_flags.fail_fast? + end + + private def fail_fast : Nil + end + + # Number of examples configured to run. + private def example_count + @examples.size + end + end +end diff --git a/src/spectator/runner_events.cr b/src/spectator/runner_events.cr new file mode 100644 index 0000000..8f4e97e --- /dev/null +++ b/src/spectator/runner_events.cr @@ -0,0 +1,93 @@ +require "./formatting/formatter" +require "./formatting/notifications" + +module Spectator + # Mix-in for announcing events from a `Runner`. + # All events invoke their corresponding method on the formatter. + module RunnerEvents + # Triggers the 'start' event. + # See `Formatting::Formatter#start` + private def start + notification = Formatting::StartNotification.new(example_count) + formatter.start(notification) + end + + # Triggers the 'example started' event. + # Must be passed the *example* about to run. + # See `Formatting::Formatter#example_started` + private def example_started(example) + notification = Formatting::ExampleNotification.new(example) + formatter.example_started(notification) + end + + # Triggers the 'example started' event. + # Also triggers the example result event corresponding to the example's outcome. + # Must be passed the completed *example*. + # See `Formatting::Formatter#example_finished` + private def example_finished(example) + notification = Formatting::ExampleNotification.new(example) + visitor = ResultVisitor.new(formatter, notification) + formatter.example_finished(notification) + example.result.accept(visitor) + end + + # Triggers the 'stop' event. + # See `Formatting::Formatter#stop` + private def stop + formatter.stop + end + + # Triggers the 'dump' events. + private def summarize(report, profile) + formatter.start_dump + + notification = Formatting::ExampleSummaryNotification.new(report.pending) + formatter.dump_pending(notification) + + notification = Formatting::ExampleSummaryNotification.new(report.failures) + formatter.dump_failures(notification) + + if profile + notification = Formatting::ProfileNotification.new(profile) + formatter.dump_profile(notification) + end + + notification = Formatting::SummaryNotification.new(report) + formatter.dump_summary(notification) + end + + # Triggers the 'close' event. + # See `Formatting::Formatter#close` + private def close + formatter.close + end + + # Provides methods for the various result types. + private struct ResultVisitor + # Creates the visitor. + # Requires the *formatter* to notify and the *notification* to send it. + def initialize(@formatter : Formatting::Formatter, @notification : Formatting::ExampleNotification) + end + + # Invokes the example passed method. + def pass(_result) + @formatter.example_passed(@notification) + end + + # Invokes the example failed method. + def fail(_result) + @formatter.example_failed(@notification) + end + + # Invokes the example error method. + def error(_result) + @formatter.example_error(@notification) + end + + # Invokes the example pending method. + def pending(_result) + @formatter.example_pending(@notification) + end + end + end +end diff --git a/src/spectator/spec.cr b/src/spectator/spec.cr index a3894f4..9aec3a0 100644 --- a/src/spectator/spec.cr +++ b/src/spectator/spec.cr @@ -1,6 +1,6 @@ require "./config" require "./example_group" -require "./spec/*" +require "./runner" module Spectator # Contains examples to be tested and configuration for running them. diff --git a/src/spectator/spec/builder.cr b/src/spectator/spec/builder.cr deleted file mode 100644 index fe4eacd..0000000 --- a/src/spectator/spec/builder.cr +++ /dev/null @@ -1,255 +0,0 @@ -require "../config" -require "../example" -require "../example_context_method" -require "../example_group" -require "../iterative_example_group" -require "../spec" -require "../metadata" - -module Spectator - class Spec - # Progressively builds a test spec. - # - # A stack is used to track the current example group. - # Adding an example or group will nest it under the group at the top of the stack. - class Builder - Log = ::Spectator::Log.for(self) - - # Stack tracking the current group. - # The bottom of the stack (first element) is the root group. - # The root group should never be removed. - # The top of the stack (last element) is the current group. - # New examples should be added to the current group. - @group_stack : Deque(ExampleGroup) - - # Configuration for the spec. - @config : Config? - - # Creates a new spec builder. - # A root group is pushed onto the group stack. - def initialize - root_group = ExampleGroup.new - @group_stack = Deque(ExampleGroup).new - @group_stack.push(root_group) - end - - # Constructs the test spec. - # Returns the spec instance. - # - # Raises an error if there were not symmetrical calls to `#start_group` and `#end_group`. - # This would indicate a logical error somewhere in Spectator or an extension of it. - def build : Spec - raise "Mismatched start and end groups" unless root? - - Spec.new(root_group, config) - end - - # Defines a new example group and pushes it onto the group stack. - # Examples and groups defined after calling this method will be nested under the new group. - # The group will be finished and popped off the stack when `#end_example` is called. - # - # The *name* is the name or brief description of the group. - # This should be a symbol when describing a type - the type name is represented as a symbol. - # Otherwise, a string should be used. - # - # The *location* optionally defined where the group originates in source code. - # - # A set of *metadata* can be used for filtering and modifying example behavior. - # For instance, adding a "pending" tag will mark tests as pending and skip execution. - # - # The newly created group is returned. - # It shouldn't be used outside of this class until a matching `#end_group` is called. - def start_group(name, location = nil, metadata = Metadata.new) : ExampleGroup - Log.trace { "Start group: #{name.inspect} @ #{location}; metadata: #{metadata}" } - ExampleGroup.new(name, location, current_group, metadata).tap do |group| - @group_stack << group - end - end - - # Defines a new iterative example group and pushes it onto the group stack. - # Examples and groups defined after calling this method will be nested under the new group. - # The group will be finished and popped off the stack when `#end_example` is called. - # - # The *collection* is the set of items to iterate over. - # Child nodes in this group will be executed once for every item in the collection. - # - # The *location* optionally defined where the group originates in source code. - # - # A set of *metadata* can be used for filtering and modifying example behavior. - # For instance, adding a "pending" tag will mark tests as pending and skip execution. - # - # The newly created group is returned. - # It shouldn't be used outside of this class until a matching `#end_group` is called. - def start_iterative_group(collection, location = nil, metadata = Metadata.new) : ExampleGroup - Log.trace { "Start iterative group: #{typeof(collection)} @ #{location}; metadata: #{metadata}" } - IterativeExampleGroup.new(collection, location, current_group, metadata).tap do |group| - @group_stack << group - end - end - - # Completes a previously defined example group and pops it off the group stack. - # Be sure to call `#start_group` and `#end_group` symmetically. - # - # The completed group will be returned. - # At this point, it is safe to use the group. - # All of its examples and sub-groups have been populated. - def end_group : ExampleGroup - Log.trace { "End group: #{current_group}" } - raise "Can't pop root group" if root? - - @group_stack.pop - end - - # Defines a new example. - # The example is added to the group currently on the top of the stack. - # - # The *name* is the name or brief description of the example. - # This should be a string or nil. - # When nil, the example's name will be populated by the first expectation run inside of the test code. - # - # The *location* optionally defined where the example originates in source code. - # - # The *context_builder* is a proc that creates a context the test code should run in. - # See `Context` for more information. - # - # A set of *metadata* can be used for filtering and modifying example behavior. - # For instance, adding a "pending" tag will mark the test as pending and skip execution. - # - # A block must be provided. - # It will be yielded two arguments - the example created by this method, and the *context* argument. - # The return value of the block is ignored. - # It is expected that the test code runs when the block is called. - def add_example(name, location, context_builder, metadata = Metadata.new, &block : Example -> _) - Log.trace { "Add example: #{name} @ #{location}; metadata: #{metadata}" } - current_group.create_child do |group| - context = context_builder.call - Example.new(context, block, name, location, group, metadata) - end - end - - # Defines a new pending example. - # The example is added to the group currently on the top of the stack. - # - # The *name* is the name or brief description of the example. - # This should be a string or nil. - # When nil, the example's name will be an anonymous example reference. - # - # The *location* optionally defined where the example originates in source code. - # - # A set of *metadata* can be used for filtering and modifying example behavior. - # For instance, adding a "pending" tag will mark the test as pending and skip execution. - # A default *reason* can be given in case the user didn't provide one. - # - # The newly created example is returned. - def add_pending_example(name, location, metadata = Metadata.new, reason = nil) : Example - Log.trace { "Add pending example: #{name} @ #{location}; metadata: #{metadata}" } - current_group.create_child do |group| - Example.pending(name, location, group, metadata, reason) - end - end - - # Attaches a hook to be invoked before any and all examples in the current group. - def before_all(hook) - Log.trace { "Add before_all hook #{hook}" } - current_group.add_before_all_hook(hook) - end - - # Defines a block of code to execute before any and all examples in the current group. - def before_all(&block) - Log.trace { "Add before_all hook" } - current_group.before_all(&block) - end - - # Attaches a hook to be invoked before every example in the current group. - # The current example is provided as a block argument. - def before_each(hook) - Log.trace { "Add before_each hook #{hook}" } - current_group.add_before_each_hook(hook) - end - - # Defines a block of code to execute before every example in the current group. - # The current example is provided as a block argument. - def before_each(&block : Example -> _) - Log.trace { "Add before_each hook block" } - current_group.before_each(&block) - end - - # Attaches a hook to be invoked after any and all examples in the current group. - def after_all(hook) - Log.trace { "Add after_all hook #{hook}" } - current_group.add_after_all_hook(hook) - end - - # Defines a block of code to execute after any and all examples in the current group. - def after_all(&block) - Log.trace { "Add after_all hook" } - current_group.after_all(&block) - end - - # Attaches a hook to be invoked after every example in the current group. - # The current example is provided as a block argument. - def after_each(hook) - Log.trace { "Add after_each hook #{hook}" } - current_group.add_after_each_hook(hook) - end - - # Defines a block of code to execute after every example in the current group. - # The current example is provided as a block argument. - def after_each(&block : Example -> _) - Log.trace { "Add after_each hook" } - current_group.after_each(&block) - end - - # Attaches a hook to be invoked around every example in the current group. - # The current example in procsy form is provided as a block argument. - def around_each(hook) - Log.trace { "Add around_each hook #{hook}" } - current_group.add_around_each_hook(hook) - end - - # Defines a block of code to execute around every example in the current group. - # The current example in procsy form is provided as a block argument. - def around_each(&block : Example -> _) - Log.trace { "Add around_each hook" } - current_group.around_each(&block) - end - - # Builds the configuration to use for the spec. - # A `Config::Builder` is yielded to the block provided to this method. - # That builder will be used to create the configuration. - def configure(& : Config::Builder -> _) : Nil - builder = Config::Builder.new - yield builder - @config = builder.build - end - - # Sets the configuration of the spec. - # This configuration controls how examples run. - def config=(config) - @config = config - end - - # Checks if the current group is the root group. - private def root? - @group_stack.size == 1 - end - - # Retrieves the root group. - private def root_group - @group_stack.first - end - - # Retrieves the current group, which is at the top of the stack. - # This is the group that new examples should be added to. - private def current_group - @group_stack.last - end - - # Retrieves the configuration. - # If one wasn't previously set, a default configuration is used. - private def config : Config - @config || Config.default - end - end - end -end diff --git a/src/spectator/spec/events.cr b/src/spectator/spec/events.cr deleted file mode 100644 index d6ddb8f..0000000 --- a/src/spectator/spec/events.cr +++ /dev/null @@ -1,92 +0,0 @@ -module Spectator - class Spec - # Mix-in for announcing events from a `Runner`. - # All events invoke their corresponding method on the formatter. - module Events - # Triggers the 'start' event. - # See `Formatting::Formatter#start` - private def start - notification = Formatting::StartNotification.new(example_count) - formatter.start(notification) - end - - # Triggers the 'example started' event. - # Must be passed the *example* about to run. - # See `Formatting::Formatter#example_started` - private def example_started(example) - notification = Formatting::ExampleNotification.new(example) - formatter.example_started(notification) - end - - # Triggers the 'example started' event. - # Also triggers the example result event corresponding to the example's outcome. - # Must be passed the completed *example*. - # See `Formatting::Formatter#example_finished` - private def example_finished(example) - notification = Formatting::ExampleNotification.new(example) - visitor = ResultVisitor.new(formatter, notification) - formatter.example_finished(notification) - example.result.accept(visitor) - end - - # Triggers the 'stop' event. - # See `Formatting::Formatter#stop` - private def stop - formatter.stop - end - - # Triggers the 'dump' events. - private def summarize(report, profile) - formatter.start_dump - - notification = Formatting::ExampleSummaryNotification.new(report.pending) - formatter.dump_pending(notification) - - notification = Formatting::ExampleSummaryNotification.new(report.failures) - formatter.dump_failures(notification) - - if profile - notification = Formatting::ProfileNotification.new(profile) - formatter.dump_profile(notification) - end - - notification = Formatting::SummaryNotification.new(report) - formatter.dump_summary(notification) - end - - # Triggers the 'close' event. - # See `Formatting::Formatter#close` - private def close - formatter.close - end - - # Provides methods for the various result types. - private struct ResultVisitor - # Creates the visitor. - # Requires the *formatter* to notify and the *notification* to send it. - def initialize(@formatter : Formatting::Formatter, @notification : Formatting::ExampleNotification) - end - - # Invokes the example passed method. - def pass(_result) - @formatter.example_passed(@notification) - end - - # Invokes the example failed method. - def fail(_result) - @formatter.example_failed(@notification) - end - - # Invokes the example error method. - def error(_result) - @formatter.example_error(@notification) - end - - # Invokes the example pending method. - def pending(_result) - @formatter.example_pending(@notification) - end - end - end - end -end diff --git a/src/spectator/spec/runner.cr b/src/spectator/spec/runner.cr deleted file mode 100644 index 81e86c2..0000000 --- a/src/spectator/spec/runner.cr +++ /dev/null @@ -1,98 +0,0 @@ -require "../example" -require "../report" -require "../run_flags" -require "./events" - -module Spectator - class Spec - # Logic for executing examples and collecting results. - private struct Runner - include Events - - # Formatter to send events to. - private getter formatter : Formatting::Formatter - - # Creates the runner. - # The collection of *examples* should be pre-filtered and shuffled. - # This runner will run each example in the order provided. - # The *formatter* will be called for various events. - def initialize(@examples : Array(Example), @formatter : Formatting::Formatter, - @run_flags = RunFlags::None, @random_seed : UInt64? = nil) - end - - # Runs the spec. - # This will run the provided examples - # and invoke the reporters to communicate results. - # True will be returned if the spec ran successfully, - # or false if there was at least one failure. - def run : Bool - start - elapsed = Time.measure { run_examples } - stop - - report = Report.generate(@examples, elapsed, @random_seed) - profile = Profile.generate(@examples) if @run_flags.profile? && report.counts.run > 0 - summarize(report, profile) - - report.counts.fail.zero? - ensure - close - end - - # Attempts to run all examples. - # Returns a list of examples that ran. - private def run_examples - @examples.each do |example| - result = run_example(example) - - # Bail out if the example failed - # and configured to stop after the first failure. - break fail_fast if fail_fast? && result.fail? - end - end - - # Runs a single example and returns the result. - # The formatter is given the example and result information. - private def run_example(example) - example_started(example) - result = if dry_run? - # TODO: Pending examples return a pending result instead of pass in RSpec dry-run. - dry_run_result - else - example.run - end - example_finished(example) - result - end - - # Creates a fake result. - private def dry_run_result - expectations = [] of Expectation - PassResult.new(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 - - # Indicates whether examples should be simulated, but not run. - private def dry_run? - @run_flags.dry_run? - end - - # Indicates whether test execution should stop after the first failure. - private def fail_fast? - @run_flags.fail_fast? - end - - private def fail_fast : Nil - end - - # Number of examples configured to run. - private def example_count - @examples.size - end - end - end -end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr new file mode 100644 index 0000000..76797e2 --- /dev/null +++ b/src/spectator/spec_builder.cr @@ -0,0 +1,253 @@ +require "./config" +require "./example" +require "./example_context_method" +require "./example_group" +require "./iterative_example_group" +require "./spec" +require "./metadata" + +module Spectator + # Progressively builds a test spec. + # + # A stack is used to track the current example group. + # Adding an example or group will nest it under the group at the top of the stack. + class SpecBuilder + Log = ::Spectator::Log.for(self) + + # Stack tracking the current group. + # The bottom of the stack (first element) is the root group. + # The root group should never be removed. + # The top of the stack (last element) is the current group. + # New examples should be added to the current group. + @group_stack : Deque(ExampleGroup) + + # Configuration for the spec. + @config : Config? + + # Creates a new spec builder. + # A root group is pushed onto the group stack. + def initialize + root_group = ExampleGroup.new + @group_stack = Deque(ExampleGroup).new + @group_stack.push(root_group) + end + + # Constructs the test spec. + # Returns the spec instance. + # + # Raises an error if there were not symmetrical calls to `#start_group` and `#end_group`. + # This would indicate a logical error somewhere in Spectator or an extension of it. + def build : Spec + raise "Mismatched start and end groups" unless root? + + Spec.new(root_group, config) + end + + # Defines a new example group and pushes it onto the group stack. + # Examples and groups defined after calling this method will be nested under the new group. + # The group will be finished and popped off the stack when `#end_example` is called. + # + # The *name* is the name or brief description of the group. + # This should be a symbol when describing a type - the type name is represented as a symbol. + # Otherwise, a string should be used. + # + # The *location* optionally defined where the group originates in source code. + # + # A set of *metadata* can be used for filtering and modifying example behavior. + # For instance, adding a "pending" tag will mark tests as pending and skip execution. + # + # The newly created group is returned. + # It shouldn't be used outside of this class until a matching `#end_group` is called. + def start_group(name, location = nil, metadata = Metadata.new) : ExampleGroup + Log.trace { "Start group: #{name.inspect} @ #{location}; metadata: #{metadata}" } + ExampleGroup.new(name, location, current_group, metadata).tap do |group| + @group_stack << group + end + end + + # Defines a new iterative example group and pushes it onto the group stack. + # Examples and groups defined after calling this method will be nested under the new group. + # The group will be finished and popped off the stack when `#end_example` is called. + # + # The *collection* is the set of items to iterate over. + # Child nodes in this group will be executed once for every item in the collection. + # + # The *location* optionally defined where the group originates in source code. + # + # A set of *metadata* can be used for filtering and modifying example behavior. + # For instance, adding a "pending" tag will mark tests as pending and skip execution. + # + # The newly created group is returned. + # It shouldn't be used outside of this class until a matching `#end_group` is called. + def start_iterative_group(collection, location = nil, metadata = Metadata.new) : ExampleGroup + Log.trace { "Start iterative group: #{typeof(collection)} @ #{location}; metadata: #{metadata}" } + IterativeExampleGroup.new(collection, location, current_group, metadata).tap do |group| + @group_stack << group + end + end + + # Completes a previously defined example group and pops it off the group stack. + # Be sure to call `#start_group` and `#end_group` symmetically. + # + # The completed group will be returned. + # At this point, it is safe to use the group. + # All of its examples and sub-groups have been populated. + def end_group : ExampleGroup + Log.trace { "End group: #{current_group}" } + raise "Can't pop root group" if root? + + @group_stack.pop + end + + # Defines a new example. + # The example is added to the group currently on the top of the stack. + # + # The *name* is the name or brief description of the example. + # This should be a string or nil. + # When nil, the example's name will be populated by the first expectation run inside of the test code. + # + # The *location* optionally defined where the example originates in source code. + # + # The *context_builder* is a proc that creates a context the test code should run in. + # See `Context` for more information. + # + # A set of *metadata* can be used for filtering and modifying example behavior. + # For instance, adding a "pending" tag will mark the test as pending and skip execution. + # + # A block must be provided. + # It will be yielded two arguments - the example created by this method, and the *context* argument. + # The return value of the block is ignored. + # It is expected that the test code runs when the block is called. + def add_example(name, location, context_builder, metadata = Metadata.new, &block : Example -> _) + Log.trace { "Add example: #{name} @ #{location}; metadata: #{metadata}" } + current_group.create_child do |group| + context = context_builder.call + Example.new(context, block, name, location, group, metadata) + end + end + + # Defines a new pending example. + # The example is added to the group currently on the top of the stack. + # + # The *name* is the name or brief description of the example. + # This should be a string or nil. + # When nil, the example's name will be an anonymous example reference. + # + # The *location* optionally defined where the example originates in source code. + # + # A set of *metadata* can be used for filtering and modifying example behavior. + # For instance, adding a "pending" tag will mark the test as pending and skip execution. + # A default *reason* can be given in case the user didn't provide one. + # + # The newly created example is returned. + def add_pending_example(name, location, metadata = Metadata.new, reason = nil) : Example + Log.trace { "Add pending example: #{name} @ #{location}; metadata: #{metadata}" } + current_group.create_child do |group| + Example.pending(name, location, group, metadata, reason) + end + end + + # Attaches a hook to be invoked before any and all examples in the current group. + def before_all(hook) + Log.trace { "Add before_all hook #{hook}" } + current_group.add_before_all_hook(hook) + end + + # Defines a block of code to execute before any and all examples in the current group. + def before_all(&block) + Log.trace { "Add before_all hook" } + current_group.before_all(&block) + end + + # Attaches a hook to be invoked before every example in the current group. + # The current example is provided as a block argument. + def before_each(hook) + Log.trace { "Add before_each hook #{hook}" } + current_group.add_before_each_hook(hook) + end + + # Defines a block of code to execute before every example in the current group. + # The current example is provided as a block argument. + def before_each(&block : Example -> _) + Log.trace { "Add before_each hook block" } + current_group.before_each(&block) + end + + # Attaches a hook to be invoked after any and all examples in the current group. + def after_all(hook) + Log.trace { "Add after_all hook #{hook}" } + current_group.add_after_all_hook(hook) + end + + # Defines a block of code to execute after any and all examples in the current group. + def after_all(&block) + Log.trace { "Add after_all hook" } + current_group.after_all(&block) + end + + # Attaches a hook to be invoked after every example in the current group. + # The current example is provided as a block argument. + def after_each(hook) + Log.trace { "Add after_each hook #{hook}" } + current_group.add_after_each_hook(hook) + end + + # Defines a block of code to execute after every example in the current group. + # The current example is provided as a block argument. + def after_each(&block : Example -> _) + Log.trace { "Add after_each hook" } + current_group.after_each(&block) + end + + # Attaches a hook to be invoked around every example in the current group. + # The current example in procsy form is provided as a block argument. + def around_each(hook) + Log.trace { "Add around_each hook #{hook}" } + current_group.add_around_each_hook(hook) + end + + # Defines a block of code to execute around every example in the current group. + # The current example in procsy form is provided as a block argument. + def around_each(&block : Example -> _) + Log.trace { "Add around_each hook" } + current_group.around_each(&block) + end + + # Builds the configuration to use for the spec. + # A `Config::Builder` is yielded to the block provided to this method. + # That builder will be used to create the configuration. + def configure(& : Config::Builder -> _) : Nil + builder = Config::Builder.new + yield builder + @config = builder.build + end + + # Sets the configuration of the spec. + # This configuration controls how examples run. + def config=(config) + @config = config + end + + # Checks if the current group is the root group. + private def root? + @group_stack.size == 1 + end + + # Retrieves the root group. + private def root_group + @group_stack.first + end + + # Retrieves the current group, which is at the top of the stack. + # This is the group that new examples should be added to. + private def current_group + @group_stack.last + end + + # Retrieves the configuration. + # If one wasn't previously set, a default configuration is used. + private def config : Config + @config || Config.default + end + end +end From 7081c168a5f70197cae35ee3bd6b2e802af5795d Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 5 Jul 2021 22:36:37 -0600 Subject: [PATCH 323/399] Missing comma --- src/spectator/formatting/components/totals.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/formatting/components/totals.cr b/src/spectator/formatting/components/totals.cr index 5af5c30..1e232b9 100644 --- a/src/spectator/formatting/components/totals.cr +++ b/src/spectator/formatting/components/totals.cr @@ -32,7 +32,7 @@ module Spectator::Formatting::Components # Writes the counts to the output. def to_s(io) - io << @examples << " examples, " << @failures << " failures " + io << @examples << " examples, " << @failures << " failures, " if @errors > 1 io << '(' << @errors << " errors), " From aee5897922d87f0c979d0852f741921397ced124 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 6 Jul 2021 23:32:51 -0600 Subject: [PATCH 324/399] Fix comma placement --- src/spectator/formatting/components/totals.cr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/spectator/formatting/components/totals.cr b/src/spectator/formatting/components/totals.cr index 1e232b9..6071e39 100644 --- a/src/spectator/formatting/components/totals.cr +++ b/src/spectator/formatting/components/totals.cr @@ -32,13 +32,13 @@ module Spectator::Formatting::Components # Writes the counts to the output. def to_s(io) - io << @examples << " examples, " << @failures << " failures, " + io << @examples << " examples, " << @failures << " failures" if @errors > 1 - io << '(' << @errors << " errors), " + io << " (" << @errors << " errors)" end - io << @pending << " pending" + io << ", " << @pending << " pending" end end end From ccedcdac420512a04a4aebd896575f6c82b6f0c7 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 10 Jul 2021 03:31:22 -0600 Subject: [PATCH 325/399] Use getter! macro --- src/spectator/expectation.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/expectation.cr b/src/spectator/expectation.cr index 1b19562..cc376b6 100644 --- a/src/spectator/expectation.cr +++ b/src/spectator/expectation.cr @@ -10,7 +10,7 @@ module Spectator # Location of the expectation in source code. # This can be nil if the location isn't capturable, # for instance using the *should* syntax or dynamically created expectations. - getter location : Location? + getter! location : Location # Indicates whether the expectation was met. def satisfied? From aa12cdf17c84c4aa20fd1e61b4e85805423c7857 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 10 Jul 2021 03:32:55 -0600 Subject: [PATCH 326/399] Introduce non-expectation error ExampleFailed Used by fail method. Still todo: Output from failed example is missing because there are no expectations. --- spec/spec_helper.cr | 2 +- src/spectator/dsl/expectations.cr | 2 +- src/spectator/example_failed.cr | 14 ++++++++++++++ src/spectator/expectation_failed.cr | 5 +++-- src/spectator/harness.cr | 3 ++- 5 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 src/spectator/example_failed.cr diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index c9176e6..6c9a806 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -6,7 +6,7 @@ macro it_fails(description = nil, &block) it {{description}} do expect do {{block.body}} - end.to raise_error(Spectator::ExpectationFailed) + end.to raise_error(Spectator::ExampleFailed) end end diff --git a/src/spectator/dsl/expectations.cr b/src/spectator/dsl/expectations.cr index 290c716..b8a18e4 100644 --- a/src/spectator/dsl/expectations.cr +++ b/src/spectator/dsl/expectations.cr @@ -12,7 +12,7 @@ module Spectator::DSL # Immediately fail the current test. # A reason can be specified with *message*. def fail(message = "Example failed", *, _file = __FILE__, _line = __LINE__) - raise ExpectationFailed.new(Location.new(_file, _line), message) + raise ExampleFailed.new(Location.new(_file, _line), message) end # Mark the current test as pending and immediately abort. diff --git a/src/spectator/example_failed.cr b/src/spectator/example_failed.cr new file mode 100644 index 0000000..2170604 --- /dev/null +++ b/src/spectator/example_failed.cr @@ -0,0 +1,14 @@ +require "./location" + +module Spectator + # Exception that indicates an example failed. + # When raised within a test, the test should abort. + class ExampleFailed < Exception + getter! location : Location + + # Creates the exception. + def initialize(@location : Location?, message : String? = nil, cause : Exception? = nil) + super(message, cause) + end + end +end diff --git a/src/spectator/expectation_failed.cr b/src/spectator/expectation_failed.cr index 67c6dea..2fe3941 100644 --- a/src/spectator/expectation_failed.cr +++ b/src/spectator/expectation_failed.cr @@ -1,15 +1,16 @@ +require "./example_failed" require "./expectation" module Spectator # Exception that indicates an expectation from a test failed. # When raised within a test, the test should abort. - class ExpectationFailed < Exception + class ExpectationFailed < ExampleFailed # Expectation that failed. getter expectation : Expectation # Creates the exception. def initialize(@expectation : Expectation, message : String? = nil, cause : Exception? = nil) - super(message, cause) + super(expectation.location?, message, cause) end end end diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 7a4df88..5877d56 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -1,4 +1,5 @@ require "./error_result" +require "./example_failed" require "./example_pending" require "./expectation" require "./mocks" @@ -118,7 +119,7 @@ module Spectator case error when nil PassResult.new(elapsed, @expectations) - when ExpectationFailed + when ExampleFailed FailResult.new(elapsed, error, @expectations) when ExamplePending PendingResult.new(error.message || PendingResult::DEFAULT_REASON, elapsed, @expectations) From c7a90b3e64cfc1195651ba60f2169a9a5aa82172 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 10:47:16 -0600 Subject: [PATCH 327/399] Shorten names and cleanup --- src/spectator/spec_builder.cr | 56 +++++++++++++++++------------------ 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 76797e2..ede07d6 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -19,7 +19,7 @@ module Spectator # The root group should never be removed. # The top of the stack (last element) is the current group. # New examples should be added to the current group. - @group_stack : Deque(ExampleGroup) + @stack : Deque(ExampleGroup) # Configuration for the spec. @config : Config? @@ -27,9 +27,9 @@ module Spectator # Creates a new spec builder. # A root group is pushed onto the group stack. def initialize - root_group = ExampleGroup.new - @group_stack = Deque(ExampleGroup).new - @group_stack.push(root_group) + root = ExampleGroup.new + @stack = Deque(ExampleGroup).new + @stack.push(root) end # Constructs the test spec. @@ -40,7 +40,7 @@ module Spectator def build : Spec raise "Mismatched start and end groups" unless root? - Spec.new(root_group, config) + Spec.new(root, config) end # Defines a new example group and pushes it onto the group stack. @@ -60,8 +60,8 @@ module Spectator # It shouldn't be used outside of this class until a matching `#end_group` is called. def start_group(name, location = nil, metadata = Metadata.new) : ExampleGroup Log.trace { "Start group: #{name.inspect} @ #{location}; metadata: #{metadata}" } - ExampleGroup.new(name, location, current_group, metadata).tap do |group| - @group_stack << group + ExampleGroup.new(name, location, current, metadata).tap do |group| + @stack.push(group) end end @@ -81,8 +81,8 @@ module Spectator # It shouldn't be used outside of this class until a matching `#end_group` is called. def start_iterative_group(collection, location = nil, metadata = Metadata.new) : ExampleGroup Log.trace { "Start iterative group: #{typeof(collection)} @ #{location}; metadata: #{metadata}" } - IterativeExampleGroup.new(collection, location, current_group, metadata).tap do |group| - @group_stack << group + IterativeExampleGroup.new(collection, location, current, metadata).tap do |group| + @stack.push(group) end end @@ -93,10 +93,10 @@ module Spectator # At this point, it is safe to use the group. # All of its examples and sub-groups have been populated. def end_group : ExampleGroup - Log.trace { "End group: #{current_group}" } + Log.trace { "End group: #{current}" } raise "Can't pop root group" if root? - @group_stack.pop + @stack.pop end # Defines a new example. @@ -120,7 +120,7 @@ module Spectator # It is expected that the test code runs when the block is called. def add_example(name, location, context_builder, metadata = Metadata.new, &block : Example -> _) Log.trace { "Add example: #{name} @ #{location}; metadata: #{metadata}" } - current_group.create_child do |group| + current.create_child do |group| context = context_builder.call Example.new(context, block, name, location, group, metadata) end @@ -142,7 +142,7 @@ module Spectator # The newly created example is returned. def add_pending_example(name, location, metadata = Metadata.new, reason = nil) : Example Log.trace { "Add pending example: #{name} @ #{location}; metadata: #{metadata}" } - current_group.create_child do |group| + current.create_child do |group| Example.pending(name, location, group, metadata, reason) end end @@ -150,67 +150,67 @@ module Spectator # Attaches a hook to be invoked before any and all examples in the current group. def before_all(hook) Log.trace { "Add before_all hook #{hook}" } - current_group.add_before_all_hook(hook) + current.add_before_all_hook(hook) end # Defines a block of code to execute before any and all examples in the current group. def before_all(&block) Log.trace { "Add before_all hook" } - current_group.before_all(&block) + current.before_all(&block) end # Attaches a hook to be invoked before every example in the current group. # The current example is provided as a block argument. def before_each(hook) Log.trace { "Add before_each hook #{hook}" } - current_group.add_before_each_hook(hook) + current.add_before_each_hook(hook) end # Defines a block of code to execute before every example in the current group. # The current example is provided as a block argument. def before_each(&block : Example -> _) Log.trace { "Add before_each hook block" } - current_group.before_each(&block) + current.before_each(&block) end # Attaches a hook to be invoked after any and all examples in the current group. def after_all(hook) Log.trace { "Add after_all hook #{hook}" } - current_group.add_after_all_hook(hook) + current.add_after_all_hook(hook) end # Defines a block of code to execute after any and all examples in the current group. def after_all(&block) Log.trace { "Add after_all hook" } - current_group.after_all(&block) + current.after_all(&block) end # Attaches a hook to be invoked after every example in the current group. # The current example is provided as a block argument. def after_each(hook) Log.trace { "Add after_each hook #{hook}" } - current_group.add_after_each_hook(hook) + current.add_after_each_hook(hook) end # Defines a block of code to execute after every example in the current group. # The current example is provided as a block argument. def after_each(&block : Example -> _) Log.trace { "Add after_each hook" } - current_group.after_each(&block) + current.after_each(&block) end # Attaches a hook to be invoked around every example in the current group. # The current example in procsy form is provided as a block argument. def around_each(hook) Log.trace { "Add around_each hook #{hook}" } - current_group.add_around_each_hook(hook) + current.add_around_each_hook(hook) end # Defines a block of code to execute around every example in the current group. # The current example in procsy form is provided as a block argument. def around_each(&block : Example -> _) Log.trace { "Add around_each hook" } - current_group.around_each(&block) + current.around_each(&block) end # Builds the configuration to use for the spec. @@ -230,18 +230,18 @@ module Spectator # Checks if the current group is the root group. private def root? - @group_stack.size == 1 + @stack.size == 1 end # Retrieves the root group. - private def root_group - @group_stack.first + private def root + @stack.first end # Retrieves the current group, which is at the top of the stack. # This is the group that new examples should be added to. - private def current_group - @group_stack.last + private def current + @stack.last end # Retrieves the configuration. From c24c2cb5e12f06f3f2c51752395a2d41e6399ae4 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 11:10:44 -0600 Subject: [PATCH 328/399] Quick implementation of node builders --- src/spectator/example_builder.cr | 14 ++++ src/spectator/example_group_builder.cr | 86 ++++++++++++++++++++++++ src/spectator/node_builder.cr | 5 ++ src/spectator/pending_example_builder.cr | 12 ++++ src/spectator/spec_builder.cr | 54 ++++++--------- 5 files changed, 137 insertions(+), 34 deletions(-) create mode 100644 src/spectator/example_builder.cr create mode 100644 src/spectator/example_group_builder.cr create mode 100644 src/spectator/node_builder.cr create mode 100644 src/spectator/pending_example_builder.cr diff --git a/src/spectator/example_builder.cr b/src/spectator/example_builder.cr new file mode 100644 index 0000000..eb6eec9 --- /dev/null +++ b/src/spectator/example_builder.cr @@ -0,0 +1,14 @@ +require "./node_builder" + +module Spectator + class ExampleBuilder < NodeBuilder + def initialize(@context_builder : -> Context, @entrypoint : Example ->, + @name : String? = nil, @location : Location? = nil, @metadata : Metadata = Metadata.new) + end + + def build(parent) + context = @context_builder.call + Example.new(context, @entrypoint, @name, @location, parent, @metadata) + end + end +end diff --git a/src/spectator/example_group_builder.cr b/src/spectator/example_group_builder.cr new file mode 100644 index 0000000..e140203 --- /dev/null +++ b/src/spectator/example_group_builder.cr @@ -0,0 +1,86 @@ +require "./node_builder" + +module Spectator + class ExampleGroupBuilder < NodeBuilder + @children = [] of NodeBuilder + @before_all_hooks = [] of ExampleGroupHook + @before_each_hooks = [] of ExampleHook + @after_all_hooks = [] of ExampleGroupHook + @after_each_hooks = [] of ExampleHook + @around_each_hooks = [] of ExampleProcsyHook + + def initialize(@name : Label = nil, @location : Location? = nil, @metadata : Metadata = Metadata.new) + end + + # Attaches a hook to be invoked before any and all examples in the current group. + def add_before_all_hook(hook) + @before_all_hooks << hook + end + + # Defines a block of code to execute before any and all examples in the current group. + def add_before_all_hook(&block) + @before_all_hooks << ExampleGroupHook.new(&block) + end + + # Attaches a hook to be invoked before every example in the current group. + # The current example is provided as a block argument. + def add_before_each_hook(hook) + @before_each_hooks << hook + end + + # Defines a block of code to execute before every example in the current group. + # The current example is provided as a block argument. + def add_before_each_hook(&block : Example -> _) + @before_each_hooks << ExampleHook.new(&block) + end + + # Attaches a hook to be invoked after any and all examples in the current group. + def add_after_all_hook(hook) + @after_all_hooks << hook + end + + # Defines a block of code to execute after any and all examples in the current group. + def add_after_all_hook(&block) + @after_all_hooks << ExampleGroupHook.new(&block) + end + + # Attaches a hook to be invoked after every example in the current group. + # The current example is provided as a block argument. + def add_after_each_hook(hook) + @after_each_hooks << hook + end + + # Defines a block of code to execute after every example in the current group. + # The current example is provided as a block argument. + def add_after_each_hook(&block : Example -> _) + @after_each_hooks << ExampleHook.new(&block) + end + + # Attaches a hook to be invoked around every example in the current group. + # The current example in procsy form is provided as a block argument. + def add_around_each_hook(hook) + @around_each_hooks << hook + end + + # Defines a block of code to execute around every example in the current group. + # The current example in procsy form is provided as a block argument. + def add_around_each_hook(&block : Example -> _) + @around_each_hooks << ExampleProcsyHook.new(label: "around_each", &block) + end + + def build(parent = nil) + ExampleGroup.new(@name, @location, parent, @metadata).tap do |group| + @before_all_hooks.each { |hook| group.add_before_all_hook(hook) } + @before_each_hooks.each { |hook| group.add_before_each_hook(hook) } + @after_all_hooks.each { |hook| group.add_after_all_hook(hook) } + @after_each_hooks.each { |hook| group.add_after_each_hook(hook) } + @around_each_hooks.each { |hook| group.add_around_each_hook(hook) } + @children.each(&.build(group)) + end + end + + def <<(builder) + @children << builder + end + end +end diff --git a/src/spectator/node_builder.cr b/src/spectator/node_builder.cr new file mode 100644 index 0000000..457f39f --- /dev/null +++ b/src/spectator/node_builder.cr @@ -0,0 +1,5 @@ +module Spectator + abstract class NodeBuilder + abstract def build(parent) + end +end diff --git a/src/spectator/pending_example_builder.cr b/src/spectator/pending_example_builder.cr new file mode 100644 index 0000000..40880b1 --- /dev/null +++ b/src/spectator/pending_example_builder.cr @@ -0,0 +1,12 @@ +require "./node_builder" + +module Spectator + class PendingExampleBuilder < NodeBuilder + def initialize(@name : String? = nil, @location : Location? = nil, @metadata : Metadata = Metadata.new) + end + + def build(parent) + Example.pending(@name, @location, parent, @metadata) + end + end +end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index ede07d6..8e7ea3b 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -1,8 +1,11 @@ require "./config" require "./example" +require "./example_builder" require "./example_context_method" require "./example_group" +require "./example_group_builder" require "./iterative_example_group" +require "./pending_example_builder" require "./spec" require "./metadata" @@ -19,7 +22,7 @@ module Spectator # The root group should never be removed. # The top of the stack (last element) is the current group. # New examples should be added to the current group. - @stack : Deque(ExampleGroup) + @stack : Deque(ExampleGroupBuilder) # Configuration for the spec. @config : Config? @@ -27,8 +30,8 @@ module Spectator # Creates a new spec builder. # A root group is pushed onto the group stack. def initialize - root = ExampleGroup.new - @stack = Deque(ExampleGroup).new + root = ExampleGroupBuilder.new + @stack = Deque(ExampleGroupBuilder).new @stack.push(root) end @@ -40,7 +43,7 @@ module Spectator def build : Spec raise "Mismatched start and end groups" unless root? - Spec.new(root, config) + Spec.new(root.build, config) end # Defines a new example group and pushes it onto the group stack. @@ -55,14 +58,11 @@ module Spectator # # A set of *metadata* can be used for filtering and modifying example behavior. # For instance, adding a "pending" tag will mark tests as pending and skip execution. - # - # The newly created group is returned. - # It shouldn't be used outside of this class until a matching `#end_group` is called. - def start_group(name, location = nil, metadata = Metadata.new) : ExampleGroup + def start_group(name, location = nil, metadata = Metadata.new) : Nil Log.trace { "Start group: #{name.inspect} @ #{location}; metadata: #{metadata}" } - ExampleGroup.new(name, location, current, metadata).tap do |group| - @stack.push(group) - end + builder = ExampleGroupBuilder.new(name, location, metadata) + current << builder + @stack.push(builder) end # Defines a new iterative example group and pushes it onto the group stack. @@ -76,23 +76,16 @@ module Spectator # # A set of *metadata* can be used for filtering and modifying example behavior. # For instance, adding a "pending" tag will mark tests as pending and skip execution. - # - # The newly created group is returned. - # It shouldn't be used outside of this class until a matching `#end_group` is called. - def start_iterative_group(collection, location = nil, metadata = Metadata.new) : ExampleGroup + def start_iterative_group(collection, location = nil, metadata = Metadata.new) : Nil Log.trace { "Start iterative group: #{typeof(collection)} @ #{location}; metadata: #{metadata}" } - IterativeExampleGroup.new(collection, location, current, metadata).tap do |group| - @stack.push(group) - end + builder = ExampleGroupBuilder.new(collection, location, metadata) # TODO: IterativeExampleGroupBuilder + current << builder + @stack.push(builder) end # Completes a previously defined example group and pops it off the group stack. # Be sure to call `#start_group` and `#end_group` symmetically. - # - # The completed group will be returned. - # At this point, it is safe to use the group. - # All of its examples and sub-groups have been populated. - def end_group : ExampleGroup + def end_group : Nil Log.trace { "End group: #{current}" } raise "Can't pop root group" if root? @@ -118,12 +111,9 @@ module Spectator # It will be yielded two arguments - the example created by this method, and the *context* argument. # The return value of the block is ignored. # It is expected that the test code runs when the block is called. - def add_example(name, location, context_builder, metadata = Metadata.new, &block : Example -> _) + def add_example(name, location, context_builder, metadata = Metadata.new, &block : Example -> _) : Nil Log.trace { "Add example: #{name} @ #{location}; metadata: #{metadata}" } - current.create_child do |group| - context = context_builder.call - Example.new(context, block, name, location, group, metadata) - end + current << ExampleBuilder.new(context_builder, block, name, location, metadata) end # Defines a new pending example. @@ -138,13 +128,9 @@ module Spectator # A set of *metadata* can be used for filtering and modifying example behavior. # For instance, adding a "pending" tag will mark the test as pending and skip execution. # A default *reason* can be given in case the user didn't provide one. - # - # The newly created example is returned. - def add_pending_example(name, location, metadata = Metadata.new, reason = nil) : Example + def add_pending_example(name, location, metadata = Metadata.new, reason = nil) : Nil Log.trace { "Add pending example: #{name} @ #{location}; metadata: #{metadata}" } - current.create_child do |group| - Example.pending(name, location, group, metadata, reason) - end + current << PendingExampleBuilder.new(name, location, metadata) end # Attaches a hook to be invoked before any and all examples in the current group. From c79cb62a61016caa0ee912952e8dfb7c4f7228b5 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 12:06:53 -0600 Subject: [PATCH 329/399] Quick implementation of iterative group builder --- src/spectator/example_group_iteration.cr | 10 +++++ src/spectator/iterative_example_group.cr | 44 ------------------- .../iterative_example_group_builder.cr | 26 +++++++++++ src/spectator/spec_builder.cr | 4 +- 4 files changed, 38 insertions(+), 46 deletions(-) create mode 100644 src/spectator/example_group_iteration.cr delete mode 100644 src/spectator/iterative_example_group.cr create mode 100644 src/spectator/iterative_example_group_builder.cr diff --git a/src/spectator/example_group_iteration.cr b/src/spectator/example_group_iteration.cr new file mode 100644 index 0000000..9247b9b --- /dev/null +++ b/src/spectator/example_group_iteration.cr @@ -0,0 +1,10 @@ +require "./example_group" + +module Spectator + class ExampleGroupIteration(T) < ExampleGroup + def initialize(@item : T, name : Label = nil, location : Location? = nil, + group : ExampleGroup? = nil, metadata : Metadata = Metadata.new) + super(name, location, group, metadata) + end + end +end diff --git a/src/spectator/iterative_example_group.cr b/src/spectator/iterative_example_group.cr deleted file mode 100644 index ca050b9..0000000 --- a/src/spectator/iterative_example_group.cr +++ /dev/null @@ -1,44 +0,0 @@ -require "./example_group" -require "./node" - -module Spectator - # Collection of examples and sub-groups executed multiple times. - # Each sub-node is executed once for each item in a given collection. - class IterativeExampleGroup(T) < ExampleGroup - # Creates the iterative example group. - # The *collection* is a list of items to iterative over each sub-node over. - # The *location* tracks where the group exists in source code. - # This group will be assigned to the parent *group* if it is provided. - # A set of *metadata* can be used for filtering and modifying example behavior. - def initialize(collection : Enumerable(T), location : Location? = nil, - group : ExampleGroup? = nil, metadata : Metadata = Metadata.new) - super() - - @nodes = collection.map do |item| - Iteration.new(item, location, group, metadata).as(Node) - end - end - - # Creates a child that is attched to the group. - # Yields zero or more times to create the child. - # The group the child should be attached to is provided as a block argument. - def create_child - @nodes.each { |child| yield child.as(Iteration(T)) } - end - - # Adds the specified *node* to the group. - # Assigns the node to this group. - # If the node already belongs to a group, - # it will be removed from the previous group before adding it to this group. - def <<(node : Node) - @nodes.each { |child| child.as(Iteration(T)) << node.dup } - end - - private class Iteration(T) < ExampleGroup - def initialize(@item : T, location : Location? = nil, - group : ExampleGroup? = nil, metadata : Metadata = Metadata.new) - super(@item.inspect, location, group, metadata) - end - end - end -end diff --git a/src/spectator/iterative_example_group_builder.cr b/src/spectator/iterative_example_group_builder.cr new file mode 100644 index 0000000..48f5d0d --- /dev/null +++ b/src/spectator/iterative_example_group_builder.cr @@ -0,0 +1,26 @@ +require "./example_group_builder" +require "./example_group_iteration" + +module Spectator + class IterativeExampleGroupBuilder(T) < ExampleGroupBuilder + def initialize(@collection : Enumerable(T), + location : Location? = nil, metadata : Metadata = Metadata.new) + super(nil, location, metadata) + end + + def build(parent = nil) + ExampleGroup.new(@name, @location, parent, @metadata).tap do |group| + @before_all_hooks.each { |hook| group.add_before_all_hook(hook) } + @before_each_hooks.each { |hook| group.add_before_each_hook(hook) } + @after_all_hooks.each { |hook| group.add_after_all_hook(hook) } + @after_each_hooks.each { |hook| group.add_after_each_hook(hook) } + @around_each_hooks.each { |hook| group.add_around_each_hook(hook) } + @collection.each do |item| + ExampleGroupIteration.new(item, item.inspect, @location, group).tap do |iteration| + @children.each(&.build(iteration)) + end + end + end + end + end +end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 8e7ea3b..c7d0ac1 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -4,7 +4,7 @@ require "./example_builder" require "./example_context_method" require "./example_group" require "./example_group_builder" -require "./iterative_example_group" +require "./iterative_example_group_builder" require "./pending_example_builder" require "./spec" require "./metadata" @@ -78,7 +78,7 @@ module Spectator # For instance, adding a "pending" tag will mark tests as pending and skip execution. def start_iterative_group(collection, location = nil, metadata = Metadata.new) : Nil Log.trace { "Start iterative group: #{typeof(collection)} @ #{location}; metadata: #{metadata}" } - builder = ExampleGroupBuilder.new(collection, location, metadata) # TODO: IterativeExampleGroupBuilder + builder = IterativeExampleGroupBuilder.new(collection, location, metadata) current << builder @stack.push(builder) end From d8e9d3128a2683398fd50f7697ae9e1fefede4c2 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 12:07:04 -0600 Subject: [PATCH 330/399] Fetch iteration item from group --- src/spectator/dsl/groups.cr | 8 ++++++++ src/spectator/example_group_iteration.cr | 2 ++ 2 files changed, 10 insertions(+) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 453b195..a910906 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -136,6 +136,14 @@ module Spectator::DSL {{collection}} end + {% if block.args.size == 1 %} + let({{block.args.first}}) do |example| + example.group.as(::Spectator::ExampleGroupIteration(typeof(Group%group.%collection.first))).item + end + {% elsif block.args.size > 1 %} + {% raise "Expected 1 argument for 'sample' block, but got #{block.args.size}" %} + {% end %} + ::Spectator::DSL::Builder.start_iterative_group( %collection, ::Spectator::Location.new({{block.filename}}, {{block.line_number}}), diff --git a/src/spectator/example_group_iteration.cr b/src/spectator/example_group_iteration.cr index 9247b9b..d6995b8 100644 --- a/src/spectator/example_group_iteration.cr +++ b/src/spectator/example_group_iteration.cr @@ -2,6 +2,8 @@ require "./example_group" module Spectator class ExampleGroupIteration(T) < ExampleGroup + getter item : T + def initialize(@item : T, name : Label = nil, location : Location? = nil, group : ExampleGroup? = nil, metadata : Metadata = Metadata.new) super(name, location, group, metadata) From 640857bef2651299c1e59f1543560dcfb3fcf657 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 12:15:41 -0600 Subject: [PATCH 331/399] Pretty up iterative group names --- src/spectator/dsl/groups.cr | 2 ++ src/spectator/example_builder.cr | 2 +- src/spectator/iterative_example_group_builder.cr | 13 +++++++++---- src/spectator/spec_builder.cr | 6 +++--- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index a910906..2b1519c 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -146,6 +146,8 @@ module Spectator::DSL ::Spectator::DSL::Builder.start_iterative_group( %collection, + {{collection.stringify}}, + {{block.args.empty? ? :nil.id : block.args.first.stringify}}, ::Spectator::Location.new({{block.filename}}, {{block.line_number}}), metadata ) diff --git a/src/spectator/example_builder.cr b/src/spectator/example_builder.cr index eb6eec9..d261992 100644 --- a/src/spectator/example_builder.cr +++ b/src/spectator/example_builder.cr @@ -3,7 +3,7 @@ require "./node_builder" module Spectator class ExampleBuilder < NodeBuilder def initialize(@context_builder : -> Context, @entrypoint : Example ->, - @name : String? = nil, @location : Location? = nil, @metadata : Metadata = Metadata.new) + @name : String? = nil, @location : Location? = nil, @metadata : Metadata = Metadata.new) end def build(parent) diff --git a/src/spectator/iterative_example_group_builder.cr b/src/spectator/iterative_example_group_builder.cr index 48f5d0d..14c81ea 100644 --- a/src/spectator/iterative_example_group_builder.cr +++ b/src/spectator/iterative_example_group_builder.cr @@ -3,9 +3,9 @@ require "./example_group_iteration" module Spectator class IterativeExampleGroupBuilder(T) < ExampleGroupBuilder - def initialize(@collection : Enumerable(T), - location : Location? = nil, metadata : Metadata = Metadata.new) - super(nil, location, metadata) + def initialize(@collection : Enumerable(T), name : String? = nil, @iterator : String? = nil, + location : Location? = nil, metadata : Metadata = Metadata.new) + super(name, location, metadata) end def build(parent = nil) @@ -16,7 +16,12 @@ module Spectator @after_each_hooks.each { |hook| group.add_after_each_hook(hook) } @around_each_hooks.each { |hook| group.add_around_each_hook(hook) } @collection.each do |item| - ExampleGroupIteration.new(item, item.inspect, @location, group).tap do |iteration| + name = if iterator = @iterator + "#{iterator}: #{item.inspect}" + else + item.inspect + end + ExampleGroupIteration.new(item, name, @location, group).tap do |iteration| @children.each(&.build(iteration)) end end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index c7d0ac1..43e93a5 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -76,9 +76,9 @@ module Spectator # # A set of *metadata* can be used for filtering and modifying example behavior. # For instance, adding a "pending" tag will mark tests as pending and skip execution. - def start_iterative_group(collection, location = nil, metadata = Metadata.new) : Nil - Log.trace { "Start iterative group: #{typeof(collection)} @ #{location}; metadata: #{metadata}" } - builder = IterativeExampleGroupBuilder.new(collection, location, metadata) + def start_iterative_group(collection, name, iterator = nil, location = nil, metadata = Metadata.new) : Nil + Log.trace { "Start iterative group: #{name} (#{typeof(collection)}) @ #{location}; metadata: #{metadata}" } + builder = IterativeExampleGroupBuilder.new(collection, name, iterator, location, metadata) current << builder @stack.push(builder) end From 76a23131cbc44510f4372681e4c5aef4be465dec Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 12:36:21 -0600 Subject: [PATCH 332/399] More checks for missing block in DSL Improved some error messages. --- src/spectator/dsl/groups.cr | 22 ++++++++++++---------- src/spectator/dsl/memoize.cr | 36 ++++++++++++++++++------------------ 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 2b1519c..535a842 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -45,7 +45,7 @@ module Spectator::DSL metadata ) - \{{block.body}} + \{{block.body if block}} ::Spectator::DSL::Builder.end_group end @@ -136,14 +136,6 @@ module Spectator::DSL {{collection}} end - {% if block.args.size == 1 %} - let({{block.args.first}}) do |example| - example.group.as(::Spectator::ExampleGroupIteration(typeof(Group%group.%collection.first))).item - end - {% elsif block.args.size > 1 %} - {% raise "Expected 1 argument for 'sample' block, but got #{block.args.size}" %} - {% end %} - ::Spectator::DSL::Builder.start_iterative_group( %collection, {{collection.stringify}}, @@ -152,7 +144,17 @@ module Spectator::DSL metadata ) - {{block.body}} + {% if block %} + {% if block.args.size == 1 %} + let({{block.args.first}}) do |example| + example.group.as(::Spectator::ExampleGroupIteration(typeof(Group%group.%collection.first))).item + end + {% elsif block.args.size > 1 %} + {% raise "Expected 1 argument for 'sample' block, but got #{block.args.size}" %} + {% end %} + + {{block.body}} + {% end %} ::Spectator::DSL::Builder.end_group end diff --git a/src/spectator/dsl/memoize.cr b/src/spectator/dsl/memoize.cr index 0e9ab22..6ba3c9d 100644 --- a/src/spectator/dsl/memoize.cr +++ b/src/spectator/dsl/memoize.cr @@ -9,9 +9,9 @@ module Spectator::DSL # The block is evaluated only on the first time the getter is used # and the return value is saved for subsequent calls. macro let(name, &block) - {% raise "Block required for 'let'" unless block %} - {% raise "Cannot use 'let' inside of a test block" if @def %} - {% raise "Block argument count for 'let' must be 0..1" if block.args.size > 1 %} + {% raise "Missing block for 'let'" unless block %} + {% raise "Expected zero or one arguments for 'let', but got #{block.args.size}" if block.args.size > 1 %} + {% raise "Cannot use 'let' inside of an example block" if @def %} {% raise "Cannot use '#{name.id}' for 'let'" if ::Spectator::DSL::RESERVED_KEYWORDS.includes?(name.id.symbolize) %} @%value = ::Spectator::LazyWrapper.new @@ -31,9 +31,9 @@ module Spectator::DSL # The block is evaluated once before the example runs # and the return value is saved. macro let!(name, &block) - {% raise "Block required for 'let!'" unless block %} - {% raise "Cannot use 'let!' inside of a test block" if @def %} - {% raise "Block argument count for 'let!' must be 0..1" if block.args.size > 1 %} + {% raise "Missing block for 'let!'" unless block %} + {% raise "Expected zero or one arguments for 'let!', but got #{block.args.size}" if block.args.size > 1 %} + {% raise "Cannot use 'let!' inside of an example block" if @def %} {% raise "Cannot use '#{name.id}' for 'let!'" if ::Spectator::DSL::RESERVED_KEYWORDS.includes?(name.id.symbolize) %} let({{name}}) {{block}} @@ -45,9 +45,9 @@ module Spectator::DSL # The block is evaluated only the first time the subject is referenced # and the return value is saved for subsequent calls. macro subject(&block) - {% raise "Block required for 'subject'" unless block %} - {% raise "Cannot use 'subject' inside of a test block" if @def %} - {% raise "Block argument count for 'subject' must be 0..1" if block.args.size > 1 %} + {% raise "Missing block for 'subject'" unless block %} + {% raise "Expected zero or one arguments for 'let!', but got #{block.args.size}" if block.args.size > 1 %} + {% raise "Cannot use 'subject' inside of an example block" if @def %} let(subject) {{block}} end @@ -58,9 +58,9 @@ module Spectator::DSL # The block is evaluated only the first time the subject is referenced # and the return value is saved for subsequent calls. macro subject(name, &block) - {% raise "Block required for 'subject'" unless block %} - {% raise "Cannot use 'subject' inside of a test block" if @def %} - {% raise "Block argument count for 'subject' must be 0..1" if block.args.size > 1 %} + {% raise "Missing block for 'subject'" unless block %} + {% raise "Expected zero or one arguments for 'subject', but got #{block.args.size}" if block.args.size > 1 %} + {% raise "Cannot use 'subject' inside of an example block" if @def %} {% raise "Cannot use '#{name.id}' for 'subject'" if ::Spectator::DSL::RESERVED_KEYWORDS.includes?(name.id.symbolize) %} let({{name.id}}) {{block}} @@ -77,9 +77,9 @@ module Spectator::DSL # The block is evaluated once before the example runs # and the return value is saved for subsequent calls. macro subject!(&block) - {% raise "Block required for 'subject!'" unless block %} - {% raise "Cannot use 'subject!' inside of a test block" if @def %} - {% raise "Block argument count for 'subject!' must be 0..1" if block.args.size > 1 %} + {% raise "Missing block for 'subject'" unless block %} + {% raise "Expected zero or one arguments for 'subject!', but got #{block.args.size}" if block.args.size > 1 %} + {% raise "Cannot use 'subject!' inside of an example block" if @def %} let!(subject) {{block}} end @@ -90,9 +90,9 @@ module Spectator::DSL # The block is evaluated once before the example runs # and the return value is saved for subsequent calls. macro subject!(name, &block) - {% raise "Block required for 'subject!'" unless block %} - {% raise "Cannot use 'subject!' inside of a test block" if @def %} - {% raise "Block argument count for 'subject!' must be 0..1" if block.args.size > 1 %} + {% raise "Missing block for 'subject'" unless block %} + {% raise "Expected zero or one arguments for 'subject!', but got #{block.args.size}" if block.args.size > 1 %} + {% raise "Cannot use 'subject!' inside of an example block" if @def %} {% raise "Cannot use '#{name.id}' for 'subject!'" if ::Spectator::DSL::RESERVED_KEYWORDS.includes?(name.id.symbolize) %} let!({{name.id}}) {{block}} From 9d72d266308698340c986fe1a7ca0adbcebe0a2c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 12:49:11 -0600 Subject: [PATCH 333/399] Handle sample count --- CHANGELOG.md | 1 + src/spectator/dsl/groups.cr | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5d4252..3ab3a91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Cleanup and simplify DSL implementation. - Better error messages and detection when DSL methods are used when they shouldn't (i.e. `describe` inside `it`). - Prevent usage of reserved keywords in DSL (such as `initialize`). +- The count argument for `sample` and `random_sample` groups must be named (use `count: 5` instead of just `5`). - Other minor internal improvements and cleanup. ### Deprecated diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 535a842..44c75ed 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -125,8 +125,11 @@ module Spectator::DSL # Key-value pairs can also be specified. # Any falsey items will remove a previously defined tag. # + # The number of items iterated can be restricted by specifying a *count* argument. + # The first *count* items will be used if specified, otherwise all items will be used. + # # TODO: Handle string interpolation in example and group names. - macro sample(collection, *tags, **metadata, &block) + macro sample(collection, *tags, count = nil, **metadata, &block) {% raise "Cannot use 'sample' inside of a test block" if @def %} class Group%group < {{@type.id}} @@ -136,6 +139,12 @@ module Spectator::DSL {{collection}} end + {% if count %} + def self.%collection + previous_def.first({{count}}) + end + {% end %} + ::Spectator::DSL::Builder.start_iterative_group( %collection, {{collection.stringify}}, From e506c6b98140001b3a9a5bc38f121158a9b32ed3 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 13:05:03 -0600 Subject: [PATCH 334/399] Implement random_sample --- src/spectator/config.cr | 2 +- src/spectator/dsl/groups.cr | 59 +++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/spectator/config.cr b/src/spectator/config.cr index 9b0d891..65884fa 100644 --- a/src/spectator/config.cr +++ b/src/spectator/config.cr @@ -62,7 +62,7 @@ module Spectator # Retrieves the configured random number generator. # This will produce the same generator with the same seed every time. - private def random + def random Random.new(random_seed) end end diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 44c75ed..57d4017 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -168,5 +168,64 @@ module Spectator::DSL ::Spectator::DSL::Builder.end_group end end + + # Defines a new iterative example group. + # This type of group duplicates its contents for each element in *collection*. + # This is the same as `#sample` except that the items are shuffled. + # The items are selected with a RNG based on the seed. + # + # The first argument is the collection of elements to iterate over. + # + # Tags can be specified by adding symbols (keywords) after the first argument. + # Key-value pairs can also be specified. + # Any falsey items will remove a previously defined tag. + # + # The number of items iterated can be restricted by specifying a *count* argument. + # The first *count* items will be used if specified, otherwise all items will be used. + # + # TODO: Handle string interpolation in example and group names. + macro random_sample(collection, *tags, count = nil, **metadata, &block) + {% raise "Cannot use 'sample' inside of a test block" if @def %} + + class Group%group < {{@type.id}} + _spectator_metadata(:metadata, :super, {{tags.splat(", ")}} {{metadata.double_splat}}) + + def self.%collection + {{collection}} + end + + {% if count %} + def self.%collection + previous_def.sample({{count}}, ::Spectator.random) + end + {% else %} + def self.%collection + previous_def.to_a.shuffle(::Spectator.random) + end + {% end %} + + ::Spectator::DSL::Builder.start_iterative_group( + %collection, + {{collection.stringify}}, + {{block.args.empty? ? :nil.id : block.args.first.stringify}}, + ::Spectator::Location.new({{block.filename}}, {{block.line_number}}), + metadata + ) + + {% if block %} + {% if block.args.size == 1 %} + let({{block.args.first}}) do |example| + example.group.as(::Spectator::ExampleGroupIteration(typeof(Group%group.%collection.first))).item + end + {% elsif block.args.size > 1 %} + {% raise "Expected 1 argument for 'sample' block, but got #{block.args.size}" %} + {% end %} + + {{block.body}} + {% end %} + + ::Spectator::DSL::Builder.end_group + end + end end end From 571bc7d8a5abf699156dffbb3de840cb6d8c5676 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 13:25:38 -0600 Subject: [PATCH 335/399] Reuse iterative example group macro code Add support for x prefix to skip sample and random_sample groups. --- CHANGELOG.md | 2 + src/spectator/dsl/groups.cr | 157 +++++++++++++++++------------------- 2 files changed, 76 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ab3a91..0755b5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add matcher to check compiled type of values. - Examples can be skipped by using a `:pending` tag. A reason method can be specified: `pending: "Some excuse"` - Examples can be skipped during execution by using `skip` or `pending` in the example block. +- Sample blocks can be temporarily skipped by using `xsample` or `xrandom_sample`. ### Changed - Simplify and reduce defined types and generics. Should speed up compilation times. @@ -32,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Better error messages and detection when DSL methods are used when they shouldn't (i.e. `describe` inside `it`). - Prevent usage of reserved keywords in DSL (such as `initialize`). - The count argument for `sample` and `random_sample` groups must be named (use `count: 5` instead of just `5`). +- Helper methods used as arguments for `sample` and `random_sample` must be class methods. - Other minor internal improvements and cleanup. ### Deprecated diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 57d4017..d4de441 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -52,6 +52,71 @@ module Spectator::DSL end end + # Defines a macro to generate code for an iterative example group. + # The *name* is the name given to the macro. + # + # Default tags can be provided with *tags* and *metadata*. + # The tags are merged with parent groups. + # Any items with falsey values from *metadata* remove the corresponding tag. + # + # If provided, a block can be used to modify collection that will be iterated. + # It takes a single argument - the original collection from the user. + # The modified collection should be returned. + # + # TODO: Handle string interpolation in example and group names. + macro define_iterative_group(name, *tags, **metadata, &block) + macro {{name.id}}(collection, *tags, count = nil, **metadata, &block) + \{% raise "Cannot use 'sample' inside of a test block" if @def %} + + class Group\%group < \{{@type.id}} + _spectator_metadata(:metadata, :super, {{tags.splat(", ")}} {{metadata.double_splat}}) + _spectator_metadata(:metadata, :previous_def, \{{tags.splat(", ")}} \{{metadata.double_splat}}) + + def self.\%collection + \{{collection}} + end + + {% if block %} + def self.%mutate({{block.args.splat}}) + {{block.body}} + end + + def self.\%collection + %mutate(previous_def) + end + {% end %} + + \{% if count %} + def self.\%collection + previous_def.first(\{{count}}) + end + \{% end %} + + ::Spectator::DSL::Builder.start_iterative_group( + \%collection, + \{{collection.stringify}}, + \{{block.args.empty? ? :nil.id : block.args.first.stringify}}, + ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}), + metadata + ) + + \{% if block %} + \{% if block.args.size == 1 %} + let(\{{block.args.first}}) do |example| + example.group.as(::Spectator::ExampleGroupIteration(typeof(Group\%group.\%collection.first))).item + end + \{% elsif block.args.size > 1 %} + \{% raise "Expected 1 argument for 'sample' block, but got #{block.args.size}" %} + \{% end %} + + \{{block.body}} + \{% end %} + + ::Spectator::DSL::Builder.end_group + end + end + end + # Inserts the correct representation of a group's name. # If *what* appears to be a type name, it will be symbolized. # If it's a string, then it is dropped in as-is. @@ -127,47 +192,10 @@ module Spectator::DSL # # The number of items iterated can be restricted by specifying a *count* argument. # The first *count* items will be used if specified, otherwise all items will be used. - # - # TODO: Handle string interpolation in example and group names. - macro sample(collection, *tags, count = nil, **metadata, &block) - {% raise "Cannot use 'sample' inside of a test block" if @def %} + define_iterative_group :sample - class Group%group < {{@type.id}} - _spectator_metadata(:metadata, :super, {{tags.splat(", ")}} {{metadata.double_splat}}) - - def self.%collection - {{collection}} - end - - {% if count %} - def self.%collection - previous_def.first({{count}}) - end - {% end %} - - ::Spectator::DSL::Builder.start_iterative_group( - %collection, - {{collection.stringify}}, - {{block.args.empty? ? :nil.id : block.args.first.stringify}}, - ::Spectator::Location.new({{block.filename}}, {{block.line_number}}), - metadata - ) - - {% if block %} - {% if block.args.size == 1 %} - let({{block.args.first}}) do |example| - example.group.as(::Spectator::ExampleGroupIteration(typeof(Group%group.%collection.first))).item - end - {% elsif block.args.size > 1 %} - {% raise "Expected 1 argument for 'sample' block, but got #{block.args.size}" %} - {% end %} - - {{block.body}} - {% end %} - - ::Spectator::DSL::Builder.end_group - end - end + # :ditto: + define_iterative_group :xsample, skip: "Temporarily skipped with xsample" # Defines a new iterative example group. # This type of group duplicates its contents for each element in *collection*. @@ -182,50 +210,13 @@ module Spectator::DSL # # The number of items iterated can be restricted by specifying a *count* argument. # The first *count* items will be used if specified, otherwise all items will be used. - # - # TODO: Handle string interpolation in example and group names. - macro random_sample(collection, *tags, count = nil, **metadata, &block) - {% raise "Cannot use 'sample' inside of a test block" if @def %} + define_iterative_group :random_sample do |collection| + collection.to_a.shuffle(::Spectator.random) + end - class Group%group < {{@type.id}} - _spectator_metadata(:metadata, :super, {{tags.splat(", ")}} {{metadata.double_splat}}) - - def self.%collection - {{collection}} - end - - {% if count %} - def self.%collection - previous_def.sample({{count}}, ::Spectator.random) - end - {% else %} - def self.%collection - previous_def.to_a.shuffle(::Spectator.random) - end - {% end %} - - ::Spectator::DSL::Builder.start_iterative_group( - %collection, - {{collection.stringify}}, - {{block.args.empty? ? :nil.id : block.args.first.stringify}}, - ::Spectator::Location.new({{block.filename}}, {{block.line_number}}), - metadata - ) - - {% if block %} - {% if block.args.size == 1 %} - let({{block.args.first}}) do |example| - example.group.as(::Spectator::ExampleGroupIteration(typeof(Group%group.%collection.first))).item - end - {% elsif block.args.size > 1 %} - {% raise "Expected 1 argument for 'sample' block, but got #{block.args.size}" %} - {% end %} - - {{block.body}} - {% end %} - - ::Spectator::DSL::Builder.end_group - end + # :ditto: + define_iterative_group :xrandom_sample, skip: "Temporarily skipped with xrandom_sample" do |collection| + collection.to_a.shuffle(::Spectator.random) end end end From 1b53607f8ecf0d084a5e6df6fcde3317a1246111 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 14:01:27 -0600 Subject: [PATCH 336/399] Cleanup and add docs --- src/spectator/example_builder.cr | 15 ++++++- src/spectator/example_group_builder.cr | 34 +++++++++++++--- src/spectator/example_group_iteration.cr | 13 +++++++ .../iterative_example_group_builder.cr | 39 +++++++++++++------ src/spectator/node_builder.cr | 3 +- src/spectator/pending_example_builder.cr | 12 +++++- src/spectator/spec_builder.cr | 2 + 7 files changed, 99 insertions(+), 19 deletions(-) diff --git a/src/spectator/example_builder.cr b/src/spectator/example_builder.cr index d261992..c3cc01e 100644 --- a/src/spectator/example_builder.cr +++ b/src/spectator/example_builder.cr @@ -1,12 +1,25 @@ +require "./context" +require "./example" +require "./location" +require "./metadata" require "./node_builder" module Spectator + # Constructs examples. + # Call `#build` to produce an `Example`. class ExampleBuilder < NodeBuilder + # Creates the builder. + # A proc provided by *context_builder* is used to create a unique `Context` for each example produced by `#build`. + # The *entrypoint* indicates the proc used to invoke the test code in the example. + # The *name*, *location*, and *metadata* will be applied to the `Example` produced by `#build`. def initialize(@context_builder : -> Context, @entrypoint : Example ->, @name : String? = nil, @location : Location? = nil, @metadata : Metadata = Metadata.new) end - def build(parent) + # Constructs an example with previously defined attributes and context. + # The *parent* is an already constructed example group to nest the new example under. + # It can be nil if the new example won't have a parent. + def build(parent = nil) context = @context_builder.call Example.new(context, @entrypoint, @name, @location, parent, @metadata) end diff --git a/src/spectator/example_group_builder.cr b/src/spectator/example_group_builder.cr index e140203..0c3affa 100644 --- a/src/spectator/example_group_builder.cr +++ b/src/spectator/example_group_builder.cr @@ -1,6 +1,16 @@ +require "./example_group" +require "./example_group_hook" +require "./example_hook" +require "./example_procsy_hook" +require "./label" +require "./location" +require "./metadata" require "./node_builder" module Spectator + # Progressively constructs an example group. + # Hooks and builders for child nodes can be added over time to this builder. + # When done, call `#build` to produce an `ExampleGroup`. class ExampleGroupBuilder < NodeBuilder @children = [] of NodeBuilder @before_all_hooks = [] of ExampleGroupHook @@ -9,6 +19,9 @@ module Spectator @after_each_hooks = [] of ExampleHook @around_each_hooks = [] of ExampleProcsyHook + # Creates the builder. + # Initially, the builder will have no children and no hooks. + # The *name*, *location*, and *metadata* will be applied to the `ExampleGroup` produced by `#build`. def initialize(@name : Label = nil, @location : Location? = nil, @metadata : Metadata = Metadata.new) end @@ -68,19 +81,30 @@ module Spectator @around_each_hooks << ExampleProcsyHook.new(label: "around_each", &block) end + # Constructs an example group with previously defined attributes, children, and hooks. + # The *parent* is an already constructed example group to nest the new example group under. + # It can be nil if the new example group won't have a parent. def build(parent = nil) ExampleGroup.new(@name, @location, parent, @metadata).tap do |group| - @before_all_hooks.each { |hook| group.add_before_all_hook(hook) } - @before_each_hooks.each { |hook| group.add_before_each_hook(hook) } - @after_all_hooks.each { |hook| group.add_after_all_hook(hook) } - @after_each_hooks.each { |hook| group.add_after_each_hook(hook) } - @around_each_hooks.each { |hook| group.add_around_each_hook(hook) } + apply_hooks(group) @children.each(&.build(group)) end end + # Adds a child builder to the group. + # The *builder* will have `NodeBuilder#build` called on it from within `#build`. + # The new example group will be passed to it. def <<(builder) @children << builder end + + # Adds all previously configured hooks to an example group. + private def apply_hooks(group) + @before_all_hooks.each { |hook| group.add_before_all_hook(hook) } + @before_each_hooks.each { |hook| group.add_before_each_hook(hook) } + @after_all_hooks.each { |hook| group.add_after_all_hook(hook) } + @after_each_hooks.each { |hook| group.add_after_each_hook(hook) } + @around_each_hooks.each { |hook| group.add_around_each_hook(hook) } + end end end diff --git a/src/spectator/example_group_iteration.cr b/src/spectator/example_group_iteration.cr index d6995b8..d6576d2 100644 --- a/src/spectator/example_group_iteration.cr +++ b/src/spectator/example_group_iteration.cr @@ -1,9 +1,22 @@ require "./example_group" +require "./label" +require "./location" +require "./metadata" module Spectator + # Collection of examples and sub-groups for a single iteration of an iterative example group. class ExampleGroupIteration(T) < ExampleGroup + # Item for this iteration of the example groups. getter item : T + # Creates the example group iteration. + # The element for the current iteration is provided by *item*. + # The *name* describes the purpose of the group. + # It can be a `Symbol` to describe a type. + # This is typically a stringified form of *item*. + # The *location* tracks where the group exists in source code. + # This group will be assigned to the parent *group* if it is provided. + # A set of *metadata* can be used for filtering and modifying example behavior. def initialize(@item : T, name : Label = nil, location : Location? = nil, group : ExampleGroup? = nil, metadata : Metadata = Metadata.new) super(name, location, group, metadata) diff --git a/src/spectator/iterative_example_group_builder.cr b/src/spectator/iterative_example_group_builder.cr index 14c81ea..6bc866b 100644 --- a/src/spectator/iterative_example_group_builder.cr +++ b/src/spectator/iterative_example_group_builder.cr @@ -1,31 +1,48 @@ +require "./example_group" require "./example_group_builder" require "./example_group_iteration" +require "./location" +require "./metadata" module Spectator + # Progressively constructs an iterative example group. + # Hooks and builders for child nodes can be added over time to this builder. + # When done, call `#build` to produce an `ExampleGroup` with nested `ExampleGroupIteration` instances. class IterativeExampleGroupBuilder(T) < ExampleGroupBuilder + # Creates the builder. + # Initially, the builder will have no children and no hooks. + # The *name*, *location*, and *metadata* will be applied to the `ExampleGroup` produced by `#build`. + # The *collection* is the set of items to create sub-nodes for. + # The *iterator* is an optional name given to a single item in the collection. def initialize(@collection : Enumerable(T), name : String? = nil, @iterator : String? = nil, location : Location? = nil, metadata : Metadata = Metadata.new) super(name, location, metadata) end + # Constructs an iterative example group with previously defined attributes, children, and hooks. + # The *parent* is an already constructed example group to nest the new example group under. + # It can be nil if the new example group won't have a parent. def build(parent = nil) ExampleGroup.new(@name, @location, parent, @metadata).tap do |group| - @before_all_hooks.each { |hook| group.add_before_all_hook(hook) } - @before_each_hooks.each { |hook| group.add_before_each_hook(hook) } - @after_all_hooks.each { |hook| group.add_after_all_hook(hook) } - @after_each_hooks.each { |hook| group.add_after_each_hook(hook) } - @around_each_hooks.each { |hook| group.add_around_each_hook(hook) } + # Hooks are applied once to the outer group, + # instead of multiple times for each inner group (iteration). + apply_hooks(group) + @collection.each do |item| - name = if iterator = @iterator - "#{iterator}: #{item.inspect}" - else - item.inspect - end - ExampleGroupIteration.new(item, name, @location, group).tap do |iteration| + ExampleGroupIteration.new(item, iteration_name(item), @location, group).tap do |iteration| @children.each(&.build(iteration)) end end end end + + # Constructs the name of an example group iteration. + private def iteration_name(item) + if iterator = @iterator + "#{iterator}: #{item.inspect}" + else + item.inspect + end + end end end diff --git a/src/spectator/node_builder.cr b/src/spectator/node_builder.cr index 457f39f..939374b 100644 --- a/src/spectator/node_builder.cr +++ b/src/spectator/node_builder.cr @@ -1,5 +1,6 @@ module Spectator abstract class NodeBuilder - abstract def build(parent) + # Produces a node for a spec. + abstract def build(parent = nil) end end diff --git a/src/spectator/pending_example_builder.cr b/src/spectator/pending_example_builder.cr index 40880b1..c7a1a46 100644 --- a/src/spectator/pending_example_builder.cr +++ b/src/spectator/pending_example_builder.cr @@ -1,11 +1,21 @@ +require "./example" +require "./location" +require "./metadata" require "./node_builder" module Spectator + # Constructs pending examples. + # Call `#build` to produce an `Example`. class PendingExampleBuilder < NodeBuilder + # Creates the builder. + # The *name*, *location*, and *metadata* will be applied to the `Example` produced by `#build`. def initialize(@name : String? = nil, @location : Location? = nil, @metadata : Metadata = Metadata.new) end - def build(parent) + # Constructs an example with previously defined attributes. + # The *parent* is an already constructed example group to nest the new example under. + # It can be nil if the new example won't have a parent. + def build(parent = nil) Example.pending(@name, @location, parent, @metadata) end end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 43e93a5..7b63013 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -71,6 +71,8 @@ module Spectator # # The *collection* is the set of items to iterate over. # Child nodes in this group will be executed once for every item in the collection. + # The *name* should be a string representation of *collection*. + # The *iterator* is an optional name given to a single item in *collection*. # # The *location* optionally defined where the group originates in source code. # From af13a89257fb322547347242c5d40ce3954d5d01 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 14:04:17 -0600 Subject: [PATCH 337/399] Pass along fallback reason --- src/spectator/pending_example_builder.cr | 6 ++++-- src/spectator/spec_builder.cr | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/spectator/pending_example_builder.cr b/src/spectator/pending_example_builder.cr index c7a1a46..90aaf47 100644 --- a/src/spectator/pending_example_builder.cr +++ b/src/spectator/pending_example_builder.cr @@ -9,14 +9,16 @@ module Spectator class PendingExampleBuilder < NodeBuilder # Creates the builder. # The *name*, *location*, and *metadata* will be applied to the `Example` produced by `#build`. - def initialize(@name : String? = nil, @location : Location? = nil, @metadata : Metadata = Metadata.new) + # A default *reason* can be given in case the user didn't provide one. + def initialize(@name : String? = nil, @location : Location? = nil, + @metadata : Metadata = Metadata.new, @reason : String? = nil) end # Constructs an example with previously defined attributes. # The *parent* is an already constructed example group to nest the new example under. # It can be nil if the new example won't have a parent. def build(parent = nil) - Example.pending(@name, @location, parent, @metadata) + Example.pending(@name, @location, parent, @metadata, @reason) end end end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 7b63013..1e03c79 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -132,7 +132,7 @@ module Spectator # A default *reason* can be given in case the user didn't provide one. def add_pending_example(name, location, metadata = Metadata.new, reason = nil) : Nil Log.trace { "Add pending example: #{name} @ #{location}; metadata: #{metadata}" } - current << PendingExampleBuilder.new(name, location, metadata) + current << PendingExampleBuilder.new(name, location, metadata, reason) end # Attaches a hook to be invoked before any and all examples in the current group. From a810eef16c7efa9f07e43a4538e94535629facce Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 14:19:16 -0600 Subject: [PATCH 338/399] Add `before_suite` and `after_suite` --- CHANGELOG.md | 1 + src/spectator/dsl/builder.cr | 12 ++++++++++++ src/spectator/dsl/hooks.cr | 10 ++++++++++ src/spectator/spec_builder.cr | 24 ++++++++++++++++++++++++ 4 files changed, 47 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0755b5e..828b451 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Examples can be skipped by using a `:pending` tag. A reason method can be specified: `pending: "Some excuse"` - Examples can be skipped during execution by using `skip` or `pending` in the example block. - Sample blocks can be temporarily skipped by using `xsample` or `xrandom_sample`. +- Add `before_suite` and `after_suite` hooks. ### Changed - Simplify and reduce defined types and generics. Should speed up compilation times. diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr index e0b724d..16b1116 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -54,6 +54,12 @@ module Spectator::DSL @@builder.add_pending_example(*args) end + # Defines a block of code to execute before any and all examples in the test suite. + def before_suite(location = nil, label = "before_suite", &block) + hook = ExampleGroupHook.new(location: location, label: label, &block) + @@builder.before_suite(hook) + end + # Defines a block of code to execute before any and all examples in the current group. def before_all(location = nil, label = "before_all", &block) hook = ExampleGroupHook.new(location: location, label: label, &block) @@ -66,6 +72,12 @@ module Spectator::DSL @@builder.before_each(hook) end + # Defines a block of code to execute after any and all examples in the test suite. + def after_suite(location = nil, label = "after_suite", &block) + hook = ExampleGroupHook.new(location: location, label: label, &block) + @@builder.after_suite(hook) + end + # Defines a block of code to execute after any and all examples in the current group. def after_all(location = nil, label = "after_all", &block) hook = ExampleGroupHook.new(location: location, label: label, &block) diff --git a/src/spectator/dsl/hooks.cr b/src/spectator/dsl/hooks.cr index 61fa15f..1e990fd 100644 --- a/src/spectator/dsl/hooks.cr +++ b/src/spectator/dsl/hooks.cr @@ -47,6 +47,16 @@ module Spectator::DSL end end + # Defines a block of code that will be invoked once before any examples in the suite. + # The block will not run in the context of the current running example. + # This means that values defined by `let` and `subject` are not available. + define_example_group_hook :before_suite + + # Defines a block of code that will be invoked once after all examples in the suite. + # The block will not run in the context of the current running example. + # This means that values defined by `let` and `subject` are not available. + define_example_group_hook :after_suite + # Defines a block of code that will be invoked once before any examples in the group. # The block will not run in the context of the current running example. # This means that values defined by `let` and `subject` are not available. diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 1e03c79..579d922 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -135,6 +135,18 @@ module Spectator current << PendingExampleBuilder.new(name, location, metadata, reason) end + # Attaches a hook to be invoked before any and all examples in the test suite. + def before_suite(hook) + Log.trace { "Add before_suite hook #{hook}" } + root.add_before_all_hook(hook) + end + + # Defines a block of code to execute before any and all examples in the test suite. + def before_suite(&block) + Log.trace { "Add before_suite hook" } + root.before_all(&block) + end + # Attaches a hook to be invoked before any and all examples in the current group. def before_all(hook) Log.trace { "Add before_all hook #{hook}" } @@ -161,6 +173,18 @@ module Spectator current.before_each(&block) end + # Attaches a hook to be invoked after any and all examples in the test suite. + def after_suite(hook) + Log.trace { "Add after_suite hook #{hook}" } + root.add_after_all_hook(hook) + end + + # Defines a block of code to execute after any and all examples in the test suite. + def after_suite(&block) + Log.trace { "Add after_suite hook" } + root.after_all(&block) + end + # Attaches a hook to be invoked after any and all examples in the current group. def after_all(hook) Log.trace { "Add after_all hook #{hook}" } From 009266c8c275d3af3ecd4023d91a0ee1ea450dfc Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 14:32:55 -0600 Subject: [PATCH 339/399] Fix naming of hook methods when using a block --- src/spectator/example_group_builder.cr | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/spectator/example_group_builder.cr b/src/spectator/example_group_builder.cr index 0c3affa..aa59626 100644 --- a/src/spectator/example_group_builder.cr +++ b/src/spectator/example_group_builder.cr @@ -31,7 +31,7 @@ module Spectator end # Defines a block of code to execute before any and all examples in the current group. - def add_before_all_hook(&block) + def before_all(&block) @before_all_hooks << ExampleGroupHook.new(&block) end @@ -43,7 +43,7 @@ module Spectator # Defines a block of code to execute before every example in the current group. # The current example is provided as a block argument. - def add_before_each_hook(&block : Example -> _) + def before_each(&block : Example -> _) @before_each_hooks << ExampleHook.new(&block) end @@ -53,7 +53,7 @@ module Spectator end # Defines a block of code to execute after any and all examples in the current group. - def add_after_all_hook(&block) + def after_all(&block) @after_all_hooks << ExampleGroupHook.new(&block) end @@ -65,7 +65,7 @@ module Spectator # Defines a block of code to execute after every example in the current group. # The current example is provided as a block argument. - def add_after_each_hook(&block : Example -> _) + def after_each(&block : Example -> _) @after_each_hooks << ExampleHook.new(&block) end @@ -77,7 +77,7 @@ module Spectator # Defines a block of code to execute around every example in the current group. # The current example in procsy form is provided as a block argument. - def add_around_each_hook(&block : Example -> _) + def around_each(&block : Example -> _) @around_each_hooks << ExampleProcsyHook.new(label: "around_each", &block) end From 937b084f66549f9611bb24be83e391989cdf5a87 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 15:20:58 -0600 Subject: [PATCH 340/399] Support defining hooks in configuration block --- CHANGELOG.md | 1 + src/spectator/config.cr | 29 ++++++++++ src/spectator/config/builder.cr | 97 +++++++++++++++++++++++++++++++++ src/spectator/events.cr | 12 ++++ src/spectator/example_group.cr | 6 ++ src/spectator/spec_builder.cr | 20 ++++++- 6 files changed, 164 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 828b451..253d679 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Examples can be skipped during execution by using `skip` or `pending` in the example block. - Sample blocks can be temporarily skipped by using `xsample` or `xrandom_sample`. - Add `before_suite` and `after_suite` hooks. +- Support defining hooks in `Spectator.configure` block. ### Changed - Simplify and reduce defined types and generics. Should speed up compilation times. diff --git a/src/spectator/config.cr b/src/spectator/config.cr index 65884fa..549c5e6 100644 --- a/src/spectator/config.cr +++ b/src/spectator/config.cr @@ -20,6 +20,27 @@ module Spectator # Filter used to select which examples to run. getter example_filter : ExampleFilter + # List of hooks to run before all examples in the test suite. + protected getter before_suite_hooks : Array(ExampleGroupHook) + + # List of hooks to run before each top-level example group. + protected getter before_all_hooks : Array(ExampleGroupHook) + + # List of hooks to run before every example. + protected getter before_each_hooks : Array(ExampleHook) + + # List of hooks to run after all examples in the test suite. + protected getter after_suite_hooks : Array(ExampleGroupHook) + + # List of hooks to run after each top-level example group. + protected getter after_all_hooks : Array(ExampleGroupHook) + + # List of hooks to run after every example. + protected getter after_each_hooks : Array(ExampleHook) + + # List of hooks to run around every example. + protected getter around_each_hooks : Array(ExampleProcsyHook) + # Creates a new configuration. # Properties are pulled from *source*. # Typically, *source* is a `Config::Builder`. @@ -28,6 +49,14 @@ module Spectator @run_flags = source.run_flags @random_seed = source.random_seed @example_filter = source.example_filter + + @before_suite_hooks = source.before_suite_hooks + @before_all_hooks = source.before_all_hooks + @before_each_hooks = source.before_each_hooks + @after_suite_hooks = source.after_suite_hooks + @after_all_hooks = source.after_all_hooks + @after_each_hooks = source.after_each_hooks + @around_each_hooks = source.around_each_hooks end # Produces the default configuration. diff --git a/src/spectator/config/builder.cr b/src/spectator/config/builder.cr index 4ecb4d9..ff7f08a 100644 --- a/src/spectator/config/builder.cr +++ b/src/spectator/config/builder.cr @@ -20,6 +20,103 @@ module Spectator @additional_formatters = [] of Formatting::Formatter @filters = [] of ExampleFilter + # List of hooks to run before all examples in the test suite. + protected getter before_suite_hooks = [] of ExampleGroupHook + + # List of hooks to run before each top-level example group. + protected getter before_all_hooks = [] of ExampleGroupHook + + # List of hooks to run before every example. + protected getter before_each_hooks = [] of ExampleHook + + # List of hooks to run after all examples in the test suite. + protected getter after_suite_hooks = [] of ExampleGroupHook + + # List of hooks to run after each top-level example group. + protected getter after_all_hooks = [] of ExampleGroupHook + + # List of hooks to run after every example. + protected getter after_each_hooks = [] of ExampleHook + + # List of hooks to run around every example. + protected getter around_each_hooks = [] of ExampleProcsyHook + + # Attaches a hook to be invoked before all examples in the test suite. + def add_before_suite_hook(hook) + @before_suite_hooks << hook + end + + # Defines a block of code to execute before all examples in the test suite. + def before_suite(&block) + @before_suite_hooks << ExampleGroupHook.new(&block) + end + + # Attaches a hook to be invoked before each top-level example group. + def add_before_all_hook(hook) + @before_all_hooks << hook + end + + # Defines a block of code to execute before each top-level example group. + def before_all(&block) + @before_all_hooks << ExampleGroupHook.new(&block) + end + + # Attaches a hook to be invoked before every example. + # The current example is provided as a block argument. + def add_before_each_hook(hook) + @before_each_hooks << hook + end + + # Defines a block of code to execute before every. + # The current example is provided as a block argument. + def before_each(&block : Example -> _) + @before_each_hooks << ExampleHook.new(&block) + end + + # Attaches a hook to be invoked after all examples in the test suite. + def add_after_suite_hook(hook) + @after_suite_hooks << hook + end + + # Defines a block of code to execute after all examples in the test suite. + def after_suite(&block) + @after_suite_hooks << ExampleGroupHook.new(&block) + end + + # Attaches a hook to be invoked after each top-level example group. + def add_after_all_hook(hook) + @after_all_hooks << hook + end + + # Defines a block of code to execute after each top-level example group. + def after_all(&block) + @after_all_hooks << ExampleGroupHook.new(&block) + end + + # Attaches a hook to be invoked after every example. + # The current example is provided as a block argument. + def add_after_each_hook(hook) + @after_each_hooks << hook + end + + # Defines a block of code to execute after every example. + # The current example is provided as a block argument. + def after_each(&block : Example -> _) + @after_each_hooks << ExampleHook.new(&block) + end + + # Attaches a hook to be invoked around every example. + # The current example in procsy form is provided as a block argument. + def add_around_each_hook(hook) + @around_each_hooks << hook + end + + # Defines a block of code to execute around every example. + # The current example in procsy form is provided as a block argument. + def around_each(&block : Example -> _) + @around_each_hooks << ExampleProcsyHook.new(label: "around_each", &block) + end + # Creates a configuration. def build : Config Config.new(self) diff --git a/src/spectator/events.cr b/src/spectator/events.cr index 42c6bbd..12f81a6 100644 --- a/src/spectator/events.cr +++ b/src/spectator/events.cr @@ -33,6 +33,12 @@ module Spectator @%hooks << hook end + # Adds a hook to be invoked when the *{{name.id}}* event occurs. + # The hook is added to the front of the list. + def prepend_{{name.id}}_hook(hook : ExampleGroupHook) : Nil + @%hooks.unshift(hook) + end + # Defines a hook for the *{{name.id}}* event. # The block of code given to this method is invoked when the event occurs. def {{name.id}}(&block : -> _) : Nil @@ -86,6 +92,12 @@ module Spectator @%hooks << hook end + # Adds a hook to be invoked when the *{{name.id}}* event occurs. + # The hook is added to the front of the list. + def prepend_{{name.id}}_hook(hook : ExampleHook) : Nil + @%hooks.unshift(hook) + end + # Defines a hook for the *{{name.id}}* event. # The block of code given to this method is invoked when the event occurs. # The current example is provided as a block argument. diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index 01f0d0e..fb3624f 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -166,6 +166,12 @@ module Spectator @around_hooks << hook end + # Adds a hook to be invoked when the *around_each* event occurs. + # The hook is added to the front of the list. + def prepend_around_each_hook(hook : ExampleProcsyHook) : Nil + @around_hooks.unshift(hook) + end + # Defines a hook for the *around_each* event. # The block of code given to this method is invoked when the event occurs. # The current example is provided as a block argument. diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 579d922..8bbf408 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -43,7 +43,25 @@ module Spectator def build : Spec raise "Mismatched start and end groups" unless root? - Spec.new(root.build, config) + group = root.build + + # Apply hooks from configuration. + config.before_suite_hooks.each { |hook| group.prepend_before_all_hook(hook) } + config.after_suite_hooks.each { |hook| group.prepend_after_all_hook(hook) } + config.before_each_hooks.each { |hook| group.prepend_before_each_hook(hook) } + config.after_each_hooks.each { |hook| group.prepend_after_each_hook(hook) } + config.around_each_hooks.each { |hook| group.prepend_around_each_hook(hook) } + + # `before_all` and `after_all` hooks are slightly different. + # They are applied to every top-level group (groups just under root). + group.each do |node| + next unless node.is_a?(Events) + + config.before_all_hooks.reverse_each { |hook| node.prepend_before_all_hook(hook) } + config.after_all_hooks.reverse_each { |hook| node.prepend_after_all_hook(hook) } + end + + Spec.new(group, config) end # Defines a new example group and pushes it onto the group stack. From f75991f34cb57bd20e9e2c758eb99874d82b371e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 15:21:15 -0600 Subject: [PATCH 341/399] Formatting --- src/spectator/pending_example_builder.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/pending_example_builder.cr b/src/spectator/pending_example_builder.cr index 90aaf47..a1f0292 100644 --- a/src/spectator/pending_example_builder.cr +++ b/src/spectator/pending_example_builder.cr @@ -11,7 +11,7 @@ module Spectator # The *name*, *location*, and *metadata* will be applied to the `Example` produced by `#build`. # A default *reason* can be given in case the user didn't provide one. def initialize(@name : String? = nil, @location : Location? = nil, - @metadata : Metadata = Metadata.new, @reason : String? = nil) + @metadata : Metadata = Metadata.new, @reason : String? = nil) end # Constructs an example with previously defined attributes. From 9c7f39ba45cd1209b0edeb22293c6a74fcbb4868 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 16:03:01 -0600 Subject: [PATCH 342/399] Update references to existing issues --- CHANGELOG.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 253d679..3435d8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,25 +17,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Tags can be added to examples and example groups. - Add matcher to check compiled type of values. - Examples can be skipped by using a `:pending` tag. A reason method can be specified: `pending: "Some excuse"` +- Examples missing a block are marked as pending. [#37](https://gitlab.com/arctic-fox/spectator/-/issues/37) - Examples can be skipped during execution by using `skip` or `pending` in the example block. - Sample blocks can be temporarily skipped by using `xsample` or `xrandom_sample`. -- Add `before_suite` and `after_suite` hooks. -- Support defining hooks in `Spectator.configure` block. +- Add `before_suite` and `after_suite` hooks. [#21](https://gitlab.com/arctic-fox/spectator/-/issues/21) +- Support defining hooks in `Spectator.configure` block. [#21](https://gitlab.com/arctic-fox/spectator/-/issues/21) ### Changed -- Simplify and reduce defined types and generics. Should speed up compilation times. - `around_each` hooks wrap `before_all` and `after_all` hooks. [#12](https://github.com/icy-arctic-fox/spectator/issues/12) - `given` (now `provided`) blocks changed to produce a single example. `it` can no longer be nested in a `provided` block. - The "should" syntax no longer reports the source as inside Spectator. - Short-hand "should" syntax must be included by using `require "spectator/should"` - `it { should eq("foo") }` -- Overhaul example creation and handling. -- Overhaul storage of test values. -- Overhaul reporting and formatting. Cleaner output for failures and pending tests. -- Cleanup and simplify DSL implementation. - Better error messages and detection when DSL methods are used when they shouldn't (i.e. `describe` inside `it`). - Prevent usage of reserved keywords in DSL (such as `initialize`). - The count argument for `sample` and `random_sample` groups must be named (use `count: 5` instead of just `5`). - Helper methods used as arguments for `sample` and `random_sample` must be class methods. +- Simplify and reduce defined types and generics. Should speed up compilation times. +- Overhaul example creation and handling. +- Overhaul storage of test values. +- Overhaul reporting and formatting. Cleaner output for failures and pending tests. +- Cleanup and simplify DSL implementation. - Other minor internal improvements and cleanup. ### Deprecated From 7e2b267e93a359e028cb094361fc7c2f74ddc78f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 16:04:19 -0600 Subject: [PATCH 343/399] Use failure location in output if available Fixes https://gitlab.com/arctic-fox/spectator/-/issues/57 --- src/spectator/fail_result.cr | 11 +++++++++++ .../formatting/components/example_command.cr | 7 +++++-- .../formatting/components/failure_command_list.cr | 7 ++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/spectator/fail_result.cr b/src/spectator/fail_result.cr index 41a4aba..f331d07 100644 --- a/src/spectator/fail_result.cr +++ b/src/spectator/fail_result.cr @@ -1,4 +1,6 @@ require "json" +require "./example_failed" +require "./location" require "./result" module Spectator @@ -36,6 +38,15 @@ module Spectator true end + # Attempts to retrieve the location where the example failed. + # This only works if the location of the failed expectation was reported. + # If available, returns a `Location`, otherwise `nil`. + def source : Location? + return unless error = @error.as?(ExampleFailed) + + error.location? + end + # One-word description of the result. def to_s(io) io << "fail" diff --git a/src/spectator/formatting/components/example_command.cr b/src/spectator/formatting/components/example_command.cr index 8b3c0b9..92d7627 100644 --- a/src/spectator/formatting/components/example_command.cr +++ b/src/spectator/formatting/components/example_command.cr @@ -1,11 +1,14 @@ require "../../example" +require "../../location" require "./comment" module Spectator::Formatting::Components # Provides syntax for running a specific example from the command-line. struct ExampleCommand # Creates the component with the specified example. - def initialize(@example : Example) + # The location can be overridden, for instance, pointing to a problematic line in the example. + # Otherwise the example's location is used. + def initialize(@example : Example, @location : Location? = nil) end # Produces output for running the previously specified example. @@ -14,7 +17,7 @@ module Spectator::Formatting::Components # Use location for argument if it's available, since it's simpler. # Otherwise, use the example name filter argument. - if location = @example.location? + if location = (@location || @example.location?) io << location else io << "-e " << @example diff --git a/src/spectator/formatting/components/failure_command_list.cr b/src/spectator/formatting/components/failure_command_list.cr index 7da5ed2..177cf1a 100644 --- a/src/spectator/formatting/components/failure_command_list.cr +++ b/src/spectator/formatting/components/failure_command_list.cr @@ -14,7 +14,12 @@ module Spectator::Formatting::Components io.puts "Failed examples:" io.puts @failures.each do |failure| - io.puts ExampleCommand.new(failure).colorize(:red) + # Use failed location if it's available. + if (result = failure.result).responds_to?(:source) + location = result.source + end + + io.puts ExampleCommand.new(failure, location).colorize(:red) end end end From 81f1966417f3ddcb13a6aa40fc9c26baabf7a0a4 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 16:12:10 -0600 Subject: [PATCH 344/399] Use location instead of source --- src/spectator/fail_result.cr | 2 +- .../components/failure_command_list.cr | 4 ++-- .../formatting/components/result_block.cr | 20 +++++++++---------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/spectator/fail_result.cr b/src/spectator/fail_result.cr index f331d07..894defa 100644 --- a/src/spectator/fail_result.cr +++ b/src/spectator/fail_result.cr @@ -41,7 +41,7 @@ module Spectator # Attempts to retrieve the location where the example failed. # This only works if the location of the failed expectation was reported. # If available, returns a `Location`, otherwise `nil`. - def source : Location? + def location : Location? return unless error = @error.as?(ExampleFailed) error.location? diff --git a/src/spectator/formatting/components/failure_command_list.cr b/src/spectator/formatting/components/failure_command_list.cr index 177cf1a..ceb3242 100644 --- a/src/spectator/formatting/components/failure_command_list.cr +++ b/src/spectator/formatting/components/failure_command_list.cr @@ -15,8 +15,8 @@ module Spectator::Formatting::Components io.puts @failures.each do |failure| # Use failed location if it's available. - if (result = failure.result).responds_to?(:source) - location = result.source + if (result = failure.result).responds_to?(:location) + location = result.location end io.puts ExampleCommand.new(failure, location).colorize(:red) diff --git a/src/spectator/formatting/components/result_block.cr b/src/spectator/formatting/components/result_block.cr index 189bfbe..36eb1a6 100644 --- a/src/spectator/formatting/components/result_block.cr +++ b/src/spectator/formatting/components/result_block.cr @@ -48,7 +48,7 @@ module Spectator::Formatting::Components subtitle_line(io) io.puts content(io) - source_line(io) + location_line(io) end end @@ -68,16 +68,16 @@ module Spectator::Formatting::Components end end - # Produces the (example) source line. - private def source_line(io) - source = if (result = @example.result).responds_to?(:source) - result.source - else - @example.location? - end - return unless source + # Produces the (example) location line. + private def location_line(io) + location = if (result = @example.result).responds_to?(:location) + result.location + else + @example.location? + end + return unless location - line(io) { io << Comment.colorize(source) } + line(io) { io << Comment.colorize(location) } end # Computes the number of spaces the index takes From 7cb1545e83dd37658898f6bcb70f980f26035279 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 16:15:11 -0600 Subject: [PATCH 345/399] Don't use failure location in failed example block output This is problematic, since the failure could have ocurred outside the example block (in a method call). The comment line under the failure details will still point to the result location, if it's available. --- src/spectator/formatting/components/example_command.cr | 7 ++----- .../formatting/components/failure_command_list.cr | 7 +------ src/spectator/formatting/components/result_block.cr | 3 ++- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/spectator/formatting/components/example_command.cr b/src/spectator/formatting/components/example_command.cr index 92d7627..8b3c0b9 100644 --- a/src/spectator/formatting/components/example_command.cr +++ b/src/spectator/formatting/components/example_command.cr @@ -1,14 +1,11 @@ require "../../example" -require "../../location" require "./comment" module Spectator::Formatting::Components # Provides syntax for running a specific example from the command-line. struct ExampleCommand # Creates the component with the specified example. - # The location can be overridden, for instance, pointing to a problematic line in the example. - # Otherwise the example's location is used. - def initialize(@example : Example, @location : Location? = nil) + def initialize(@example : Example) end # Produces output for running the previously specified example. @@ -17,7 +14,7 @@ module Spectator::Formatting::Components # Use location for argument if it's available, since it's simpler. # Otherwise, use the example name filter argument. - if location = (@location || @example.location?) + if location = @example.location? io << location else io << "-e " << @example diff --git a/src/spectator/formatting/components/failure_command_list.cr b/src/spectator/formatting/components/failure_command_list.cr index ceb3242..7da5ed2 100644 --- a/src/spectator/formatting/components/failure_command_list.cr +++ b/src/spectator/formatting/components/failure_command_list.cr @@ -14,12 +14,7 @@ module Spectator::Formatting::Components io.puts "Failed examples:" io.puts @failures.each do |failure| - # Use failed location if it's available. - if (result = failure.result).responds_to?(:location) - location = result.location - end - - io.puts ExampleCommand.new(failure, location).colorize(:red) + io.puts ExampleCommand.new(failure).colorize(:red) end end end diff --git a/src/spectator/formatting/components/result_block.cr b/src/spectator/formatting/components/result_block.cr index 36eb1a6..6d89f3f 100644 --- a/src/spectator/formatting/components/result_block.cr +++ b/src/spectator/formatting/components/result_block.cr @@ -68,7 +68,8 @@ module Spectator::Formatting::Components end end - # Produces the (example) location line. + # Produces the location line. + # This is where the result was determined. private def location_line(io) location = if (result = @example.result).responds_to?(:location) result.location From 6c6dff363b8e80a704d34abf3f0db4697e3c2839 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 16:25:32 -0600 Subject: [PATCH 346/399] Track source location of pending result --- src/spectator/dsl/expectations.cr | 8 ++++---- src/spectator/example_pending.cr | 8 ++++++++ src/spectator/harness.cr | 2 +- src/spectator/pending_result.cr | 7 ++++++- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/spectator/dsl/expectations.cr b/src/spectator/dsl/expectations.cr index b8a18e4..c42d272 100644 --- a/src/spectator/dsl/expectations.cr +++ b/src/spectator/dsl/expectations.cr @@ -17,14 +17,14 @@ module Spectator::DSL # Mark the current test as pending and immediately abort. # A reason can be specified with *message*. - def pending(message = PendingResult::DEFAULT_REASON) - raise ExamplePending.new(message) + def pending(message = PendingResult::DEFAULT_REASON, *, _file = __FILE__, _line = __LINE__) + raise ExamplePending.new(Location.new(_file, _line), message) end # Mark the current test as skipped and immediately abort. # A reason can be specified with *message*. - def skip(message = PendingResult::DEFAULT_REASON) - raise ExamplePending.new(message) + def skip(message = PendingResult::DEFAULT_REASON, *, _file = __FILE__, _line = __LINE__) + raise ExamplePending.new(Location.new(_file, _line), message) end # Starts an expectation. diff --git a/src/spectator/example_pending.cr b/src/spectator/example_pending.cr index 6099fed..16c0709 100644 --- a/src/spectator/example_pending.cr +++ b/src/spectator/example_pending.cr @@ -2,5 +2,13 @@ module Spectator # Exception that indicates an example is pending and should be skipped. # When raised within a test, the test should abort. class ExamplePending < Exception + # Location of where the example was aborted. + getter location : Location? + + # Creates the exception. + # Specify *location* to where the example was aborted. + def initialize(@location : Location? = nil, message : String? = nil, cause : Exception? = nil) + super(message, cause) + end end end diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 47e2b0f..8034262 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -123,7 +123,7 @@ module Spectator when ExampleFailed FailResult.new(elapsed, error, @expectations) when ExamplePending - PendingResult.new(error.message || PendingResult::DEFAULT_REASON, elapsed, @expectations) + PendingResult.new(error.message || PendingResult::DEFAULT_REASON, error.location, elapsed, @expectations) else ErrorResult.new(elapsed, error, @expectations) end diff --git a/src/spectator/pending_result.cr b/src/spectator/pending_result.cr index 0854fdc..29b0fbc 100644 --- a/src/spectator/pending_result.cr +++ b/src/spectator/pending_result.cr @@ -10,10 +10,15 @@ module Spectator # Reason the example was skipped or marked pending. getter reason : String + # Location the pending result was triggered from. + getter location : Location? + # Creates the result. # *elapsed* is the length of time it took to run the example. # A *reason* for the skip/pending result can be specified. - def initialize(@reason = DEFAULT_REASON, elapsed = Time::Span::ZERO, expectations = [] of Expectation) + # If the pending result was triggered inside of an example, then *location* can be set. + def initialize(@reason = DEFAULT_REASON, @location = nil, + elapsed = Time::Span::ZERO, expectations = [] of Expectation) super(elapsed, expectations) end From e316dd8a117b5a9bd2253eb8745d57a9d08a6bdc Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 16:27:38 -0600 Subject: [PATCH 347/399] Fix missing example location in output --- src/spectator/formatting/components/result_block.cr | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/spectator/formatting/components/result_block.cr b/src/spectator/formatting/components/result_block.cr index 6d89f3f..51f013d 100644 --- a/src/spectator/formatting/components/result_block.cr +++ b/src/spectator/formatting/components/result_block.cr @@ -73,9 +73,8 @@ module Spectator::Formatting::Components private def location_line(io) location = if (result = @example.result).responds_to?(:location) result.location - else - @example.location? end + location ||= @example.location? return unless location line(io) { io << Comment.colorize(location) } From 52a0ae938a79914d143d51dafd2d6064f805aa3b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 16:34:15 -0600 Subject: [PATCH 348/399] Consistency with location and location? --- src/spectator/fail_result.cr | 9 ++++++++- src/spectator/formatting/components/result_block.cr | 4 ++-- src/spectator/pending_result.cr | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/spectator/fail_result.cr b/src/spectator/fail_result.cr index 894defa..5e69a3d 100644 --- a/src/spectator/fail_result.cr +++ b/src/spectator/fail_result.cr @@ -41,12 +41,19 @@ module Spectator # Attempts to retrieve the location where the example failed. # This only works if the location of the failed expectation was reported. # If available, returns a `Location`, otherwise `nil`. - def location : Location? + def location? : Location? return unless error = @error.as?(ExampleFailed) error.location? end + # Attempts to retrieve the location where the example failed. + # This only works if the location of the failed expectation was reported. + # If available, returns a `Location`, otherwise raises `NilAssertionError`. + def location : Location + location? || raise(NilAssertionError.new("Source location of result unavailable")) + end + # One-word description of the result. def to_s(io) io << "fail" diff --git a/src/spectator/formatting/components/result_block.cr b/src/spectator/formatting/components/result_block.cr index 51f013d..bf4b3d2 100644 --- a/src/spectator/formatting/components/result_block.cr +++ b/src/spectator/formatting/components/result_block.cr @@ -71,8 +71,8 @@ module Spectator::Formatting::Components # Produces the location line. # This is where the result was determined. private def location_line(io) - location = if (result = @example.result).responds_to?(:location) - result.location + location = if (result = @example.result).responds_to?(:location?) + result.location? end location ||= @example.location? return unless location diff --git a/src/spectator/pending_result.cr b/src/spectator/pending_result.cr index 29b0fbc..afb5c9c 100644 --- a/src/spectator/pending_result.cr +++ b/src/spectator/pending_result.cr @@ -11,7 +11,7 @@ module Spectator getter reason : String # Location the pending result was triggered from. - getter location : Location? + getter! location : Location # Creates the result. # *elapsed* is the length of time it took to run the example. From eda4328a92efe5125a64593104e4f667afab4125 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 16:35:35 -0600 Subject: [PATCH 349/399] Blank line after stack trace --- src/spectator/formatting/components/error_result_block.cr | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/spectator/formatting/components/error_result_block.cr b/src/spectator/formatting/components/error_result_block.cr index 56d25e2..353c096 100644 --- a/src/spectator/formatting/components/error_result_block.cr +++ b/src/spectator/formatting/components/error_result_block.cr @@ -39,6 +39,8 @@ module Spectator::Formatting::Components if backtrace = error.backtrace? indent { write_backtrace(io, backtrace) } end + + io.puts end # Display just the error type. From e60cc2a447896ba02059b6197d511e1540ef1530 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 16:41:49 -0600 Subject: [PATCH 350/399] Phrasing --- src/spectator/fail_result.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/fail_result.cr b/src/spectator/fail_result.cr index 5e69a3d..36ea8fb 100644 --- a/src/spectator/fail_result.cr +++ b/src/spectator/fail_result.cr @@ -51,7 +51,7 @@ module Spectator # This only works if the location of the failed expectation was reported. # If available, returns a `Location`, otherwise raises `NilAssertionError`. def location : Location - location? || raise(NilAssertionError.new("Source location of result unavailable")) + location? || raise(NilAssertionError.new("Source location of failure unavailable")) end # One-word description of the result. From 2b37d34f262c4721e08c015a689f0e5d9281393a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 16:55:27 -0600 Subject: [PATCH 351/399] Reference issues --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3435d8b..2f70cf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,11 +17,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Tags can be added to examples and example groups. - Add matcher to check compiled type of values. - Examples can be skipped by using a `:pending` tag. A reason method can be specified: `pending: "Some excuse"` -- Examples missing a block are marked as pending. [#37](https://gitlab.com/arctic-fox/spectator/-/issues/37) -- Examples can be skipped during execution by using `skip` or `pending` in the example block. +- Examples without a test block are marked as pending. [#37](https://gitlab.com/arctic-fox/spectator/-/issues/37) +- Examples can be skipped during execution by using `skip` or `pending` in the example block. [#17](https://gitlab.com/arctic-fox/spectator/-/issues/17) - Sample blocks can be temporarily skipped by using `xsample` or `xrandom_sample`. - Add `before_suite` and `after_suite` hooks. [#21](https://gitlab.com/arctic-fox/spectator/-/issues/21) - Support defining hooks in `Spectator.configure` block. [#21](https://gitlab.com/arctic-fox/spectator/-/issues/21) +- Examples with failures or skipped during execution will report the location of that result. [#57](https://gitlab.com/arctic-fox/spectator/-/issues/57) ### Changed - `around_each` hooks wrap `before_all` and `after_all` hooks. [#12](https://github.com/icy-arctic-fox/spectator/issues/12) From 0c4379c731feb1bbecab081e78dc47bd76c8260d Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 16:55:43 -0600 Subject: [PATCH 352/399] Formatting --- src/spectator/pending_result.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/pending_result.cr b/src/spectator/pending_result.cr index afb5c9c..03700d9 100644 --- a/src/spectator/pending_result.cr +++ b/src/spectator/pending_result.cr @@ -18,7 +18,7 @@ module Spectator # A *reason* for the skip/pending result can be specified. # If the pending result was triggered inside of an example, then *location* can be set. def initialize(@reason = DEFAULT_REASON, @location = nil, - elapsed = Time::Span::ZERO, expectations = [] of Expectation) + elapsed = Time::Span::ZERO, expectations = [] of Expectation) super(elapsed, expectations) end From e8413db33f8f9f3d44ac2729431a6d3e19449194 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 17:42:25 -0600 Subject: [PATCH 353/399] Support custom messages for failed expectations Fixes https://gitlab.com/arctic-fox/spectator/-/issues/28 --- CHANGELOG.md | 1 + spec/custom_message_spec.cr | 31 +++++++++++++++++++ src/spectator/expectation.cr | 60 +++++++++++++++++++++--------------- src/spectator/should.cr | 46 +++++++++++++++------------ 4 files changed, 94 insertions(+), 44 deletions(-) create mode 100644 spec/custom_message_spec.cr diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f70cf5..63ed0f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `before_suite` and `after_suite` hooks. [#21](https://gitlab.com/arctic-fox/spectator/-/issues/21) - Support defining hooks in `Spectator.configure` block. [#21](https://gitlab.com/arctic-fox/spectator/-/issues/21) - Examples with failures or skipped during execution will report the location of that result. [#57](https://gitlab.com/arctic-fox/spectator/-/issues/57) +- Support custom messages for failed expectations. [#28](https://gitlab.com/arctic-fox/spectator/-/issues/28) ### Changed - `around_each` hooks wrap `before_all` and `after_all` hooks. [#12](https://github.com/icy-arctic-fox/spectator/issues/12) diff --git a/spec/custom_message_spec.cr b/spec/custom_message_spec.cr new file mode 100644 index 0000000..64ca32b --- /dev/null +++ b/spec/custom_message_spec.cr @@ -0,0 +1,31 @@ +require "./spec_helper" + +Spectator.describe Spectator do + it "supports custom expectation messages" do + expect do + expect(false).to be_true, "paradox!" + end.to raise_error(Spectator::ExampleFailed, "paradox!") + end + + it "supports custom expectation messages with a proc" do + count = 0 + expect do + expect(false).to be_true, ->{ count += 1; "Failed #{count} times" } + end.to raise_error(Spectator::ExampleFailed, "Failed 1 times") + end + + context "not_to" do + it "supports custom expectation messages" do + expect do + expect(true).not_to be_true, "paradox!" + end.to raise_error(Spectator::ExampleFailed, "paradox!") + end + + it "supports custom expectation messages with a proc" do + count = 0 + expect do + expect(true).not_to be_true, ->{ count += 1; "Failed #{count} times" } + end.to raise_error(Spectator::ExampleFailed, "Failed 1 times") + end + end +end diff --git a/src/spectator/expectation.cr b/src/spectator/expectation.cr index cc376b6..adb2abb 100644 --- a/src/spectator/expectation.cr +++ b/src/spectator/expectation.cr @@ -24,7 +24,13 @@ module Spectator # If nil, then the match was successful. def failure_message? - @match_data.as?(Matchers::FailedMatchData).try(&.failure_message) + return unless match_data = @match_data.as?(Matchers::FailedMatchData) + + case message = @message + when String then message + when Proc(String) then @message = message.call # Cache result of call. + else match_data.failure_message + end end # Description of why the match failed. @@ -50,7 +56,9 @@ module Spectator # Creates the expectation. # The *match_data* comes from the result of calling `Matcher#match`. # The *location* is the location of the expectation in source code, if available. - def initialize(@match_data : Matchers::MatchData, @location : Location? = nil) + # A custom *message* can be used in case of a failure. + def initialize(@match_data : Matchers::MatchData, @location : Location? = nil, + @message : String? | Proc(String) = nil) end # Creates the JSON representation of the expectation. @@ -92,9 +100,10 @@ module Spectator end # Asserts that some criteria defined by the matcher is satisfied. - def to(matcher) : Nil + # Allows a custom message to be used. + def to(matcher, message = nil) : Nil match_data = matcher.match(@expression) - report(match_data) + report(match_data, message) end def to(stub : Mocks::MethodStub) : Nil @@ -110,9 +119,16 @@ module Spectator # Asserts that some criteria defined by the matcher is not satisfied. # This is effectively the opposite of `#to`. - def to_not(matcher) : Nil + # Allows a custom message to be used. + def to_not(matcher, message = nil) : Nil match_data = matcher.negated_match(@expression) - report(match_data) + report(match_data, message) + end + + # :ditto: + @[AlwaysInline] + def not_to(matcher, message = nil) : Nil + to_not(matcher, message) end def to_not(stub : Mocks::MethodStub) : Nil @@ -125,16 +141,11 @@ module Spectator stubs.each { |stub| to_not(stub) } end - # :ditto: - @[AlwaysInline] - def not_to(matcher) : Nil - to_not(matcher) - end - # Asserts that some criteria defined by the matcher is eventually satisfied. # The expectation is checked after the example finishes and all hooks have run. - def to_eventually(matcher) : Nil - Harness.current.defer { to(matcher) } + # Allows a custom message to be used. + def to_eventually(matcher, message = nil) : Nil + Harness.current.defer { to(matcher, message) } end def to_eventually(stub : Mocks::MethodStub) : Nil @@ -147,8 +158,15 @@ module Spectator # Asserts that some criteria defined by the matcher is never satisfied. # The expectation is checked after the example finishes and all hooks have run. - def to_never(matcher) : Nil - Harness.current.defer { to_not(matcher) } + # Allows a custom message to be used. + def to_never(matcher, message = nil) : Nil + Harness.current.defer { to_not(matcher, message) } + end + + # :ditto: + @[AlwaysInline] + def never_to(matcher, message = nil) : Nil + to_never(matcher, message) end def to_never(stub : Mocks::MethodStub) : Nil @@ -159,15 +177,9 @@ module Spectator to_not(stub) end - # :ditto: - @[AlwaysInline] - def never_to(matcher) : Nil - to_never(matcher) - end - # Reports an expectation to the current harness. - private def report(match_data : Matchers::MatchData) - expectation = Expectation.new(match_data, @location) + private def report(match_data : Matchers::MatchData, message : String? | Proc(String) = nil) + expectation = Expectation.new(match_data, @location, message) Harness.current.report(expectation) end end diff --git a/src/spectator/should.cr b/src/spectator/should.cr index 7ebcf84..71e5574 100644 --- a/src/spectator/should.cr +++ b/src/spectator/should.cr @@ -9,6 +9,12 @@ class Object # end # ``` # + # An optional message can be used in case the expectation fails. + # It can be a string or proc returning a string. + # ``` + # subject.should_not be_nil, "Shouldn't be nil" + # ``` + # # NOTE: By default, the should-syntax is disabled. # The expect-syntax is preferred, # since it doesn't [monkey-patch](https://en.wikipedia.org/wiki/Monkey_patch) all objects. @@ -16,69 +22,69 @@ class Object # ``` # require "spectator/should" # ``` - def should(matcher) + def should(matcher, message = nil) actual = ::Spectator::Value.new(self) match_data = matcher.match(actual) - expectation = ::Spectator::Expectation.new(match_data) + expectation = ::Spectator::Expectation.new(match_data, message: message) ::Spectator::Harness.current.report(expectation) end # Works the same as `#should` except the condition is inverted. # When `#should` succeeds, this method will fail, and vice-versa. - def should_not(matcher) + def should_not(matcher, message = nil) actual = ::Spectator::Value.new(self) match_data = matcher.negated_match(actual) - expectation = ::Spectator::Expectation.new(match_data) + expectation = ::Spectator::Expectation.new(match_data, message: message) ::Spectator::Harness.current.report(expectation) end # Works the same as `#should` except that the condition check is postphoned. # The expectation is checked after the example finishes and all hooks have run. - def should_eventually(matcher) - ::Spectator::Harness.current.defer { should(matcher) } + def should_eventually(matcher, message = nil) + ::Spectator::Harness.current.defer { should(matcher, message) } end # Works the same as `#should_not` except that the condition check is postphoned. # The expectation is checked after the example finishes and all hooks have run. - def should_never(matcher) - ::Spectator::Harness.current.defer { should_not(matcher) } + def should_never(matcher, message = nil) + ::Spectator::Harness.current.defer { should_not(matcher, message) } end end struct Proc(*T, R) # Extension method to create an expectation for a block of code (proc). # Depending on the matcher, the proc may be executed multiple times. - def should(matcher) + def should(matcher, message = nil) actual = ::Spectator::Block.new(self) match_data = matcher.match(actual) - expectation = ::Spectator::Expectation.new(match_data) + expectation = ::Spectator::Expectation.new(match_data, message: message) ::Spectator::Harness.current.report(expectation) end # Works the same as `#should` except the condition is inverted. # When `#should` succeeds, this method will fail, and vice-versa. - def should_not(matcher) + def should_not(matcher, message = nil) actual = ::Spectator::Block.new(self) match_data = matcher.negated_match(actual) - expectation = ::Spectator::Expectation.new(match_data) + expectation = ::Spectator::Expectation.new(match_data, message: message) ::Spectator::Harness.current.report(expectation) end end module Spectator::DSL::Expectations - macro should(matcher) - expect(subject).to({{matcher}}) + macro should(*args) + expect(subject).to({{args.splat}}) end - macro should_not(matcher) - expect(subject).to_not({{matcher}}) + macro should_not(*args) + expect(subject).to_not({{args.splat}}) end - macro should_eventually(matcher) - expect(subject).to_eventually({{matcher}}) + macro should_eventually(*args) + expect(subject).to_eventually({{args.splat}}) end - macro should_never(matcher) - expect(subject).to_never({{matcher}}) + macro should_never(*args) + expect(subject).to_never({{args.splat}}) end end From ef1832721c45b7abcc643abbcdacd6081a376378 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 20 Jul 2021 18:29:26 -0600 Subject: [PATCH 354/399] Remove unecessary branch --- src/spectator/config/cli_arguments_applicator.cr | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/spectator/config/cli_arguments_applicator.cr b/src/spectator/config/cli_arguments_applicator.cr index 75b32c5..a19cba0 100644 --- a/src/spectator/config/cli_arguments_applicator.cr +++ b/src/spectator/config/cli_arguments_applicator.cr @@ -96,8 +96,6 @@ module Spectator else Log.debug { "Randomizing test order (--order rand)" } end - else - nil end end end From 9a97596b84d8c3ed6e7035b000978f385c2d7816 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 31 Jul 2021 10:15:16 -0600 Subject: [PATCH 355/399] Allow named arguments in `provided` block --- CHANGELOG.md | 1 + spec/spectator/concise_spec.cr | 55 ++++++++++++++++++++++++++++++++++ src/spectator/dsl/concise.cr | 9 ++++-- 3 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 spec/spectator/concise_spec.cr diff --git a/CHANGELOG.md b/CHANGELOG.md index 63ed0f7..6541b7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support defining hooks in `Spectator.configure` block. [#21](https://gitlab.com/arctic-fox/spectator/-/issues/21) - Examples with failures or skipped during execution will report the location of that result. [#57](https://gitlab.com/arctic-fox/spectator/-/issues/57) - Support custom messages for failed expectations. [#28](https://gitlab.com/arctic-fox/spectator/-/issues/28) +- Allow named arguments and assignments for `provided` (`given`) block. ### Changed - `around_each` hooks wrap `before_all` and `after_all` hooks. [#12](https://github.com/icy-arctic-fox/spectator/issues/12) diff --git a/spec/spectator/concise_spec.cr b/spec/spectator/concise_spec.cr new file mode 100644 index 0000000..c472a6e --- /dev/null +++ b/spec/spectator/concise_spec.cr @@ -0,0 +1,55 @@ +require "../spec_helper" + +Spectator.describe Spectator do + context "consice syntax" do + describe "provided group with a single assignment" do + provided x = 42 do + expect(x).to eq(42) + end + end + + describe "provided group with multiple assignments" do + provided x = 42, y = 123 do + expect(x).to eq(42) + expect(y).to eq(123) + end + end + + describe "provided group with a single named argument" do + provided x: 42 do + expect(x).to eq(42) + end + end + + describe "provided group with multiple named arguments" do + provided x: 42, y: 123 do + expect(x).to eq(42) + expect(y).to eq(123) + end + end + + describe "provided group with mix of assignments and named arguments" do + provided x = 42, y: 123 do + expect(x).to eq(42) + expect(y).to eq(123) + end + + provided x = 42, y = 123, z: 0, foo: "bar" do + expect(x).to eq(42) + expect(y).to eq(123) + expect(z).to eq(0) + expect(foo).to eq("bar") + end + end + + describe "provided group with references to other arguments" do + let(foo) { "bar" } + + provided x = 3, y: x * 5, baz: foo.sub('r', 'z') do + expect(x).to eq(3) + expect(y).to eq(15) + expect(baz).to eq("baz") + end + end + end +end diff --git a/src/spectator/dsl/concise.cr b/src/spectator/dsl/concise.cr index bba29f2..a7222ae 100644 --- a/src/spectator/dsl/concise.cr +++ b/src/spectator/dsl/concise.cr @@ -18,13 +18,16 @@ module Spectator::DSL # expect(x).to eq(42) # end # ``` - macro provided(*assignments, &block) + macro provided(*assignments, **kwargs, &block) {% raise "Cannot use 'provided' inside of a test block" if @def %} class Given%given < {{@type.id}} {% for assignment in assignments %} let({{assignment.target}}) { {{assignment.value}} } {% end %} + {% for name, value in kwargs %} + let({{name}}) { {{value}} } + {% end %} {% if block %} example {{block}} @@ -36,9 +39,9 @@ module Spectator::DSL # :ditto: @[Deprecated("Use `provided` instead.")] - macro given(*assignments, &block) + macro given(*assignments, **kwargs, &block) {% raise "Cannot use 'given' inside of a test block" if @def %} - provided({{assignments.splat}}) {{block}} + provided({{assignments.splat(",")}} {{kwargs.double_splat}}) {{block}} end end end From 4c125d98d45ebcf03e78f2f4ff54bf38632c6519 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 31 Jul 2021 11:56:53 -0600 Subject: [PATCH 356/399] Implement aggregate_failures --- CHANGELOG.md | 1 + spec/spectator/aggregate_failures_spec.cr | 32 ++++++++++++++++ src/spectator/dsl/expectations.cr | 15 ++++++++ src/spectator/harness.cr | 38 ++++++++++++++++++- src/spectator/multiple_expectations_failed.cr | 16 ++++++++ 5 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 spec/spectator/aggregate_failures_spec.cr create mode 100644 src/spectator/multiple_expectations_failed.cr diff --git a/CHANGELOG.md b/CHANGELOG.md index 6541b7b..3acacc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Examples with failures or skipped during execution will report the location of that result. [#57](https://gitlab.com/arctic-fox/spectator/-/issues/57) - Support custom messages for failed expectations. [#28](https://gitlab.com/arctic-fox/spectator/-/issues/28) - Allow named arguments and assignments for `provided` (`given`) block. +- Add `aggregate_failures` to capture and report multiple failed expectations. [#24](https://gitlab.com/arctic-fox/spectator/-/issues/24) ### Changed - `around_each` hooks wrap `before_all` and `after_all` hooks. [#12](https://github.com/icy-arctic-fox/spectator/issues/12) diff --git a/spec/spectator/aggregate_failures_spec.cr b/spec/spectator/aggregate_failures_spec.cr new file mode 100644 index 0000000..5fa5074 --- /dev/null +++ b/spec/spectator/aggregate_failures_spec.cr @@ -0,0 +1,32 @@ +require "../spec_helper" + +Spectator.describe Spectator do + describe "aggregate_failures" do + it "captures multiple failed expectations" do + expect do + aggregate_failures do + expect(true).to be_false + expect(false).to be_true + end + end.to raise_error(Spectator::MultipleExpectationsFailed, /2 failures/) + end + + it "raises normally for one failed expectation" do + expect do + aggregate_failures do + expect(true).to be_false + expect(true).to be_true + end + end.to raise_error(Spectator::ExpectationFailed) + end + + it "doesn't raise when there are no failed expectations" do + expect do + aggregate_failures do + expect(false).to be_false + expect(true).to be_true + end + end.to_not raise_error(Spectator::ExpectationFailed) + end + end +end diff --git a/src/spectator/dsl/expectations.cr b/src/spectator/dsl/expectations.cr index c42d272..7a228a1 100644 --- a/src/spectator/dsl/expectations.cr +++ b/src/spectator/dsl/expectations.cr @@ -162,5 +162,20 @@ module Spectator::DSL macro is_not(expected) expect(subject).not_to(eq({{expected}})) end + + # Captures multiple possible failures. + # Aborts after the block completes if there were any failed expectations in the block. + # + # ``` + # aggregate_failures do + # expect(true).to be_false + # expect(false).to be_true + # end + # ``` + def aggregate_failures + ::Spectator::Harness.current.aggregate_failures do + yield + end + end end end diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 8034262..100b4ac 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -2,7 +2,9 @@ require "./error_result" require "./example_failed" require "./example_pending" require "./expectation" +require "./expectation_failed" require "./mocks" +require "./multiple_expectations_failed" require "./pass_result" require "./result" @@ -66,6 +68,7 @@ module Spectator @deferred = Deque(->).new @expectations = [] of Expectation + @aggregate : Array(Expectation)? = nil # Runs test code and produces a result based on the outcome. # The test code should be called from within the block given to this method. @@ -75,14 +78,20 @@ module Spectator translate(elapsed + elapsed2, error || error2) end - def report(expectation : Expectation) : Nil + def report(expectation : Expectation) : Bool Log.debug { "Reporting expectation #{expectation}" } @expectations << expectation # TODO: Move this out of harness, maybe to `Example`. Example.current.name = expectation.description unless Example.current.name? - raise ExpectationFailed.new(expectation, expectation.failure_message) if expectation.failed? + if expectation.failed? + raise ExpectationFailed.new(expectation, expectation.failure_message) unless (aggregate = @aggregate) + aggregate << expectation + false + else + true + end end # Stores a block of code to be executed later. @@ -91,6 +100,31 @@ module Spectator @deferred << block end + def aggregate_failures + previous = @aggregate + @aggregate = aggregate = [] of Expectation + begin + yield.tap do + # If there's an nested aggregate (for some reason), allow the top-level one to handle things. + check_aggregate(aggregate) unless previous + end + ensure + @aggregate = previous + end + end + + private def check_aggregate(aggregate) + failures = aggregate.select(&.failed?) + case failures.size + when 0 then return + when 1 + expectation = failures.first + raise ExpectationFailed.new(expectation, expectation.failure_message) + else + raise MultipleExpectationsFailed.new(failures, "Got #{failures.size} failures from failure aggregation block") + end + end + # Yields to run the test code and returns information about the outcome. # Returns a tuple with the elapsed time and an error if one occurred (otherwise nil). private def capture : Tuple(Time::Span, Exception?) diff --git a/src/spectator/multiple_expectations_failed.cr b/src/spectator/multiple_expectations_failed.cr new file mode 100644 index 0000000..9a911fa --- /dev/null +++ b/src/spectator/multiple_expectations_failed.cr @@ -0,0 +1,16 @@ +require "./example_failed" +require "./expectation" + +module Spectator + # Exception that indicates more than one expectation from a test failed. + # When raised within a test, the test should abort. + class MultipleExpectationsFailed < ExampleFailed + # Expectations that failed. + getter expectations : Array(Expectation) + + # Creates the exception. + def initialize(@expectations : Array(Expectation), message : String? = nil, cause : Exception? = nil) + super(nil, message, cause) + end + end +end From f53ffabf6b35b5ad1998c1a9e3eef62e81c205c1 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 31 Jul 2021 12:04:43 -0600 Subject: [PATCH 357/399] Support label for aggregate_failures block --- spec/spectator/aggregate_failures_spec.cr | 9 +++++++++ src/spectator/dsl/expectations.cr | 4 ++-- src/spectator/harness.cr | 10 ++++++---- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/spec/spectator/aggregate_failures_spec.cr b/spec/spectator/aggregate_failures_spec.cr index 5fa5074..0c4506c 100644 --- a/spec/spectator/aggregate_failures_spec.cr +++ b/spec/spectator/aggregate_failures_spec.cr @@ -28,5 +28,14 @@ Spectator.describe Spectator do end end.to_not raise_error(Spectator::ExpectationFailed) end + + it "supports naming the block" do + expect do + aggregate_failures "contradiction" do + expect(true).to be_false + expect(false).to be_true + end + end.to raise_error(Spectator::MultipleExpectationsFailed, /contradiction/) + end end end diff --git a/src/spectator/dsl/expectations.cr b/src/spectator/dsl/expectations.cr index 7a228a1..d4dc8d8 100644 --- a/src/spectator/dsl/expectations.cr +++ b/src/spectator/dsl/expectations.cr @@ -172,8 +172,8 @@ module Spectator::DSL # expect(false).to be_true # end # ``` - def aggregate_failures - ::Spectator::Harness.current.aggregate_failures do + def aggregate_failures(label = nil) + ::Spectator::Harness.current.aggregate_failures(label) do yield end end diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 100b4ac..3e0962b 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -100,20 +100,20 @@ module Spectator @deferred << block end - def aggregate_failures + def aggregate_failures(label = nil) previous = @aggregate @aggregate = aggregate = [] of Expectation begin yield.tap do # If there's an nested aggregate (for some reason), allow the top-level one to handle things. - check_aggregate(aggregate) unless previous + check_aggregate(aggregate, label) unless previous end ensure @aggregate = previous end end - private def check_aggregate(aggregate) + private def check_aggregate(aggregate, label) failures = aggregate.select(&.failed?) case failures.size when 0 then return @@ -121,7 +121,9 @@ module Spectator expectation = failures.first raise ExpectationFailed.new(expectation, expectation.failure_message) else - raise MultipleExpectationsFailed.new(failures, "Got #{failures.size} failures from failure aggregation block") + message = "Got #{failures.size} failures from failure aggregation block" + message += " \"#{label}\"" if label + raise MultipleExpectationsFailed.new(failures, message) end end From 868aa1d00a5cb76f543c2ce4761df462f8db53fb Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 31 Jul 2021 14:16:39 -0600 Subject: [PATCH 358/399] Support custom handling of hooks --- src/spectator/dsl/hooks.cr | 82 +++++++++++++++++++++++++++++++------- 1 file changed, 67 insertions(+), 15 deletions(-) diff --git a/src/spectator/dsl/hooks.cr b/src/spectator/dsl/hooks.cr index 1e990fd..d11ff08 100644 --- a/src/spectator/dsl/hooks.cr +++ b/src/spectator/dsl/hooks.cr @@ -6,42 +6,94 @@ module Spectator::DSL module Hooks # Defines a macro to create an example group hook. # The *type* indicates when the hook runs and must be a method on `Spectator::DSL::Builder`. - macro define_example_group_hook(type) - macro {{type.id}}(&block) - \{% raise "Missing block for '{{type.id}}' hook" unless block %} - \{% raise "Cannot use '{{type.id}}' inside of a test block" if @def %} + # A custom *name* can be used for the hook method. + # If not provided, *type* will be used instead. + # Additionally, a block can be provided. + # The block can perform any operations necessary and yield to invoke the end-user hook. + macro define_example_group_hook(type, name = nil, &block) + macro {{(name ||= type).id}}(&block) + \{% raise "Missing block for '{{name.id}}' hook" unless block %} + \{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %} private def self.\%hook : Nil \{{block.body}} end + {% if block %} + private def self.%wrapper : Nil + {{block.body}} + end + {% end %} + ::Spectator::DSL::Builder.{{type.id}}( ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}) - ) { \%hook } + ) do + {% if block %} + %wrapper do |*args| + \{% if block.args.empty? %} + \%hook + \{% else %} + \%hook(*args) + \{% end %} + end + {% else %} + \%hook + {% end %} + end end end # Defines a macro to create an example hook. # The *type* indicates when the hook runs and must be a method on `Spectator::DSL::Builder`. - macro define_example_hook(type) - macro {{type.id}}(&block) - \{% raise "Missing block for '{{type.id}}' hook" unless block %} - \{% raise "Block argument count '{{type.id}}' hook must be 0..1" if block.args.size > 1 %} - \{% raise "Cannot use '{{type.id}}' inside of a test block" if @def %} + # A custom *name* can be used for the hook method. + # If not provided, *type* will be used instead. + # Additionally, a block can be provided that takes the current example as an argument. + # The block can perform any operations necessary and yield to invoke the end-user hook. + macro define_example_hook(type, name = nil, &block) + macro {{(name ||= type).id}}(&block) + \{% raise "Missing block for '{{name.id}}' hook" unless block %} + \{% raise "Block argument count '{{name.id}}' hook must be 0..1" if block.args.size > 1 %} + \{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %} private def \%hook(\{{block.args.splat}}) : Nil \{{block.body}} end + {% if block %} + private def %wrapper({{block.args.splat}}) : Nil + {{block.body}} + end + {% end %} + ::Spectator::DSL::Builder.{{type.id}}( ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}) ) do |example| example.with_context(\{{@type.name}}) do - \{% if block.args.empty? %} - \%hook - \{% else %} - \%hook(example) - \{% end %} + {% if block %} + {% if block.args.empty? %} + %wrapper do |*args| + \{% if block.args.empty? %} + \%hook + \{% else %} + \%hook(*args) + \{% end %} + end + {% else %} + %wrapper(example) do |*args| + \{% if block.args.empty? %} + \%hook + \{% else %} + \%hook(*args) + \{% end %} + end + {% end %} + {% else %} + \{% if block.args.empty? %} + \%hook + \{% else %} + \%hook(example) + \{% end %} + {% end %} end end end From abe78410c4014fd2ed0604a957e87609749b2bf7 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 31 Jul 2021 14:18:59 -0600 Subject: [PATCH 359/399] Formatting --- src/spectator/dsl/expectations.cr | 4 ++-- src/spectator/expectation.cr | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/spectator/dsl/expectations.cr b/src/spectator/dsl/expectations.cr index d4dc8d8..0878dda 100644 --- a/src/spectator/dsl/expectations.cr +++ b/src/spectator/dsl/expectations.cr @@ -168,8 +168,8 @@ module Spectator::DSL # # ``` # aggregate_failures do - # expect(true).to be_false - # expect(false).to be_true + # expect(true).to be_false + # expect(false).to be_true # end # ``` def aggregate_failures(label = nil) diff --git a/src/spectator/expectation.cr b/src/spectator/expectation.cr index adb2abb..ff9dc57 100644 --- a/src/spectator/expectation.cr +++ b/src/spectator/expectation.cr @@ -27,9 +27,9 @@ module Spectator return unless match_data = @match_data.as?(Matchers::FailedMatchData) case message = @message - when String then message + when String then message when Proc(String) then @message = message.call # Cache result of call. - else match_data.failure_message + else match_data.failure_message end end @@ -58,7 +58,7 @@ module Spectator # The *location* is the location of the expectation in source code, if available. # A custom *message* can be used in case of a failure. def initialize(@match_data : Matchers::MatchData, @location : Location? = nil, - @message : String? | Proc(String) = nil) + @message : String? | Proc(String) = nil) end # Creates the JSON representation of the expectation. From edcb5118e66c0cda3748fd7718151eb41ad3caa6 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 31 Jul 2021 15:02:21 -0600 Subject: [PATCH 360/399] Show pending count only if there are pending examples --- src/spectator/formatting/components/totals.cr | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/spectator/formatting/components/totals.cr b/src/spectator/formatting/components/totals.cr index 6071e39..bd65ead 100644 --- a/src/spectator/formatting/components/totals.cr +++ b/src/spectator/formatting/components/totals.cr @@ -38,7 +38,9 @@ module Spectator::Formatting::Components io << " (" << @errors << " errors)" end - io << ", " << @pending << " pending" + if @pending > 1 + io << ", " << @pending << " pending" + end end end end From 3dc3b88dbeb2ef3834451a5d2548db43bb603daa Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 31 Jul 2021 15:48:54 -0600 Subject: [PATCH 361/399] Add "after" hooks in reverse order to match RSpec --- src/spectator/example_group_builder.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/example_group_builder.cr b/src/spectator/example_group_builder.cr index aa59626..155c3f1 100644 --- a/src/spectator/example_group_builder.cr +++ b/src/spectator/example_group_builder.cr @@ -102,8 +102,8 @@ module Spectator private def apply_hooks(group) @before_all_hooks.each { |hook| group.add_before_all_hook(hook) } @before_each_hooks.each { |hook| group.add_before_each_hook(hook) } - @after_all_hooks.each { |hook| group.add_after_all_hook(hook) } - @after_each_hooks.each { |hook| group.add_after_each_hook(hook) } + @after_all_hooks.each { |hook| group.prepend_after_all_hook(hook) } + @after_each_hooks.each { |hook| group.prepend_after_each_hook(hook) } @around_each_hooks.each { |hook| group.add_around_each_hook(hook) } end end From 10b652f4b50f4420db8636a3da4358410a6a36af Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 31 Jul 2021 19:11:51 -0600 Subject: [PATCH 362/399] Remove unique/temp names --- src/spectator/events.cr | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/spectator/events.cr b/src/spectator/events.cr index 12f81a6..e77c61e 100644 --- a/src/spectator/events.cr +++ b/src/spectator/events.cr @@ -25,18 +25,18 @@ module Spectator # end # ``` private macro group_event(name, &block) - @%hooks = [] of ExampleGroupHook - @%called = Atomic::Flag.new + @{{name.id}}_hooks = [] of ExampleGroupHook + @{{name.id}}_called = Atomic::Flag.new # Adds a hook to be invoked when the *{{name.id}}* event occurs. def add_{{name.id}}_hook(hook : ExampleGroupHook) : Nil - @%hooks << hook + @{{name.id}}_hooks << hook end # Adds a hook to be invoked when the *{{name.id}}* event occurs. # The hook is added to the front of the list. def prepend_{{name.id}}_hook(hook : ExampleGroupHook) : Nil - @%hooks.unshift(hook) + @{{name.id}}_hooks.unshift(hook) end # Defines a hook for the *{{name.id}}* event. @@ -49,20 +49,20 @@ module Spectator # Signals that the *{{name.id}}* event has occurred. # All hooks associated with the event will be called. def call_{{name.id}} : Nil - %block(@%hooks) + handle_{{name.id}}(@{{name.id}}_hooks) end # Signals that the *{{name.id}}* event has occurred. # Only calls the hooks if the event hasn't been triggered before by this method. # Returns true if the hooks were called and false if they weren't (called previously). def call_once_{{name.id}} : Bool - first = @%called.test_and_set + first = @{{name.id}}_called.test_and_set call_{{name.id}} if first first end # Logic specific to invoking the *{{name.id}}* hook. - private def %block({{block.args.splat}}) + private def handle_{{name.id}}({{block.args.splat}}) {{block.body}} end end @@ -85,17 +85,17 @@ module Spectator # end # ``` private macro example_event(name, &block) - @%hooks = [] of ExampleHook + @{{name.id}}_hooks = [] of ExampleHook # Adds a hook to be invoked when the *{{name.id}}* event occurs. def add_{{name.id}}_hook(hook : ExampleHook) : Nil - @%hooks << hook + @{{name.id}}_hooks << hook end # Adds a hook to be invoked when the *{{name.id}}* event occurs. # The hook is added to the front of the list. def prepend_{{name.id}}_hook(hook : ExampleHook) : Nil - @%hooks.unshift(hook) + @{{name.id}}_hooks.unshift(hook) end # Defines a hook for the *{{name.id}}* event. @@ -110,11 +110,11 @@ module Spectator # All hooks associated with the event will be called. # The *example* should be the current example. def call_{{name.id}}(example : Example) : Nil - %block(@%hooks, example) + handle_{{name.id}}(@{{name.id}}_hooks, example) end # Logic specific to invoking the *{{name.id}}* hook. - private def %block({{block.args.splat}}) + private def handle_{{name.id}}({{block.args.splat}}) {{block.body}} end end From b9f0a31a4aa086ab4c2c89c70294e060431b630d Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 7 Aug 2021 21:45:49 -0600 Subject: [PATCH 363/399] Overhaul hooks Mostly cleanup and make managing hooks simpler, hopefully. Tests indicate this configuration matches hook execution order of RSpec. --- CHANGELOG.md | 2 + src/spectator/config.cr | 14 +-- src/spectator/config/builder.cr | 51 +++++---- src/spectator/events.cr | 122 -------------------- src/spectator/example.cr | 11 +- src/spectator/example_group.cr | 126 +++++--------------- src/spectator/example_group_builder.cr | 80 +++---------- src/spectator/example_group_hook.cr | 10 ++ src/spectator/example_hook.cr | 9 +- src/spectator/hooks.cr | 114 ++++++++++++++++++ src/spectator/includes.cr | 2 +- src/spectator/spec_builder.cr | 153 ++++++++++--------------- 12 files changed, 279 insertions(+), 415 deletions(-) delete mode 100644 src/spectator/events.cr create mode 100644 src/spectator/hooks.cr diff --git a/CHANGELOG.md b/CHANGELOG.md index 3acacc6..f7d70df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,9 +26,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support custom messages for failed expectations. [#28](https://gitlab.com/arctic-fox/spectator/-/issues/28) - Allow named arguments and assignments for `provided` (`given`) block. - Add `aggregate_failures` to capture and report multiple failed expectations. [#24](https://gitlab.com/arctic-fox/spectator/-/issues/24) +- Add `append_` and `prepend_` variants of hook creation methods. ### Changed - `around_each` hooks wrap `before_all` and `after_all` hooks. [#12](https://github.com/icy-arctic-fox/spectator/issues/12) +- Hook execution order has been tweaked to match RSpec. - `given` (now `provided`) blocks changed to produce a single example. `it` can no longer be nested in a `provided` block. - The "should" syntax no longer reports the source as inside Spectator. - Short-hand "should" syntax must be included by using `require "spectator/should"` - `it { should eq("foo") }` diff --git a/src/spectator/config.cr b/src/spectator/config.cr index 549c5e6..577b938 100644 --- a/src/spectator/config.cr +++ b/src/spectator/config.cr @@ -21,25 +21,25 @@ module Spectator getter example_filter : ExampleFilter # List of hooks to run before all examples in the test suite. - protected getter before_suite_hooks : Array(ExampleGroupHook) + protected getter before_suite_hooks : Deque(ExampleGroupHook) # List of hooks to run before each top-level example group. - protected getter before_all_hooks : Array(ExampleGroupHook) + protected getter before_all_hooks : Deque(ExampleGroupHook) # List of hooks to run before every example. - protected getter before_each_hooks : Array(ExampleHook) + protected getter before_each_hooks : Deque(ExampleHook) # List of hooks to run after all examples in the test suite. - protected getter after_suite_hooks : Array(ExampleGroupHook) + protected getter after_suite_hooks : Deque(ExampleGroupHook) # List of hooks to run after each top-level example group. - protected getter after_all_hooks : Array(ExampleGroupHook) + protected getter after_all_hooks : Deque(ExampleGroupHook) # List of hooks to run after every example. - protected getter after_each_hooks : Array(ExampleHook) + protected getter after_each_hooks : Deque(ExampleHook) # List of hooks to run around every example. - protected getter around_each_hooks : Array(ExampleProcsyHook) + protected getter around_each_hooks : Deque(ExampleProcsyHook) # Creates a new configuration. # Properties are pulled from *source*. diff --git a/src/spectator/config/builder.cr b/src/spectator/config/builder.cr index ff7f08a..9c73506 100644 --- a/src/spectator/config/builder.cr +++ b/src/spectator/config/builder.cr @@ -21,100 +21,107 @@ module Spectator @filters = [] of ExampleFilter # List of hooks to run before all examples in the test suite. - protected getter before_suite_hooks = [] of ExampleGroupHook + protected getter before_suite_hooks = Deque(ExampleGroupHook).new # List of hooks to run before each top-level example group. - protected getter before_all_hooks = [] of ExampleGroupHook + protected getter before_all_hooks = Deque(ExampleGroupHook).new # List of hooks to run before every example. - protected getter before_each_hooks = [] of ExampleHook + protected getter before_each_hooks = Deque(ExampleHook).new # List of hooks to run after all examples in the test suite. - protected getter after_suite_hooks = [] of ExampleGroupHook + protected getter after_suite_hooks = Deque(ExampleGroupHook).new # List of hooks to run after each top-level example group. - protected getter after_all_hooks = [] of ExampleGroupHook + protected getter after_all_hooks = Deque(ExampleGroupHook).new # List of hooks to run after every example. - protected getter after_each_hooks = [] of ExampleHook + protected getter after_each_hooks = Deque(ExampleHook).new # List of hooks to run around every example. - protected getter around_each_hooks = [] of ExampleProcsyHook + protected getter around_each_hooks = Deque(ExampleProcsyHook).new # Attaches a hook to be invoked before all examples in the test suite. def add_before_suite_hook(hook) - @before_suite_hooks << hook + @before_suite_hooks.push(hook) end # Defines a block of code to execute before all examples in the test suite. def before_suite(&block) - @before_suite_hooks << ExampleGroupHook.new(&block) + hook = ExampleGroupHook.new(&block) + add_before_suite_hook(hook) end # Attaches a hook to be invoked before each top-level example group. def add_before_all_hook(hook) - @before_all_hooks << hook + @before_all_hooks.push(hook) end # Defines a block of code to execute before each top-level example group. def before_all(&block) - @before_all_hooks << ExampleGroupHook.new(&block) + hook = ExampleGroupHook.new(&block) + add_before_all_hook(hook) end # Attaches a hook to be invoked before every example. # The current example is provided as a block argument. def add_before_each_hook(hook) - @before_each_hooks << hook + @before_each_hooks.push(hook) end # Defines a block of code to execute before every. # The current example is provided as a block argument. def before_each(&block : Example -> _) - @before_each_hooks << ExampleHook.new(&block) + hook = ExampleHook.new(&block) + add_before_each_hook(hook) end # Attaches a hook to be invoked after all examples in the test suite. def add_after_suite_hook(hook) - @after_suite_hooks << hook + @after_suite_hooks.unshift(hook) end # Defines a block of code to execute after all examples in the test suite. def after_suite(&block) - @after_suite_hooks << ExampleGroupHook.new(&block) + hook = ExampleGroupHook.new(&block) + add_after_suite_hook(hook) end # Attaches a hook to be invoked after each top-level example group. def add_after_all_hook(hook) - @after_all_hooks << hook + @after_all_hooks.unshift(hook) end # Defines a block of code to execute after each top-level example group. def after_all(&block) - @after_all_hooks << ExampleGroupHook.new(&block) + hook = ExampleGroupHook.new(&block) + add_after_all_hook(hook) end # Attaches a hook to be invoked after every example. # The current example is provided as a block argument. def add_after_each_hook(hook) - @after_each_hooks << hook + @after_each_hooks.unshift(hook) end # Defines a block of code to execute after every example. # The current example is provided as a block argument. def after_each(&block : Example -> _) - @after_each_hooks << ExampleHook.new(&block) + hook = ExampleHook.new(&block) + add_after_each_hook(hook) end # Attaches a hook to be invoked around every example. # The current example in procsy form is provided as a block argument. def add_around_each_hook(hook) - @around_each_hooks << hook + @around_each_hooks.push(hook) end # Defines a block of code to execute around every example. # The current example in procsy form is provided as a block argument. - def around_each(&block : Example -> _) - @around_each_hooks << ExampleProcsyHook.new(label: "around_each", &block) + def around_each(&block : Example::Procsy -> _) + hook = ExampleProcsyHook.new(label: "around_each", &block) + add_around_each_hook(hook) end # Creates a configuration. diff --git a/src/spectator/events.cr b/src/spectator/events.cr deleted file mode 100644 index e77c61e..0000000 --- a/src/spectator/events.cr +++ /dev/null @@ -1,122 +0,0 @@ -require "./example_group_hook" -require "./example_hook" - -module Spectator - # Mix-in for managing events and hooks. - # This module is intended to be included by `ExampleGroup`. - module Events - # Defines an event for an example group. - # This event typically runs before or after an example group finishes. - # No contextual information (or example) is provided to the hooks. - # - # The *name* defines the name of the event. - # This must be unique across all events, not just group events. - # Four public methods are defined - two to add a hook and the others to trigger the event which calls every hook. - # One trigger method, prefixed with *call_* will always call the event hooks. - # The other trigger method, prefixed with *call_once_* will only call the event hooks on the first invocation. - # - # A block must be provided to this macro. - # The block defines the logic for invoking all of the hooks. - # A single argument is yielded to the block - the set of hooks for the event. - # - # ``` - # group_event some_hook do |hooks| - # hooks.each(&.call) - # end - # ``` - private macro group_event(name, &block) - @{{name.id}}_hooks = [] of ExampleGroupHook - @{{name.id}}_called = Atomic::Flag.new - - # Adds a hook to be invoked when the *{{name.id}}* event occurs. - def add_{{name.id}}_hook(hook : ExampleGroupHook) : Nil - @{{name.id}}_hooks << hook - end - - # Adds a hook to be invoked when the *{{name.id}}* event occurs. - # The hook is added to the front of the list. - def prepend_{{name.id}}_hook(hook : ExampleGroupHook) : Nil - @{{name.id}}_hooks.unshift(hook) - end - - # Defines a hook for the *{{name.id}}* event. - # The block of code given to this method is invoked when the event occurs. - def {{name.id}}(&block : -> _) : Nil - hook = ExampleGroupHook.new(label: {{name.stringify}}, &block) - add_{{name.id}}_hook(hook) - end - - # Signals that the *{{name.id}}* event has occurred. - # All hooks associated with the event will be called. - def call_{{name.id}} : Nil - handle_{{name.id}}(@{{name.id}}_hooks) - end - - # Signals that the *{{name.id}}* event has occurred. - # Only calls the hooks if the event hasn't been triggered before by this method. - # Returns true if the hooks were called and false if they weren't (called previously). - def call_once_{{name.id}} : Bool - first = @{{name.id}}_called.test_and_set - call_{{name.id}} if first - first - end - - # Logic specific to invoking the *{{name.id}}* hook. - private def handle_{{name.id}}({{block.args.splat}}) - {{block.body}} - end - end - - # Defines an event for an example. - # This event typically runs before or after an example finishes. - # The current example is provided to the hooks. - # - # The *name* defines the name of the event. - # This must be unique across all events. - # Three public methods are defined - two to add a hook and the other to trigger the event which calls every hook. - # - # A block must be provided to this macro. - # The block defines the logic for invoking all of the hooks. - # Two arguments are yielded to the block - the set of hooks for the event, and the current example. - # - # ``` - # example_event some_hook do |hooks, example| - # hooks.each(&.call(example)) - # end - # ``` - private macro example_event(name, &block) - @{{name.id}}_hooks = [] of ExampleHook - - # Adds a hook to be invoked when the *{{name.id}}* event occurs. - def add_{{name.id}}_hook(hook : ExampleHook) : Nil - @{{name.id}}_hooks << hook - end - - # Adds a hook to be invoked when the *{{name.id}}* event occurs. - # The hook is added to the front of the list. - def prepend_{{name.id}}_hook(hook : ExampleHook) : Nil - @{{name.id}}_hooks.unshift(hook) - end - - # Defines a hook for the *{{name.id}}* event. - # The block of code given to this method is invoked when the event occurs. - # The current example is provided as a block argument. - def {{name.id}}(&block : Example ->) : Nil - hook = ExampleHook.new(label: {{name.stringify}}, &block) - add_{{name.id}}_hook(hook) - end - - # Signals that the *{{name.id}}* event has occurred. - # All hooks associated with the event will be called. - # The *example* should be the current example. - def call_{{name.id}}(example : Example) : Nil - handle_{{name.id}}(@{{name.id}}_hooks, example) - end - - # Logic specific to invoking the *{{name.id}}* hook. - private def handle_{{name.id}}({{block.args.splat}}) - {{block.body}} - end - end - end -end diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 513cca8..eacf2e1 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -98,14 +98,14 @@ module Spectator begin @result = Harness.run do - @group.try(&.call_once_before_all) + @group.try(&.call_before_all) if (parent = @group) - parent.call_around_each(self) { run_internal } + parent.call_around_each(procsy).call else run_internal end if (parent = @group) - parent.call_once_after_all if parent.finished? + parent.call_after_all if parent.finished? end end ensure @@ -191,6 +191,11 @@ module Spectator end end + # Creates a procsy from this example that runs the example. + def procsy + Procsy.new(self) { run_internal } + end + # Creates a procsy from this example and the provided block. def procsy(&block : ->) Procsy.new(self, &block) diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index fb3624f..4edd2e7 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -1,12 +1,12 @@ -require "./events" require "./example_procsy_hook" +require "./hooks" require "./node" module Spectator # Collection of examples and sub-groups. class ExampleGroup < Node include Enumerable(Node) - include Events + include Hooks include Iterable(Node) @nodes = [] of Node @@ -19,66 +19,44 @@ module Spectator # `ExampleGroup` manages the association of nodes to groups. protected setter group : ExampleGroup? - # Calls all hooks from the parent group if there is a parent. - # The *hook* is the method name of the group hook to invoke. - private macro call_parent_hooks(hook) - if (parent = @group) - parent.{{hook.id}} - end - end - - # Calls all hooks from the parent group if there is a parent. - # The *hook* is the method name of the example hook to invoke. - # The current *example* must be provided. - private macro call_parent_hooks(hook, example) - if (parent = @group) - parent.{{hook.id}}({{example}}) - end - end - - # Calls group hooks of the current group. - private def call_hooks(hooks) - hooks.each do |hook| - Log.trace { "Invoking hook #{hook}" } - hook.call - end - end - - # Calls example hooks of the current group. - # Requires the current *example*. - private def call_hooks(hooks, example) - hooks.each do |hook| - Log.trace { "Invoking hook #{hook}" } - hook.call(example) - end - end - - group_event before_all do |hooks| + define_hook before_all : ExampleGroupHook do Log.trace { "Processing before_all hooks for #{self}" } - call_parent_hooks(:call_once_before_all) - call_hooks(hooks) + @group.try &.call_before_all + before_all_hooks.each &.call_once end - group_event after_all do |hooks| + define_hook after_all : ExampleGroupHook do Log.trace { "Processing after_all hooks for #{self}" } - call_hooks(hooks) - call_parent_hooks(:call_once_after_all) if @group.try(&.finished?) + after_all_hooks.each &.call_once if finished? + if group = @group + group.call_after_all if group.finished? + end end - example_event before_each do |hooks, example| + define_hook before_each : ExampleHook do |example| Log.trace { "Processing before_each hooks for #{self}" } - call_parent_hooks(:call_before_each, example) - call_hooks(hooks, example) + @group.try &.call_before_each(example) + before_each_hooks.each &.call(example) end - example_event after_each do |hooks, example| + define_hook after_each : ExampleHook do |example| Log.trace { "Processing after_each hooks for #{self}" } - call_hooks(hooks, example) - call_parent_hooks(:call_after_each, example) + after_each_hooks.each &.call(example) + @group.try &.call_after_each(example) + end + + define_hook around_each : ExampleProcsyHook do |procsy| + Log.trace { "Processing around_each hooks for #{self}" } + + around_each_hooks.reverse_each { |hook| procsy = hook.wrap(procsy) } + if group = @group + procsy = group.call_around_each(procsy) + end + procsy end # Creates the example group. @@ -158,57 +136,5 @@ module Spectator @nodes << node node.group = self end - - @around_hooks = [] of ExampleProcsyHook - - # Adds a hook to be invoked when the *around_each* event occurs. - def add_around_each_hook(hook : ExampleProcsyHook) : Nil - @around_hooks << hook - end - - # Adds a hook to be invoked when the *around_each* event occurs. - # The hook is added to the front of the list. - def prepend_around_each_hook(hook : ExampleProcsyHook) : Nil - @around_hooks.unshift(hook) - end - - # Defines a hook for the *around_each* event. - # The block of code given to this method is invoked when the event occurs. - # The current example is provided as a block argument. - def around_each(&block) : Nil - hook = ExampleProcsyHook.new(label: "around_each", &block) - add_around_each_hook(hook) - end - - # Signals that the *around_each* event has occurred. - # All hooks associated with the event will be called. - def call_around_each(example, &block : -> _) : Nil - # Avoid overhead if there's no hooks. - return yield if @around_hooks.empty? - - # Start with a procsy that wraps the original code. - procsy = example.procsy(&block) - procsy = wrap_around_each(procsy) - procsy.call - end - - # Wraps a procsy with the *around_each* hooks from this example group. - # The returned procsy will call each hook then *procsy*. - protected def wrap_around_each(procsy) - # Avoid overhead if there's no hooks. - return procsy if @around_hooks.empty? - - # Wrap each hook with the next. - outer = procsy - @around_hooks.reverse_each do |hook| - outer = hook.wrap(outer) - end - - # If there's a parent, wrap the procsy with its hooks. - # Otherwise, return the outermost procsy. - return outer unless (parent = group?) - - parent.wrap_around_each(outer) - end end end diff --git a/src/spectator/example_group_builder.cr b/src/spectator/example_group_builder.cr index 155c3f1..b5286c1 100644 --- a/src/spectator/example_group_builder.cr +++ b/src/spectator/example_group_builder.cr @@ -2,6 +2,7 @@ require "./example_group" require "./example_group_hook" require "./example_hook" require "./example_procsy_hook" +require "./hooks" require "./label" require "./location" require "./metadata" @@ -12,12 +13,15 @@ module Spectator # Hooks and builders for child nodes can be added over time to this builder. # When done, call `#build` to produce an `ExampleGroup`. class ExampleGroupBuilder < NodeBuilder + include Hooks + + define_hook before_all : ExampleGroupHook + define_hook after_all : ExampleGroupHook, :prepend + define_hook before_each : ExampleHook + define_hook after_each : ExampleHook, :prepend + define_hook around_each : ExampleProcsyHook + @children = [] of NodeBuilder - @before_all_hooks = [] of ExampleGroupHook - @before_each_hooks = [] of ExampleHook - @after_all_hooks = [] of ExampleGroupHook - @after_each_hooks = [] of ExampleHook - @around_each_hooks = [] of ExampleProcsyHook # Creates the builder. # Initially, the builder will have no children and no hooks. @@ -25,62 +29,6 @@ module Spectator def initialize(@name : Label = nil, @location : Location? = nil, @metadata : Metadata = Metadata.new) end - # Attaches a hook to be invoked before any and all examples in the current group. - def add_before_all_hook(hook) - @before_all_hooks << hook - end - - # Defines a block of code to execute before any and all examples in the current group. - def before_all(&block) - @before_all_hooks << ExampleGroupHook.new(&block) - end - - # Attaches a hook to be invoked before every example in the current group. - # The current example is provided as a block argument. - def add_before_each_hook(hook) - @before_each_hooks << hook - end - - # Defines a block of code to execute before every example in the current group. - # The current example is provided as a block argument. - def before_each(&block : Example -> _) - @before_each_hooks << ExampleHook.new(&block) - end - - # Attaches a hook to be invoked after any and all examples in the current group. - def add_after_all_hook(hook) - @after_all_hooks << hook - end - - # Defines a block of code to execute after any and all examples in the current group. - def after_all(&block) - @after_all_hooks << ExampleGroupHook.new(&block) - end - - # Attaches a hook to be invoked after every example in the current group. - # The current example is provided as a block argument. - def add_after_each_hook(hook) - @after_each_hooks << hook - end - - # Defines a block of code to execute after every example in the current group. - # The current example is provided as a block argument. - def after_each(&block : Example -> _) - @after_each_hooks << ExampleHook.new(&block) - end - - # Attaches a hook to be invoked around every example in the current group. - # The current example in procsy form is provided as a block argument. - def add_around_each_hook(hook) - @around_each_hooks << hook - end - - # Defines a block of code to execute around every example in the current group. - # The current example in procsy form is provided as a block argument. - def around_each(&block : Example -> _) - @around_each_hooks << ExampleProcsyHook.new(label: "around_each", &block) - end - # Constructs an example group with previously defined attributes, children, and hooks. # The *parent* is an already constructed example group to nest the new example group under. # It can be nil if the new example group won't have a parent. @@ -100,11 +48,11 @@ module Spectator # Adds all previously configured hooks to an example group. private def apply_hooks(group) - @before_all_hooks.each { |hook| group.add_before_all_hook(hook) } - @before_each_hooks.each { |hook| group.add_before_each_hook(hook) } - @after_all_hooks.each { |hook| group.prepend_after_all_hook(hook) } - @after_each_hooks.each { |hook| group.prepend_after_each_hook(hook) } - @around_each_hooks.each { |hook| group.add_around_each_hook(hook) } + before_all_hooks.each { |hook| group.before_all(hook) } + before_each_hooks.each { |hook| group.before_each(hook) } + after_all_hooks.each { |hook| group.after_all(hook) } + after_each_hooks.each { |hook| group.after_each(hook) } + around_each_hooks.each { |hook| group.around_each(hook) } end end end diff --git a/src/spectator/example_group_hook.cr b/src/spectator/example_group_hook.cr index ef44e8b..bd6bac8 100644 --- a/src/spectator/example_group_hook.cr +++ b/src/spectator/example_group_hook.cr @@ -11,6 +11,7 @@ module Spectator getter! label : Label @proc : -> + @called = Atomic::Flag.new # Creates the hook with a proc. # The *proc* will be called when the hook is invoked. @@ -27,9 +28,18 @@ module Spectator # Invokes the hook. def call : Nil + @called.test_and_set @proc.call end + # Invokes the hook if it hasn't already been invoked. + # Returns true if the hook was invoked (first time being called). + def call_once : Bool + first = @called.test_and_set + @proc.call if first + first + end + # Produces the string representation of the hook. # Includes the location and label if they're not nil. def to_s(io) diff --git a/src/spectator/example_hook.cr b/src/spectator/example_hook.cr index f57c908..edebf26 100644 --- a/src/spectator/example_hook.cr +++ b/src/spectator/example_hook.cr @@ -4,25 +4,28 @@ require "./location" module Spectator # Information about a hook tied to an example and a proc to invoke it. class ExampleHook + # Method signature for example hooks. + alias Proc = Example -> + # Location of the hook in source code. getter! location : Location # User-defined description of the hook. getter! label : Label - @proc : Example -> + @proc : Proc # Creates the hook with a proc. # The *proc* will be called when the hook is invoked. # A *location* and *label* can be provided for debugging. - def initialize(@proc : (Example ->), *, @location : Location? = nil, @label : Label = nil) + def initialize(@proc : Proc, *, @location : Location? = nil, @label : Label = nil) end # Creates the hook with a block. # The block must take a single argument - the current example. # The block will be executed when the hook is invoked. # A *location* and *label* can be provided for debugging. - def initialize(*, @location : Location? = nil, @label : Label = nil, &block : Example -> _) + def initialize(*, @location : Location? = nil, @label : Label = nil, &block : Proc) @proc = block end diff --git a/src/spectator/hooks.cr b/src/spectator/hooks.cr new file mode 100644 index 0000000..1dd60db --- /dev/null +++ b/src/spectator/hooks.cr @@ -0,0 +1,114 @@ +module Spectator + # Mix-in for defining hook methods. + module Hooks + # Defines various methods for adding hooks of a specific type. + # + # The *declaration* defines the name and type of hook. + # It should be a type declaration in the form: `some_hook : ExampleHook`, + # where `some_hook` is the name of the hook, and `ExampleHook` is type type. + # + # A default order can be specified by *order*. + # The *order* argument must be *append* or *prepend*. + # This indicates the order hooks are added by default when called by client code. + # + # Multiple methods are generated. + # The primary methods will be named the same as the hook (from *declaration*). + # These take a pre-built hook instance, or arguments to pass to the hook type's initializer. + # The new hook is added a collection in the order specified by *order*. + # + # Alternate methods are also generated that add hooks in the opposite order of *order*. + # These are prefixed with the opposite order word. + # For instance, when *order* is "append", the prefix will be "prepend", + # resulting in a method named `prepend_some_hook`. + # + # A private getter method is created so that the hooks can be accessed if needed. + # The getter method has `_hooks` appended to the hook name. + # For instance, if the *declaration* contains `important_thing`, then the getter is `important_thing_hooks`. + # + # Lastly, an optional block can be provided. + # If given, a protected method will be defined with the block's contents. + # This method typically operates on (calls) the hooks. + # The private getter method mentioned above can be used to access the hooks. + # Any block arguments will be used as argument in the method. + # The method name has the prefix `call_` followed by the hook name. + # + # ``` + # define_hook important_event : ImportantHook do |example| + # important_event_hooks.each &.call(example) + # end + # + # # ... + # + # important_event do |example| + # puts "An important event occurred for #{example}" + # end + # ``` + macro define_hook(declaration, order = :append, &block) + {% if order.id == :append.id + method = :push.id + alt_method = :unshift.id + alt_prefix = :prepend.id + elsif order.id == :prepend.id + method = :unshift.id + alt_method = :push.id + alt_prefix = :append.id + else + raise "Unknown hook order type - #{order}" + end %} + + # Retrieves all registered hooks for {{declaration.var}}. + protected getter {{declaration.var}}_hooks = Deque({{declaration.type}}).new + + # Registers a new "{{declaration.var}}" hook. + # The hook will be {{order.id}}ed to the list. + def {{declaration.var}}(hook : {{declaration.type}}) : Nil + @{{declaration.var}}_hooks.{{method}}(hook) + end + + # Registers a new "{{declaration.var}}" hook. + # The hook will be {{order.id}}ed to the list. + # A new hook will be created by passing args to `{{declaration.type}}.new`. + def {{declaration.var}}(*args, **kwargs) : Nil + hook = {{declaration.type}}.new(*args, **kwargs) + {{declaration.var}}(hook) + end + + # Registers a new "{{declaration.var}}" hook. + # The hook will be {{order.id}}ed to the list. + # A new hook will be created by passing args to `{{declaration.type}}.new`. + def {{declaration.var}}(*args, **kwargs, &block) : Nil + hook = {{declaration.type}}.new(*args, **kwargs, &block) + {{declaration.var}}(hook) + end + + # Registers a new "{{declaration.var}}" hook. + # The hook will be {{alt_prefix}}ed to the list. + def {{alt_prefix}}_{{declaration.var}}(hook : {{declaration.type}}) : Nil + @{{declaration.var}}_hooks.{{alt_method}}(hook) + end + + # Registers a new "{{declaration.var}}" hook. + # The hook will be {{alt_prefix}}ed to the list. + # A new hook will be created by passing args to `{{declaration.type}}.new`. + def {{alt_prefix}}_{{declaration.var}}(*args, **kwargs) : Nil + hook = {{declaration.type}}.new(*args, **kwargs) + {{alt_prefix}}_{{declaration.var}}(hook) + end + + # Registers a new "{{declaration.var}}" hook. + # The hook will be {{alt_prefix}}ed to the list. + # A new hook will be created by passing args to `{{declaration.type}}.new`. + def {{alt_prefix}}_{{declaration.var}}(*args, **kwargs, &block) : Nil + hook = {{declaration.type}}.new(*args, **kwargs, &block) + {{alt_prefix}}_{{declaration.var}}(hook) + end + + {% if block %} + # Handles calling all "{{declaration.var}}" hooks. + protected def call_{{declaration.var}}({{block.args.splat}}) + {{block.body}} + end + {% end %} + end + end +end diff --git a/src/spectator/includes.cr b/src/spectator/includes.cr index 7f87649..d4fdaa6 100644 --- a/src/spectator/includes.cr +++ b/src/spectator/includes.cr @@ -14,7 +14,6 @@ require "./context_delegate" require "./context_method" require "./dsl" require "./error_result" -require "./events" require "./example_context_delegate" require "./example_context_method" require "./example" @@ -30,6 +29,7 @@ require "./expression" require "./fail_result" require "./formatting" require "./harness" +require "./hooks" require "./label" require "./lazy" require "./lazy_wrapper" diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 8bbf408..55c35fc 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -4,6 +4,7 @@ require "./example_builder" require "./example_context_method" require "./example_group" require "./example_group_builder" +require "./hooks" require "./iterative_example_group_builder" require "./pending_example_builder" require "./spec" @@ -17,6 +18,8 @@ module Spectator class SpecBuilder Log = ::Spectator::Log.for(self) + delegate before_all, after_all, before_each, after_each, around_each, to: current + # Stack tracking the current group. # The bottom of the stack (first element) is the root group. # The root group should never be removed. @@ -44,23 +47,7 @@ module Spectator raise "Mismatched start and end groups" unless root? group = root.build - - # Apply hooks from configuration. - config.before_suite_hooks.each { |hook| group.prepend_before_all_hook(hook) } - config.after_suite_hooks.each { |hook| group.prepend_after_all_hook(hook) } - config.before_each_hooks.each { |hook| group.prepend_before_each_hook(hook) } - config.after_each_hooks.each { |hook| group.prepend_after_each_hook(hook) } - config.around_each_hooks.each { |hook| group.prepend_around_each_hook(hook) } - - # `before_all` and `after_all` hooks are slightly different. - # They are applied to every top-level group (groups just under root). - group.each do |node| - next unless node.is_a?(Events) - - config.before_all_hooks.reverse_each { |hook| node.prepend_before_all_hook(hook) } - config.after_all_hooks.reverse_each { |hook| node.prepend_after_all_hook(hook) } - end - + apply_config_hooks(group) Spec.new(group, config) end @@ -153,94 +140,60 @@ module Spectator current << PendingExampleBuilder.new(name, location, metadata, reason) end - # Attaches a hook to be invoked before any and all examples in the test suite. - def before_suite(hook) - Log.trace { "Add before_suite hook #{hook}" } - root.add_before_all_hook(hook) + # Registers a new "before_suite" hook. + # The hook will be appended to the list. + # A new hook will be created by passing args to `ExampleGroupHook.new`. + def before_suite(*args, **kwargs) : Nil + root.before_all(*args, **kwargs) end - # Defines a block of code to execute before any and all examples in the test suite. - def before_suite(&block) - Log.trace { "Add before_suite hook" } - root.before_all(&block) + # Registers a new "before_suite" hook. + # The hook will be appended to the list. + # A new hook will be created by passing args to `ExampleGroupHook.new`. + def before_suite(*args, **kwargs, &block) : Nil + root.before_all(*args, **kwargs, &block) end - # Attaches a hook to be invoked before any and all examples in the current group. - def before_all(hook) - Log.trace { "Add before_all hook #{hook}" } - current.add_before_all_hook(hook) + # Registers a new "before_suite" hook. + # The hook will be prepended to the list. + # A new hook will be created by passing args to `ExampleGroupHook.new`. + def prepend_before_suite(*args, **kwargs) : Nil + root.prepend_before_all(*args, **kwargs) end - # Defines a block of code to execute before any and all examples in the current group. - def before_all(&block) - Log.trace { "Add before_all hook" } - current.before_all(&block) + # Registers a new "before_suite" hook. + # The hook will be prepended to the list. + # A new hook will be created by passing args to `ExampleGroupHook.new`. + def prepend_before_suite(*args, **kwargs, &block) : Nil + root.prepend_before_all(*args, **kwargs, &block) end - # Attaches a hook to be invoked before every example in the current group. - # The current example is provided as a block argument. - def before_each(hook) - Log.trace { "Add before_each hook #{hook}" } - current.add_before_each_hook(hook) + # Registers a new "after_suite" hook. + # The hook will be prepended to the list. + # A new hook will be created by passing args to `ExampleGroupHook.new`. + def after_suite(*args, **kwargs) : Nil + root.before_all(*args, **kwargs) end - # Defines a block of code to execute before every example in the current group. - # The current example is provided as a block argument. - def before_each(&block : Example -> _) - Log.trace { "Add before_each hook block" } - current.before_each(&block) + # Registers a new "after_suite" hook. + # The hook will be prepended to the list. + # A new hook will be created by passing args to `ExampleGroupHook.new`. + def after_suite(*args, **kwargs, &block) : Nil + root.after_all(*args, **kwargs, &block) end - # Attaches a hook to be invoked after any and all examples in the test suite. - def after_suite(hook) - Log.trace { "Add after_suite hook #{hook}" } - root.add_after_all_hook(hook) + # Registers a new "after_suite" hook. + # The hook will be appended to the list. + # A new hook will be created by passing args to `ExampleGroupHook.new`. + def append_after_suite(*args, **kwargs) : Nil + root.append_after_all(*args, **kwargs) end - # Defines a block of code to execute after any and all examples in the test suite. - def after_suite(&block) - Log.trace { "Add after_suite hook" } - root.after_all(&block) - end - - # Attaches a hook to be invoked after any and all examples in the current group. - def after_all(hook) - Log.trace { "Add after_all hook #{hook}" } - current.add_after_all_hook(hook) - end - - # Defines a block of code to execute after any and all examples in the current group. - def after_all(&block) - Log.trace { "Add after_all hook" } - current.after_all(&block) - end - - # Attaches a hook to be invoked after every example in the current group. - # The current example is provided as a block argument. - def after_each(hook) - Log.trace { "Add after_each hook #{hook}" } - current.add_after_each_hook(hook) - end - - # Defines a block of code to execute after every example in the current group. - # The current example is provided as a block argument. - def after_each(&block : Example -> _) - Log.trace { "Add after_each hook" } - current.after_each(&block) - end - - # Attaches a hook to be invoked around every example in the current group. - # The current example in procsy form is provided as a block argument. - def around_each(hook) - Log.trace { "Add around_each hook #{hook}" } - current.add_around_each_hook(hook) - end - - # Defines a block of code to execute around every example in the current group. - # The current example in procsy form is provided as a block argument. - def around_each(&block : Example -> _) - Log.trace { "Add around_each hook" } - current.around_each(&block) + # Registers a new "after_suite" hook. + # The hook will be appended to the list. + # A new hook will be created by passing args to `ExampleGroupHook.new`. + def append_after_suite(*args, **kwargs, &block) : Nil + root.append_after_all(*args, **kwargs, &block) end # Builds the configuration to use for the spec. @@ -279,5 +232,23 @@ module Spectator private def config : Config @config || Config.default end + + # Copy all hooks from config to top-level group. + private def apply_config_hooks(group) + config.before_suite_hooks.reverse_each { |hook| group.prepend_before_all(hook) } + config.after_suite_hooks.each { |hook| group.after_all(hook) } + config.before_each_hooks.reverse_each { |hook| group.prepend_before_each(hook) } + config.after_each_hooks.each { |hook| group.after_each(hook) } + config.around_each_hooks.reverse_each { |hook| group.prepend_around_each(hook) } + + # `before_all` and `after_all` hooks from config are slightly different. + # They are applied to every top-level group (groups just under root). + group.each do |node| + next unless node.is_a?(Hooks) + + config.before_all_hooks.reverse_each { |hook| node.prepend_before_all(hook.dup) } + config.after_all_hooks.each { |hook| node.after_all(hook.dup) } + end + end end end From 841efc236d957d78e03a6add0c0afca03fa10745 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Aug 2021 10:27:34 -0600 Subject: [PATCH 364/399] Fix flipped append/prepend of "after" hooks --- src/spectator/example_group.cr | 4 ++-- src/spectator/example_group_builder.cr | 4 ++-- src/spectator/spec_builder.cr | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index 4edd2e7..3b174a2 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -26,7 +26,7 @@ module Spectator before_all_hooks.each &.call_once end - define_hook after_all : ExampleGroupHook do + define_hook after_all : ExampleGroupHook, :prepend do Log.trace { "Processing after_all hooks for #{self}" } after_all_hooks.each &.call_once if finished? @@ -42,7 +42,7 @@ module Spectator before_each_hooks.each &.call(example) end - define_hook after_each : ExampleHook do |example| + define_hook after_each : ExampleHook, :prepend do |example| Log.trace { "Processing after_each hooks for #{self}" } after_each_hooks.each &.call(example) diff --git a/src/spectator/example_group_builder.cr b/src/spectator/example_group_builder.cr index b5286c1..b7fd79b 100644 --- a/src/spectator/example_group_builder.cr +++ b/src/spectator/example_group_builder.cr @@ -50,8 +50,8 @@ module Spectator private def apply_hooks(group) before_all_hooks.each { |hook| group.before_all(hook) } before_each_hooks.each { |hook| group.before_each(hook) } - after_all_hooks.each { |hook| group.after_all(hook) } - after_each_hooks.each { |hook| group.after_each(hook) } + after_all_hooks.reverse_each { |hook| group.after_all(hook) } + after_each_hooks.reverse_each { |hook| group.after_each(hook) } around_each_hooks.each { |hook| group.around_each(hook) } end end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 55c35fc..4e11611 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -236,9 +236,9 @@ module Spectator # Copy all hooks from config to top-level group. private def apply_config_hooks(group) config.before_suite_hooks.reverse_each { |hook| group.prepend_before_all(hook) } - config.after_suite_hooks.each { |hook| group.after_all(hook) } + config.after_suite_hooks.each { |hook| group.append_after_all(hook) } config.before_each_hooks.reverse_each { |hook| group.prepend_before_each(hook) } - config.after_each_hooks.each { |hook| group.after_each(hook) } + config.after_each_hooks.each { |hook| group.append_after_each(hook) } config.around_each_hooks.reverse_each { |hook| group.prepend_around_each(hook) } # `before_all` and `after_all` hooks from config are slightly different. @@ -247,7 +247,7 @@ module Spectator next unless node.is_a?(Hooks) config.before_all_hooks.reverse_each { |hook| node.prepend_before_all(hook.dup) } - config.after_all_hooks.each { |hook| node.after_all(hook.dup) } + config.after_all_hooks.each { |hook| node.append_after_all(hook.dup) } end end end From 605b82c5323a7114181e41b0585c399ec54d36d5 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Aug 2021 10:51:39 -0600 Subject: [PATCH 365/399] Add prepend and append variants of hooks to DSL --- src/spectator/dsl/builder.cr | 42 +++++++++++++++++++++++++++++++ src/spectator/dsl/hooks.cr | 47 +++++++++++++++++++++++++++++++++++ src/spectator/spec_builder.cr | 12 ++++++++- 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr index 16b1116..ddd659e 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -60,42 +60,84 @@ module Spectator::DSL @@builder.before_suite(hook) end + # Defines a block of code to execute before any and all examples in the test suite. + def prepend_before_suite(location = nil, label = "before_suite", &block) + hook = ExampleGroupHook.new(location: location, label: label, &block) + @@builder.prepend_before_suite(hook) + end + # Defines a block of code to execute before any and all examples in the current group. def before_all(location = nil, label = "before_all", &block) hook = ExampleGroupHook.new(location: location, label: label, &block) @@builder.before_all(hook) end + # Defines a block of code to execute before any and all examples in the current group. + def prepend_before_all(location = nil, label = "before_all", &block) + hook = ExampleGroupHook.new(location: location, label: label, &block) + @@builder.prepend_before_all(hook) + end + # Defines a block of code to execute before every example in the current group def before_each(location = nil, label = "before_each", &block : Example -> _) hook = ExampleHook.new(location: location, label: label, &block) @@builder.before_each(hook) end + # Defines a block of code to execute before every example in the current group + def prepend_before_each(location = nil, label = "before_each", &block : Example -> _) + hook = ExampleHook.new(location: location, label: label, &block) + @@builder.prepend_before_each(hook) + end + # Defines a block of code to execute after any and all examples in the test suite. def after_suite(location = nil, label = "after_suite", &block) hook = ExampleGroupHook.new(location: location, label: label, &block) @@builder.after_suite(hook) end + # Defines a block of code to execute after any and all examples in the test suite. + def append_after_suite(location = nil, label = "after_suite", &block) + hook = ExampleGroupHook.new(location: location, label: label, &block) + @@builder.append_after_suite(hook) + end + # Defines a block of code to execute after any and all examples in the current group. def after_all(location = nil, label = "after_all", &block) hook = ExampleGroupHook.new(location: location, label: label, &block) @@builder.after_all(hook) end + # Defines a block of code to execute after any and all examples in the current group. + def append_after_all(location = nil, label = "after_all", &block) + hook = ExampleGroupHook.new(location: location, label: label, &block) + @@builder.append_after_all(hook) + end + # Defines a block of code to execute after every example in the current group. def after_each(location = nil, label = "after_each", &block : Example ->) hook = ExampleHook.new(location: location, label: label, &block) @@builder.after_each(hook) end + # Defines a block of code to execute after every example in the current group. + def append_after_each(location = nil, label = "after_each", &block : Example ->) + hook = ExampleHook.new(location: location, label: label, &block) + @@builder.append_after_each(hook) + end + # Defines a block of code to execute around every example in the current group. def around_each(location = nil, label = "around_each", &block : Example::Procsy ->) hook = ExampleProcsyHook.new(location: location, label: label, &block) @@builder.around_each(hook) end + # Defines a block of code to execute around every example in the current group. + def prepend_around_each(location = nil, label = "around_each", &block : Example::Procsy ->) + hook = ExampleProcsyHook.new(location: location, label: label, &block) + @@builder.prepend_around_each(hook) + end + # Sets the configuration of the spec. # # See `Spec::Builder#config=` for usage details. diff --git a/src/spectator/dsl/hooks.cr b/src/spectator/dsl/hooks.cr index d11ff08..0cae6ee 100644 --- a/src/spectator/dsl/hooks.cr +++ b/src/spectator/dsl/hooks.cr @@ -104,31 +104,67 @@ module Spectator::DSL # This means that values defined by `let` and `subject` are not available. define_example_group_hook :before_suite + # Defines a block of code that will be invoked once before any examples in the suite. + # The block will not run in the context of the current running example. + # This means that values defined by `let` and `subject` are not available. + # The hook is added before all others others of the same type in this context. + define_example_group_hook :prepend_before_suite + # Defines a block of code that will be invoked once after all examples in the suite. # The block will not run in the context of the current running example. # This means that values defined by `let` and `subject` are not available. define_example_group_hook :after_suite + # Defines a block of code that will be invoked once after all examples in the suite. + # The block will not run in the context of the current running example. + # This means that values defined by `let` and `subject` are not available. + # The hook is added after all others others of the same type in this context. + define_example_group_hook :append_after_suite + # Defines a block of code that will be invoked once before any examples in the group. # The block will not run in the context of the current running example. # This means that values defined by `let` and `subject` are not available. define_example_group_hook :before_all + # Defines a block of code that will be invoked once before any examples in the group. + # The block will not run in the context of the current running example. + # This means that values defined by `let` and `subject` are not available. + # The hook is added before all others others of the same type in this context. + define_example_group_hook :prepend_before_all + # Defines a block of code that will be invoked once after all examples in the group. # The block will not run in the context of the current running example. # This means that values defined by `let` and `subject` are not available. define_example_group_hook :after_all + # Defines a block of code that will be invoked once after all examples in the group. + # The block will not run in the context of the current running example. + # This means that values defined by `let` and `subject` are not available. + # The hook is added after all others others of the same type in this context. + define_example_group_hook :append_after_all + # Defines a block of code that will be invoked before every example in the group. # The block will be run in the context of the current running example. # This means that values defined by `let` and `subject` are available. define_example_hook :before_each + # Defines a block of code that will be invoked before every example in the group. + # The block will be run in the context of the current running example. + # This means that values defined by `let` and `subject` are available. + # The hook is added before all others others of the same type in this context. + define_example_hook :prepend_before_each + # Defines a block of code that will be invoked after every example in the group. # The block will be run in the context of the current running example. # This means that values defined by `let` and `subject` are available. define_example_hook :after_each + # Defines a block of code that will be invoked after every example in the group. + # The block will be run in the context of the current running example. + # This means that values defined by `let` and `subject` are available. + # The hook is added after all others others of the same type in this context. + define_example_hook :append_after_each + # Defines a block of code that will be invoked around every example in the group. # The block will be run in the context of the current running example. # This means that values defined by `let` and `subject` are available. @@ -138,5 +174,16 @@ module Spectator::DSL # The `Example::Procsy#run` method should be called to ensure the example runs. # More code can run afterwards (in the block). define_example_hook :around_each + + # Defines a block of code that will be invoked around every example in the group. + # The block will be run in the context of the current running example. + # This means that values defined by `let` and `subject` are available. + # The hook is added before all others others of the same type in this context. + # + # The block will execute before the example. + # An `Example::Procsy` is passed to the block. + # The `Example::Procsy#run` method should be called to ensure the example runs. + # More code can run afterwards (in the block). + define_example_hook :prepend_around_each end end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 4e11611..57cd57a 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -18,7 +18,17 @@ module Spectator class SpecBuilder Log = ::Spectator::Log.for(self) - delegate before_all, after_all, before_each, after_each, around_each, to: current + delegate before_all, + prepend_before_all, + after_all, + append_after_all, + before_each, + prepend_before_each, + after_each, + append_after_each, + around_each, + prepend_around_each, + to: current # Stack tracking the current group. # The bottom of the stack (first element) is the root group. From 91d21b38e215cef91a257993230510090a79ef30 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Aug 2021 11:25:06 -0600 Subject: [PATCH 366/399] Lazily initialize global DSL spec builder Require config to create a spec builder. Config should be fully set up before any DSL is encountered. --- src/spectator.cr | 7 ++--- src/spectator/dsl/builder.cr | 49 +++++++++++++++-------------------- src/spectator/spec_builder.cr | 22 ++-------------- 3 files changed, 25 insertions(+), 53 deletions(-) diff --git a/src/spectator.cr b/src/spectator.cr index 638e9e6..0459ae4 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -68,7 +68,6 @@ module Spectator ::Log.setup_from_env(default_level: :none) # Build the spec and run it. - DSL::Builder.config = config spec = DSL::Builder.build spec.run rescue ex @@ -84,10 +83,8 @@ module Spectator false end - # Processes and builds up a configuration to use for running tests. - private def config - @@config ||= build_config - end + # Global configuration used by Spectator for running tests. + class_getter(config) { build_config } # Builds the configuration. private def build_config diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr index ddd659e..9d60dba 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -10,7 +10,7 @@ module Spectator::DSL extend self # Underlying spec builder. - @@builder = SpecBuilder.new + private class_getter(builder) { SpecBuilder.new(Spectator.config) } # Defines a new example group and pushes it onto the group stack. # Examples and groups defined after calling this method will be nested under the new group. @@ -18,7 +18,7 @@ module Spectator::DSL # # See `Spec::Builder#start_group` for usage details. def start_group(*args) - @@builder.start_group(*args) + builder.start_group(*args) end # Defines a new iterative example group and pushes it onto the group stack. @@ -27,7 +27,7 @@ module Spectator::DSL # # See `Spec::Builder#start_iterative_group` for usage details. def start_iterative_group(*args) - @@builder.start_iterative_group(*args) + builder.start_iterative_group(*args) end # Completes a previously defined example group and pops it off the group stack. @@ -35,7 +35,7 @@ module Spectator::DSL # # See `Spec::Builder#end_group` for usage details. def end_group(*args) - @@builder.end_group(*args) + builder.end_group(*args) end # Defines a new example. @@ -43,7 +43,7 @@ module Spectator::DSL # # See `Spec::Builder#add_example` for usage details. def add_example(*args, &block : Example ->) - @@builder.add_example(*args, &block) + builder.add_example(*args, &block) end # Defines a new pending example. @@ -51,98 +51,91 @@ module Spectator::DSL # # See `Spec::Builder#add_pending_example` for usage details. def add_pending_example(*args) - @@builder.add_pending_example(*args) + builder.add_pending_example(*args) end # Defines a block of code to execute before any and all examples in the test suite. def before_suite(location = nil, label = "before_suite", &block) hook = ExampleGroupHook.new(location: location, label: label, &block) - @@builder.before_suite(hook) + builder.before_suite(hook) end # Defines a block of code to execute before any and all examples in the test suite. def prepend_before_suite(location = nil, label = "before_suite", &block) hook = ExampleGroupHook.new(location: location, label: label, &block) - @@builder.prepend_before_suite(hook) + builder.prepend_before_suite(hook) end # Defines a block of code to execute before any and all examples in the current group. def before_all(location = nil, label = "before_all", &block) hook = ExampleGroupHook.new(location: location, label: label, &block) - @@builder.before_all(hook) + builder.before_all(hook) end # Defines a block of code to execute before any and all examples in the current group. def prepend_before_all(location = nil, label = "before_all", &block) hook = ExampleGroupHook.new(location: location, label: label, &block) - @@builder.prepend_before_all(hook) + builder.prepend_before_all(hook) end # Defines a block of code to execute before every example in the current group def before_each(location = nil, label = "before_each", &block : Example -> _) hook = ExampleHook.new(location: location, label: label, &block) - @@builder.before_each(hook) + builder.before_each(hook) end # Defines a block of code to execute before every example in the current group def prepend_before_each(location = nil, label = "before_each", &block : Example -> _) hook = ExampleHook.new(location: location, label: label, &block) - @@builder.prepend_before_each(hook) + builder.prepend_before_each(hook) end # Defines a block of code to execute after any and all examples in the test suite. def after_suite(location = nil, label = "after_suite", &block) hook = ExampleGroupHook.new(location: location, label: label, &block) - @@builder.after_suite(hook) + builder.after_suite(hook) end # Defines a block of code to execute after any and all examples in the test suite. def append_after_suite(location = nil, label = "after_suite", &block) hook = ExampleGroupHook.new(location: location, label: label, &block) - @@builder.append_after_suite(hook) + builder.append_after_suite(hook) end # Defines a block of code to execute after any and all examples in the current group. def after_all(location = nil, label = "after_all", &block) hook = ExampleGroupHook.new(location: location, label: label, &block) - @@builder.after_all(hook) + builder.after_all(hook) end # Defines a block of code to execute after any and all examples in the current group. def append_after_all(location = nil, label = "after_all", &block) hook = ExampleGroupHook.new(location: location, label: label, &block) - @@builder.append_after_all(hook) + builder.append_after_all(hook) end # Defines a block of code to execute after every example in the current group. def after_each(location = nil, label = "after_each", &block : Example ->) hook = ExampleHook.new(location: location, label: label, &block) - @@builder.after_each(hook) + builder.after_each(hook) end # Defines a block of code to execute after every example in the current group. def append_after_each(location = nil, label = "after_each", &block : Example ->) hook = ExampleHook.new(location: location, label: label, &block) - @@builder.append_after_each(hook) + builder.append_after_each(hook) end # Defines a block of code to execute around every example in the current group. def around_each(location = nil, label = "around_each", &block : Example::Procsy ->) hook = ExampleProcsyHook.new(location: location, label: label, &block) - @@builder.around_each(hook) + builder.around_each(hook) end # Defines a block of code to execute around every example in the current group. def prepend_around_each(location = nil, label = "around_each", &block : Example::Procsy ->) hook = ExampleProcsyHook.new(location: location, label: label, &block) - @@builder.prepend_around_each(hook) - end - - # Sets the configuration of the spec. - # - # See `Spec::Builder#config=` for usage details. - def config=(config) - @@builder.config = config + builder.prepend_around_each(hook) end # Constructs the test spec. @@ -151,7 +144,7 @@ module Spectator::DSL # Raises an error if there were not symmetrical calls to `#start_group` and `#end_group`. # This would indicate a logical error somewhere in Spectator or an extension of it. def build : Spec - @@builder.build + builder.build end end end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 57cd57a..cc274e5 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -37,12 +37,9 @@ module Spectator # New examples should be added to the current group. @stack : Deque(ExampleGroupBuilder) - # Configuration for the spec. - @config : Config? - # Creates a new spec builder. # A root group is pushed onto the group stack. - def initialize + def initialize(@config : Config) root = ExampleGroupBuilder.new @stack = Deque(ExampleGroupBuilder).new @stack.push(root) @@ -58,7 +55,7 @@ module Spectator group = root.build apply_config_hooks(group) - Spec.new(group, config) + Spec.new(group, @config) end # Defines a new example group and pushes it onto the group stack. @@ -206,21 +203,6 @@ module Spectator root.append_after_all(*args, **kwargs, &block) end - # Builds the configuration to use for the spec. - # A `Config::Builder` is yielded to the block provided to this method. - # That builder will be used to create the configuration. - def configure(& : Config::Builder -> _) : Nil - builder = Config::Builder.new - yield builder - @config = builder.build - end - - # Sets the configuration of the spec. - # This configuration controls how examples run. - def config=(config) - @config = config - end - # Checks if the current group is the root group. private def root? @stack.size == 1 From 0f7a9ed9e8b39aa5848c1380bb547b896be8b78e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Aug 2021 11:50:30 -0600 Subject: [PATCH 367/399] Remove append and prepend variants of hook definition methods RSpec defines these as applying to a scope (example, context, suite) as opposed to example group. Mimicing this is currently not possible in Spectator and would require a substantial restructure of how hooks are handled. This may be implemented in the future. --- CHANGELOG.md | 9 ++-- src/spectator/dsl/builder.cr | 42 ------------------ src/spectator/dsl/hooks.cr | 47 -------------------- src/spectator/hooks.cr | 31 ------------- src/spectator/spec_builder.cr | 84 +++++++++++------------------------ 5 files changed, 29 insertions(+), 184 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7d70df..e7ebaa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Fixed - Fix resolution of types with the same name in nested scopes. [#31](https://github.com/icy-arctic-fox/spectator/issues/31) +- `around_each` hooks wrap `before_all` and `after_all` hooks. [#12](https://github.com/icy-arctic-fox/spectator/issues/12) +- Hook execution order has been tweaked to match RSpec. ### Added -- Hooks are yielded the current example as a block argument. +- `before_each`, `after_each`, and `around_each` hooks are yielded the current example as a block argument. - The `let` and `subject` blocks are yielded the current example as a block argument. - Add internal logging that uses Crystal's `Log` utility. Provide the `LOG_LEVEL` environment variable to enable. - Support dynamic creation of examples. @@ -26,11 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support custom messages for failed expectations. [#28](https://gitlab.com/arctic-fox/spectator/-/issues/28) - Allow named arguments and assignments for `provided` (`given`) block. - Add `aggregate_failures` to capture and report multiple failed expectations. [#24](https://gitlab.com/arctic-fox/spectator/-/issues/24) -- Add `append_` and `prepend_` variants of hook creation methods. ### Changed -- `around_each` hooks wrap `before_all` and `after_all` hooks. [#12](https://github.com/icy-arctic-fox/spectator/issues/12) -- Hook execution order has been tweaked to match RSpec. - `given` (now `provided`) blocks changed to produce a single example. `it` can no longer be nested in a `provided` block. - The "should" syntax no longer reports the source as inside Spectator. - Short-hand "should" syntax must be included by using `require "spectator/should"` - `it { should eq("foo") }` @@ -38,7 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Prevent usage of reserved keywords in DSL (such as `initialize`). - The count argument for `sample` and `random_sample` groups must be named (use `count: 5` instead of just `5`). - Helper methods used as arguments for `sample` and `random_sample` must be class methods. -- Simplify and reduce defined types and generics. Should speed up compilation times. +- Simplify and reduce instanced types and generics. Should speed up compilation times. - Overhaul example creation and handling. - Overhaul storage of test values. - Overhaul reporting and formatting. Cleaner output for failures and pending tests. diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr index 9d60dba..4054906 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -60,84 +60,42 @@ module Spectator::DSL builder.before_suite(hook) end - # Defines a block of code to execute before any and all examples in the test suite. - def prepend_before_suite(location = nil, label = "before_suite", &block) - hook = ExampleGroupHook.new(location: location, label: label, &block) - builder.prepend_before_suite(hook) - end - # Defines a block of code to execute before any and all examples in the current group. def before_all(location = nil, label = "before_all", &block) hook = ExampleGroupHook.new(location: location, label: label, &block) builder.before_all(hook) end - # Defines a block of code to execute before any and all examples in the current group. - def prepend_before_all(location = nil, label = "before_all", &block) - hook = ExampleGroupHook.new(location: location, label: label, &block) - builder.prepend_before_all(hook) - end - # Defines a block of code to execute before every example in the current group def before_each(location = nil, label = "before_each", &block : Example -> _) hook = ExampleHook.new(location: location, label: label, &block) builder.before_each(hook) end - # Defines a block of code to execute before every example in the current group - def prepend_before_each(location = nil, label = "before_each", &block : Example -> _) - hook = ExampleHook.new(location: location, label: label, &block) - builder.prepend_before_each(hook) - end - # Defines a block of code to execute after any and all examples in the test suite. def after_suite(location = nil, label = "after_suite", &block) hook = ExampleGroupHook.new(location: location, label: label, &block) builder.after_suite(hook) end - # Defines a block of code to execute after any and all examples in the test suite. - def append_after_suite(location = nil, label = "after_suite", &block) - hook = ExampleGroupHook.new(location: location, label: label, &block) - builder.append_after_suite(hook) - end - # Defines a block of code to execute after any and all examples in the current group. def after_all(location = nil, label = "after_all", &block) hook = ExampleGroupHook.new(location: location, label: label, &block) builder.after_all(hook) end - # Defines a block of code to execute after any and all examples in the current group. - def append_after_all(location = nil, label = "after_all", &block) - hook = ExampleGroupHook.new(location: location, label: label, &block) - builder.append_after_all(hook) - end - # Defines a block of code to execute after every example in the current group. def after_each(location = nil, label = "after_each", &block : Example ->) hook = ExampleHook.new(location: location, label: label, &block) builder.after_each(hook) end - # Defines a block of code to execute after every example in the current group. - def append_after_each(location = nil, label = "after_each", &block : Example ->) - hook = ExampleHook.new(location: location, label: label, &block) - builder.append_after_each(hook) - end - # Defines a block of code to execute around every example in the current group. def around_each(location = nil, label = "around_each", &block : Example::Procsy ->) hook = ExampleProcsyHook.new(location: location, label: label, &block) builder.around_each(hook) end - # Defines a block of code to execute around every example in the current group. - def prepend_around_each(location = nil, label = "around_each", &block : Example::Procsy ->) - hook = ExampleProcsyHook.new(location: location, label: label, &block) - builder.prepend_around_each(hook) - end - # Constructs the test spec. # Returns the spec instance. # diff --git a/src/spectator/dsl/hooks.cr b/src/spectator/dsl/hooks.cr index 0cae6ee..d11ff08 100644 --- a/src/spectator/dsl/hooks.cr +++ b/src/spectator/dsl/hooks.cr @@ -104,67 +104,31 @@ module Spectator::DSL # This means that values defined by `let` and `subject` are not available. define_example_group_hook :before_suite - # Defines a block of code that will be invoked once before any examples in the suite. - # The block will not run in the context of the current running example. - # This means that values defined by `let` and `subject` are not available. - # The hook is added before all others others of the same type in this context. - define_example_group_hook :prepend_before_suite - # Defines a block of code that will be invoked once after all examples in the suite. # The block will not run in the context of the current running example. # This means that values defined by `let` and `subject` are not available. define_example_group_hook :after_suite - # Defines a block of code that will be invoked once after all examples in the suite. - # The block will not run in the context of the current running example. - # This means that values defined by `let` and `subject` are not available. - # The hook is added after all others others of the same type in this context. - define_example_group_hook :append_after_suite - # Defines a block of code that will be invoked once before any examples in the group. # The block will not run in the context of the current running example. # This means that values defined by `let` and `subject` are not available. define_example_group_hook :before_all - # Defines a block of code that will be invoked once before any examples in the group. - # The block will not run in the context of the current running example. - # This means that values defined by `let` and `subject` are not available. - # The hook is added before all others others of the same type in this context. - define_example_group_hook :prepend_before_all - # Defines a block of code that will be invoked once after all examples in the group. # The block will not run in the context of the current running example. # This means that values defined by `let` and `subject` are not available. define_example_group_hook :after_all - # Defines a block of code that will be invoked once after all examples in the group. - # The block will not run in the context of the current running example. - # This means that values defined by `let` and `subject` are not available. - # The hook is added after all others others of the same type in this context. - define_example_group_hook :append_after_all - # Defines a block of code that will be invoked before every example in the group. # The block will be run in the context of the current running example. # This means that values defined by `let` and `subject` are available. define_example_hook :before_each - # Defines a block of code that will be invoked before every example in the group. - # The block will be run in the context of the current running example. - # This means that values defined by `let` and `subject` are available. - # The hook is added before all others others of the same type in this context. - define_example_hook :prepend_before_each - # Defines a block of code that will be invoked after every example in the group. # The block will be run in the context of the current running example. # This means that values defined by `let` and `subject` are available. define_example_hook :after_each - # Defines a block of code that will be invoked after every example in the group. - # The block will be run in the context of the current running example. - # This means that values defined by `let` and `subject` are available. - # The hook is added after all others others of the same type in this context. - define_example_hook :append_after_each - # Defines a block of code that will be invoked around every example in the group. # The block will be run in the context of the current running example. # This means that values defined by `let` and `subject` are available. @@ -174,16 +138,5 @@ module Spectator::DSL # The `Example::Procsy#run` method should be called to ensure the example runs. # More code can run afterwards (in the block). define_example_hook :around_each - - # Defines a block of code that will be invoked around every example in the group. - # The block will be run in the context of the current running example. - # This means that values defined by `let` and `subject` are available. - # The hook is added before all others others of the same type in this context. - # - # The block will execute before the example. - # An `Example::Procsy` is passed to the block. - # The `Example::Procsy#run` method should be called to ensure the example runs. - # More code can run afterwards (in the block). - define_example_hook :prepend_around_each end end diff --git a/src/spectator/hooks.cr b/src/spectator/hooks.cr index 1dd60db..f06b0e1 100644 --- a/src/spectator/hooks.cr +++ b/src/spectator/hooks.cr @@ -16,11 +16,6 @@ module Spectator # These take a pre-built hook instance, or arguments to pass to the hook type's initializer. # The new hook is added a collection in the order specified by *order*. # - # Alternate methods are also generated that add hooks in the opposite order of *order*. - # These are prefixed with the opposite order word. - # For instance, when *order* is "append", the prefix will be "prepend", - # resulting in a method named `prepend_some_hook`. - # # A private getter method is created so that the hooks can be accessed if needed. # The getter method has `_hooks` appended to the hook name. # For instance, if the *declaration* contains `important_thing`, then the getter is `important_thing_hooks`. @@ -46,12 +41,8 @@ module Spectator macro define_hook(declaration, order = :append, &block) {% if order.id == :append.id method = :push.id - alt_method = :unshift.id - alt_prefix = :prepend.id elsif order.id == :prepend.id method = :unshift.id - alt_method = :push.id - alt_prefix = :append.id else raise "Unknown hook order type - #{order}" end %} @@ -81,28 +72,6 @@ module Spectator {{declaration.var}}(hook) end - # Registers a new "{{declaration.var}}" hook. - # The hook will be {{alt_prefix}}ed to the list. - def {{alt_prefix}}_{{declaration.var}}(hook : {{declaration.type}}) : Nil - @{{declaration.var}}_hooks.{{alt_method}}(hook) - end - - # Registers a new "{{declaration.var}}" hook. - # The hook will be {{alt_prefix}}ed to the list. - # A new hook will be created by passing args to `{{declaration.type}}.new`. - def {{alt_prefix}}_{{declaration.var}}(*args, **kwargs) : Nil - hook = {{declaration.type}}.new(*args, **kwargs) - {{alt_prefix}}_{{declaration.var}}(hook) - end - - # Registers a new "{{declaration.var}}" hook. - # The hook will be {{alt_prefix}}ed to the list. - # A new hook will be created by passing args to `{{declaration.type}}.new`. - def {{alt_prefix}}_{{declaration.var}}(*args, **kwargs, &block) : Nil - hook = {{declaration.type}}.new(*args, **kwargs, &block) - {{alt_prefix}}_{{declaration.var}}(hook) - end - {% if block %} # Handles calling all "{{declaration.var}}" hooks. protected def call_{{declaration.var}}({{block.args.splat}}) diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index cc274e5..c4d1485 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -18,17 +18,7 @@ module Spectator class SpecBuilder Log = ::Spectator::Log.for(self) - delegate before_all, - prepend_before_all, - after_all, - append_after_all, - before_each, - prepend_before_each, - after_each, - append_after_each, - around_each, - prepend_around_each, - to: current + delegate before_all, after_all, before_each, after_each, around_each, to: current # Stack tracking the current group. # The bottom of the stack (first element) is the root group. @@ -73,6 +63,12 @@ module Spectator def start_group(name, location = nil, metadata = Metadata.new) : Nil Log.trace { "Start group: #{name.inspect} @ #{location}; metadata: #{metadata}" } builder = ExampleGroupBuilder.new(name, location, metadata) + + # `before_all` and `after_all` hooks from config are slightly different. + # They are applied to every top-level group (groups just under root). + apply_top_level_config_hooks(builder) if root? + + # Add group to the stack. current << builder @stack.push(builder) end @@ -93,6 +89,12 @@ module Spectator def start_iterative_group(collection, name, iterator = nil, location = nil, metadata = Metadata.new) : Nil Log.trace { "Start iterative group: #{name} (#{typeof(collection)}) @ #{location}; metadata: #{metadata}" } builder = IterativeExampleGroupBuilder.new(collection, name, iterator, location, metadata) + + # `before_all` and `after_all` hooks from config are slightly different. + # They are applied to every top-level group (groups just under root). + apply_top_level_config_hooks(builder) if root? + + # Add group to the stack. current << builder @stack.push(builder) end @@ -161,20 +163,6 @@ module Spectator root.before_all(*args, **kwargs, &block) end - # Registers a new "before_suite" hook. - # The hook will be prepended to the list. - # A new hook will be created by passing args to `ExampleGroupHook.new`. - def prepend_before_suite(*args, **kwargs) : Nil - root.prepend_before_all(*args, **kwargs) - end - - # Registers a new "before_suite" hook. - # The hook will be prepended to the list. - # A new hook will be created by passing args to `ExampleGroupHook.new`. - def prepend_before_suite(*args, **kwargs, &block) : Nil - root.prepend_before_all(*args, **kwargs, &block) - end - # Registers a new "after_suite" hook. # The hook will be prepended to the list. # A new hook will be created by passing args to `ExampleGroupHook.new`. @@ -189,20 +177,6 @@ module Spectator root.after_all(*args, **kwargs, &block) end - # Registers a new "after_suite" hook. - # The hook will be appended to the list. - # A new hook will be created by passing args to `ExampleGroupHook.new`. - def append_after_suite(*args, **kwargs) : Nil - root.append_after_all(*args, **kwargs) - end - - # Registers a new "after_suite" hook. - # The hook will be appended to the list. - # A new hook will be created by passing args to `ExampleGroupHook.new`. - def append_after_suite(*args, **kwargs, &block) : Nil - root.append_after_all(*args, **kwargs, &block) - end - # Checks if the current group is the root group. private def root? @stack.size == 1 @@ -219,28 +193,20 @@ module Spectator @stack.last end - # Retrieves the configuration. - # If one wasn't previously set, a default configuration is used. - private def config : Config - @config || Config.default + # Copy all hooks from config to root group. + private def apply_config_hooks(group) + @config.before_suite_hooks.each { |hook| group.before_all(hook) } + @config.after_suite_hooks.reverse_each { |hook| group.after_all(hook) } + @config.before_each_hooks.each { |hook| group.before_each(hook) } + @config.after_each_hooks.reverse_each { |hook| group.after_each(hook) } + @config.around_each_hooks.each { |hook| group.around_each(hook) } end - # Copy all hooks from config to top-level group. - private def apply_config_hooks(group) - config.before_suite_hooks.reverse_each { |hook| group.prepend_before_all(hook) } - config.after_suite_hooks.each { |hook| group.append_after_all(hook) } - config.before_each_hooks.reverse_each { |hook| group.prepend_before_each(hook) } - config.after_each_hooks.each { |hook| group.append_after_each(hook) } - config.around_each_hooks.reverse_each { |hook| group.prepend_around_each(hook) } - - # `before_all` and `after_all` hooks from config are slightly different. - # They are applied to every top-level group (groups just under root). - group.each do |node| - next unless node.is_a?(Hooks) - - config.before_all_hooks.reverse_each { |hook| node.prepend_before_all(hook.dup) } - config.after_all_hooks.each { |hook| node.append_after_all(hook.dup) } - end + # Copy `before_all` and `after_all` hooks to a group. + private def apply_top_level_config_hooks(group) + # Hooks are dupped so that they retain their original state (call once). + @config.before_all_hooks.each { |hook| group.before_all(hook.dup) } + @config.after_all_hooks.reverse_each { |hook| group.after_all(hook.dup) } end end end From aa81f1d94890c541c3a1b700987fbf9f0132161a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Aug 2021 12:13:59 -0600 Subject: [PATCH 368/399] Change ExampleFilter to a NodeFilter Node filters may operate on example groups as well as examples. --- src/spectator/composite_example_filter.cr | 15 --------------- src/spectator/composite_node_filter.cr | 15 +++++++++++++++ src/spectator/config.cr | 8 ++++---- src/spectator/config/builder.cr | 18 +++++++++--------- .../config/cli_arguments_applicator.cr | 18 +++++++++--------- src/spectator/example_filter.cr | 14 -------------- src/spectator/includes.cr | 12 ++++++------ src/spectator/line_example_filter.cr | 17 ----------------- src/spectator/line_node_filter.cr | 19 +++++++++++++++++++ src/spectator/location_example_filter.cr | 14 -------------- src/spectator/location_node_filter.cr | 17 +++++++++++++++++ src/spectator/name_example_filter.cr | 13 ------------- src/spectator/name_node_filter.cr | 15 +++++++++++++++ src/spectator/node_filter.cr | 14 ++++++++++++++ src/spectator/null_example_filter.cr | 9 --------- src/spectator/null_node_filter.cr | 11 +++++++++++ 16 files changed, 119 insertions(+), 110 deletions(-) delete mode 100644 src/spectator/composite_example_filter.cr create mode 100644 src/spectator/composite_node_filter.cr delete mode 100644 src/spectator/example_filter.cr delete mode 100644 src/spectator/line_example_filter.cr create mode 100644 src/spectator/line_node_filter.cr delete mode 100644 src/spectator/location_example_filter.cr create mode 100644 src/spectator/location_node_filter.cr delete mode 100644 src/spectator/name_example_filter.cr create mode 100644 src/spectator/name_node_filter.cr create mode 100644 src/spectator/node_filter.cr delete mode 100644 src/spectator/null_example_filter.cr create mode 100644 src/spectator/null_node_filter.cr diff --git a/src/spectator/composite_example_filter.cr b/src/spectator/composite_example_filter.cr deleted file mode 100644 index 1162c88..0000000 --- a/src/spectator/composite_example_filter.cr +++ /dev/null @@ -1,15 +0,0 @@ -require "./example_filter" - -module Spectator - # Filter that combines multiple other filters. - class CompositeExampleFilter < ExampleFilter - # Creates the example filter. - def initialize(@filters : Array(ExampleFilter)) - end - - # Checks whether the example satisfies the filter. - def includes?(example) : Bool - @filters.any?(&.includes?(example)) - end - end -end diff --git a/src/spectator/composite_node_filter.cr b/src/spectator/composite_node_filter.cr new file mode 100644 index 0000000..1450bcc --- /dev/null +++ b/src/spectator/composite_node_filter.cr @@ -0,0 +1,15 @@ +require "./node_filter" + +module Spectator + # Filter that combines multiple other filters. + class CompositeNodeFilter < NodeFilter + # Creates the example filter. + def initialize(@filters : Array(NodeFilter)) + end + + # Checks whether the node satisfies the filter. + def includes?(node) : Bool + @filters.any?(&.includes?(node)) + end + end +end diff --git a/src/spectator/config.cr b/src/spectator/config.cr index 577b938..cd7175a 100644 --- a/src/spectator/config.cr +++ b/src/spectator/config.cr @@ -1,5 +1,5 @@ require "./config/*" -require "./example_filter" +require "./node_filter" require "./example_group" require "./example_iterator" require "./formatting/formatter" @@ -18,7 +18,7 @@ module Spectator getter random_seed : UInt64 # Filter used to select which examples to run. - getter example_filter : ExampleFilter + getter node_filter : NodeFilter # List of hooks to run before all examples in the test suite. protected getter before_suite_hooks : Deque(ExampleGroupHook) @@ -48,7 +48,7 @@ module Spectator @formatter = source.formatter @run_flags = source.run_flags @random_seed = source.random_seed - @example_filter = source.example_filter + @node_filter = source.node_filter @before_suite_hooks = source.before_suite_hooks @before_all_hooks = source.before_all_hooks @@ -86,7 +86,7 @@ module Spectator # Creates an iterator configured to select the filtered examples. def iterator(group : ExampleGroup) - ExampleIterator.new(group).select(@example_filter) + ExampleIterator.new(group).select(@node_filter) end # Retrieves the configured random number generator. diff --git a/src/spectator/config/builder.cr b/src/spectator/config/builder.cr index 9c73506..f669923 100644 --- a/src/spectator/config/builder.cr +++ b/src/spectator/config/builder.cr @@ -1,7 +1,7 @@ -require "../composite_example_filter" -require "../example_filter" +require "../composite_node_filter" +require "../node_filter" require "../formatting" -require "../null_example_filter" +require "../null_node_filter" require "../run_flags" module Spectator @@ -18,7 +18,7 @@ module Spectator @primary_formatter : Formatting::Formatter? @additional_formatters = [] of Formatting::Formatter - @filters = [] of ExampleFilter + @filters = [] of NodeFilter # List of hooks to run before all examples in the test suite. protected getter before_suite_hooks = Deque(ExampleGroupHook).new @@ -259,18 +259,18 @@ module Spectator end # Adds a filter to determine which examples can run. - def add_example_filter(filter : ExampleFilter) + def add_node_filter(filter : NodeFilter) @filters << filter end # Retrieves a filter that determines which examples can run. - # If no filters were added with `#add_example_filter`, + # If no filters were added with `#add_node_filter`, # then the returned filter will allow all examples to be run. - protected def example_filter + protected def node_filter case (filters = @filters) - when .empty? then NullExampleFilter.new + when .empty? then NullNodeFilter.new when .one? then filters.first - else CompositeExampleFilter.new(filters) + else CompositeNodeFilter.new(filters) end end end diff --git a/src/spectator/config/cli_arguments_applicator.cr b/src/spectator/config/cli_arguments_applicator.cr index a19cba0..6580fba 100644 --- a/src/spectator/config/cli_arguments_applicator.cr +++ b/src/spectator/config/cli_arguments_applicator.cr @@ -1,10 +1,10 @@ require "colorize" require "option_parser" require "../formatting" -require "../line_example_filter" +require "../line_node_filter" require "../location" -require "../location_example_filter" -require "../name_example_filter" +require "../location_node_filter" +require "../name_node_filter" module Spectator class Config @@ -111,8 +111,8 @@ module Spectator private def example_option(parser, builder) parser.on("-e", "--example STRING", "Run examples whose full nested names include STRING") do |pattern| Log.debug { "Filtering for examples named '#{pattern}' (-e '#{pattern}')" } - filter = NameExampleFilter.new(pattern) - builder.add_example_filter(filter) + filter = NameNodeFilter.new(pattern) + builder.add_node_filter(filter) end end @@ -120,8 +120,8 @@ module Spectator private def line_option(parser, builder) parser.on("-l", "--line LINE", "Run examples whose line matches LINE") do |line| Log.debug { "Filtering for examples on line #{line} (-l #{line})" } - filter = LineExampleFilter.new(line.to_i) - builder.add_example_filter(filter) + filter = LineNodeFilter.new(line.to_i) + builder.add_node_filter(filter) end end @@ -130,8 +130,8 @@ module Spectator parser.on("--location FILE:LINE", "Run the example at line 'LINE' in the file 'FILE', multiple allowed") do |location| Log.debug { "Filtering for examples at #{location} (--location '#{location}')" } location = Location.parse(location) - filter = LocationExampleFilter.new(location) - builder.add_example_filter(filter) + filter = LocationNodeFilter.new(location) + builder.add_node_filter(filter) end end diff --git a/src/spectator/example_filter.cr b/src/spectator/example_filter.cr deleted file mode 100644 index 5c040c3..0000000 --- a/src/spectator/example_filter.cr +++ /dev/null @@ -1,14 +0,0 @@ -module Spectator - # Base class for all example filters. - # Checks whether an example should be run. - # Sub-classes must implement the `#includes?` method. - abstract class ExampleFilter - # Checks if an example is in the filter, and should be run. - abstract def includes?(example : Example) : Bool - - # :ditto: - def ===(example : Example) - includes?(example) - end - end -end diff --git a/src/spectator/includes.cr b/src/spectator/includes.cr index d4fdaa6..d6c3628 100644 --- a/src/spectator/includes.cr +++ b/src/spectator/includes.cr @@ -7,7 +7,7 @@ require "./abstract_expression" require "./anything" require "./block" -require "./composite_example_filter" +require "./composite_node_filter" require "./config" require "./context" require "./context_delegate" @@ -17,7 +17,7 @@ require "./error_result" require "./example_context_delegate" require "./example_context_method" require "./example" -require "./example_filter" +require "./node_filter" require "./example_group" require "./example_group_hook" require "./example_hook" @@ -33,15 +33,15 @@ require "./hooks" require "./label" require "./lazy" require "./lazy_wrapper" -require "./line_example_filter" +require "./line_node_filter" require "./location" -require "./location_example_filter" +require "./location_node_filter" require "./matchers" require "./metadata" require "./mocks" -require "./name_example_filter" +require "./name_node_filter" require "./null_context" -require "./null_example_filter" +require "./null_node_filter" require "./pass_result" require "./pending_result" require "./profile" diff --git a/src/spectator/line_example_filter.cr b/src/spectator/line_example_filter.cr deleted file mode 100644 index 22a7c38..0000000 --- a/src/spectator/line_example_filter.cr +++ /dev/null @@ -1,17 +0,0 @@ -module Spectator - # Filter that matches examples on a given line. - class LineExampleFilter < ExampleFilter - # Creates the example filter. - def initialize(@line : Int32) - end - - # Checks whether the example satisfies the filter. - def includes?(example) : Bool - return false unless location = example.location? - - start_line = location.line - end_line = location.end_line - (start_line..end_line).covers?(@line) - end - end -end diff --git a/src/spectator/line_node_filter.cr b/src/spectator/line_node_filter.cr new file mode 100644 index 0000000..52213e4 --- /dev/null +++ b/src/spectator/line_node_filter.cr @@ -0,0 +1,19 @@ +require "./node_filter" + +module Spectator + # Filter that matches nodes on a given line. + class LineNodeFilter < NodeFilter + # Creates the node filter. + def initialize(@line : Int32) + end + + # Checks whether the node satisfies the filter. + def includes?(node) : Bool + return false unless location = node.location? + + start_line = location.line + end_line = location.end_line + (start_line..end_line).covers?(@line) + end + end +end diff --git a/src/spectator/location_example_filter.cr b/src/spectator/location_example_filter.cr deleted file mode 100644 index 0cae2bb..0000000 --- a/src/spectator/location_example_filter.cr +++ /dev/null @@ -1,14 +0,0 @@ -module Spectator - # Filter that matches examples in a given file and line. - class LocationExampleFilter < ExampleFilter - # Creates the filter. - # The *location* indicates which file and line the example must be on. - def initialize(@location : Location) - end - - # Checks whether the example satisfies the filter. - def includes?(example) : Bool - @location === example.location? - end - end -end diff --git a/src/spectator/location_node_filter.cr b/src/spectator/location_node_filter.cr new file mode 100644 index 0000000..3774db2 --- /dev/null +++ b/src/spectator/location_node_filter.cr @@ -0,0 +1,17 @@ +require "./location" +require "./node_filter" + +module Spectator + # Filter that matches nodes in a given file and line. + class LocationNodeFilter < NodeFilter + # Creates the filter. + # The *location* indicates which file and line the node must contain. + def initialize(@location : Location) + end + + # Checks whether the node satisfies the filter. + def includes?(node) : Bool + @location === node.location? + end + end +end diff --git a/src/spectator/name_example_filter.cr b/src/spectator/name_example_filter.cr deleted file mode 100644 index a90560c..0000000 --- a/src/spectator/name_example_filter.cr +++ /dev/null @@ -1,13 +0,0 @@ -module Spectator - # Filter that matches examples based on their name. - class NameExampleFilter < ExampleFilter - # Creates the example filter. - def initialize(@name : String) - end - - # Checks whether the example satisfies the filter. - def includes?(example) : Bool - @name == example.to_s - end - end -end diff --git a/src/spectator/name_node_filter.cr b/src/spectator/name_node_filter.cr new file mode 100644 index 0000000..6d4e64a --- /dev/null +++ b/src/spectator/name_node_filter.cr @@ -0,0 +1,15 @@ +require "./node_filter" + +module Spectator + # Filter that matches nodes based on their name. + class NameNodeFilter < NodeFilter + # Creates the node filter. + def initialize(@name : String) + end + + # Checks whether the node satisfies the filter. + def includes?(node) : Bool + @name == node.to_s + end + end +end diff --git a/src/spectator/node_filter.cr b/src/spectator/node_filter.cr new file mode 100644 index 0000000..6530d4a --- /dev/null +++ b/src/spectator/node_filter.cr @@ -0,0 +1,14 @@ +module Spectator + # Base class for all node filters. + # Checks whether a node should be included in the test run. + # Sub-classes must implement the `#includes?` method. + abstract class NodeFilter + # Checks if a node is in the filter, and should be included in the test run. + abstract def includes?(node) : Bool + + # :ditto: + def ===(node) + includes?(node) + end + end +end diff --git a/src/spectator/null_example_filter.cr b/src/spectator/null_example_filter.cr deleted file mode 100644 index 5a3e036..0000000 --- a/src/spectator/null_example_filter.cr +++ /dev/null @@ -1,9 +0,0 @@ -module Spectator - # Filter that matches all examples. - class NullExampleFilter < ExampleFilter - # Checks whether the example satisfies the filter. - def includes?(example) : Bool - true - end - end -end diff --git a/src/spectator/null_node_filter.cr b/src/spectator/null_node_filter.cr new file mode 100644 index 0000000..3ff5f7f --- /dev/null +++ b/src/spectator/null_node_filter.cr @@ -0,0 +1,11 @@ +require "./node_filter" + +module Spectator + # Filter that matches all nodes. + class NullNodeFilter < NodeFilter + # Checks whether the node satisfies the filter. + def includes?(_node) : Bool + true + end + end +end From 3803d4582c9569cce9c211e836b9c3676807a2d3 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 16 Aug 2021 20:27:21 -0600 Subject: [PATCH 369/399] Use Iterable(Node) instead of ExampleGroup --- src/spectator/example_iterator.cr | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/spectator/example_iterator.cr b/src/spectator/example_iterator.cr index f28a8bf..d767f31 100644 --- a/src/spectator/example_iterator.cr +++ b/src/spectator/example_iterator.cr @@ -1,5 +1,4 @@ require "./example" -require "./example_group" require "./node" module Spectator @@ -13,7 +12,7 @@ module Spectator # Creates a new iterator. # The *group* is the example group to iterate through. - def initialize(@group : ExampleGroup) + def initialize(@group : Iterable(Node)) iter = @group.each.as(Iterator(Node)) @stack = [iter] end @@ -55,7 +54,7 @@ module Spectator # Get the iterator from the top of the stack. # Advance the iterator and check what the next item is. case (item = top.next) - when ExampleGroup + when Iterable(Node) # If the next thing is a group, # we need to traverse its branch. # Push its iterator onto the stack and return. From 4e5a52215a18470ac48534cfb6146a62c4e251e3 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 16 Aug 2021 20:27:27 -0600 Subject: [PATCH 370/399] Simplify --- src/spectator/spec.cr | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/spectator/spec.cr b/src/spectator/spec.cr index 9aec3a0..a086171 100644 --- a/src/spectator/spec.cr +++ b/src/spectator/spec.cr @@ -24,9 +24,7 @@ module Spectator # Selects and shuffles the examples that should run. private def examples iterator = @config.iterator(@root) - iterator.to_a.tap do |examples| - @config.shuffle!(examples) - end + @config.shuffle!(iterator.to_a) end end end From 837e4cb85dc974cc116aadbcc0bd6841382245bc Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 17 Aug 2021 12:54:36 -0600 Subject: [PATCH 371/399] Implement Indexable(Node) on ExampleGroup --- src/spectator/example_group.cr | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index 3b174a2..1186ea0 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -5,9 +5,8 @@ require "./node" module Spectator # Collection of examples and sub-groups. class ExampleGroup < Node - include Enumerable(Node) include Hooks - include Iterable(Node) + include Indexable(Node) @nodes = [] of Node @@ -71,6 +70,8 @@ module Spectator group << self if group end + delegate size, unsafe_fetch, to: @nodes + # Creates a child that is attched to the group. # Yields zero or more times to create the child. # The group the child should be attached to is provided as a block argument. @@ -88,16 +89,6 @@ module Spectator @nodes.delete(node) end - # Yields each node (example and sub-group). - def each - @nodes.each { |node| yield node } - end - - # Returns an iterator for each (example and sub-group). - def each - @nodes.each - end - # Checks if all examples and sub-groups have finished. def finished? : Bool @nodes.all?(&.finished?) From cf7d67c972fd9fb0df02d428b8b2d479848fb1fb Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 17 Aug 2021 12:55:22 -0600 Subject: [PATCH 372/399] Simplify ExampleIterator and remove unecessary allocations --- src/spectator/example_iterator.cr | 57 ++++++++++--------------------- 1 file changed, 18 insertions(+), 39 deletions(-) diff --git a/src/spectator/example_iterator.cr b/src/spectator/example_iterator.cr index d767f31..3a4ba30 100644 --- a/src/spectator/example_iterator.cr +++ b/src/spectator/example_iterator.cr @@ -3,18 +3,17 @@ require "./node" module Spectator # Iterates through all examples in a group and its nested groups. + # Nodes are iterated in pre-order. class ExampleIterator include Iterator(Example) - # Stack that contains the iterators for each group. # A stack is used to track where in the tree this iterator is. - @stack : Array(Iterator(Node)) + @stack = Deque(Node).new(1) # Creates a new iterator. # The *group* is the example group to iterate through. - def initialize(@group : Iterable(Node)) - iter = @group.each.as(Iterator(Node)) - @stack = [iter] + def initialize(@group : Node) + @stack.push(@group) end # Retrieves the next `Example`. @@ -24,50 +23,30 @@ module Spectator # a. an example is found. # b. the stack is empty. until @stack.empty? - # Retrieve the next "thing". + # Retrieve the next node. # This could be an `Example` or a group. - item = advance - # Return the item if it's an example. + node = @stack.pop + + # If the node is a group, add its direct children to the queue + # in reverse order so that the tree is traversed in pre-order. + if node.is_a?(Indexable(Node)) + node.reverse_each { |child| @stack.push(child) } + end + + # Return the node if it's an example. # Otherwise, advance and check the next one. - return item if item.is_a?(Example) + return node if node.is_a?(Example) end + # Nothing left to iterate. stop end # Restart the iterator at the beginning. def rewind - # Same code as `#initialize`, but return self. - iter = @group.each.as(Iterator(Node)) - @stack = [iter] + @stack.clear + @stack.push(@group) self end - - # Retrieves the top of the stack. - private def top - @stack.last - end - - # Retrieves the next "thing" from the tree. - # This method will return an `Example` or "something else." - private def advance - # Get the iterator from the top of the stack. - # Advance the iterator and check what the next item is. - case (item = top.next) - when Iterable(Node) - # If the next thing is a group, - # we need to traverse its branch. - # Push its iterator onto the stack and return. - @stack.push(item.each) - when Iterator::Stop - # If a stop instance is encountered, - # then the current group is done. - # Pop its iterator from the stack and return. - @stack.pop - else - # Found an example, return it. - item - end - end end end From 67b4c0589b1bcd9357ba976731b47fcff74ead09 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 17 Aug 2021 12:55:49 -0600 Subject: [PATCH 373/399] Implement NodeIterator --- src/spectator/node_iterator.cr | 49 ++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/spectator/node_iterator.cr diff --git a/src/spectator/node_iterator.cr b/src/spectator/node_iterator.cr new file mode 100644 index 0000000..48610a7 --- /dev/null +++ b/src/spectator/node_iterator.cr @@ -0,0 +1,49 @@ +require "./node" + +module Spectator + # Iterates through all nodes in a group and its nested groups. + # Nodes are iterated in pre-order. + class NodeIterator + include Iterator(Node) + + # A stack is used to track where in the tree this iterator is. + @stack = Deque(Node).new(1) + + # Creates a new iterator. + # The *group* is the example group to iterate through. + def initialize(@group : Node) + @stack.push(@group) + end + + # Retrieves the next `Node`. + # If there are none left, then `Iterator::Stop` is returned. + def next + # Keep going until either: + # a. a node is found. + # b. the stack is empty. + until @stack.empty? + # Retrieve the next node. + node = @stack.pop + + # If the node is a group, add its direct children to the queue + # in reverse order so that the tree is traversed in pre-order. + if node.is_a?(Indexable(Node)) + node.reverse_each { |child| @stack.push(child) } + end + + # Return the current node. + return node + end + + # Nothing left to iterate. + stop + end + + # Restart the iterator at the beginning. + def rewind + @stack.clear + @stack.push(@group) + self + end + end +end From 65799fdd3bf2d6a6043a045919315d2e3441cd6c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 17 Aug 2021 13:49:58 -0600 Subject: [PATCH 374/399] Pass along end line --- src/spectator/dsl/groups.cr | 4 ++-- src/spectator/dsl/hooks.cr | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index d4de441..002860c 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -41,7 +41,7 @@ module Spectator::DSL ::Spectator::DSL::Builder.start_group( _spectator_group_name(\{{what}}), - ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}), + ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}, \{{block.end_line_number}}), metadata ) @@ -96,7 +96,7 @@ module Spectator::DSL \%collection, \{{collection.stringify}}, \{{block.args.empty? ? :nil.id : block.args.first.stringify}}, - ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}), + ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}, \{{block.end_line_number}}), metadata ) diff --git a/src/spectator/dsl/hooks.cr b/src/spectator/dsl/hooks.cr index d11ff08..928b3ee 100644 --- a/src/spectator/dsl/hooks.cr +++ b/src/spectator/dsl/hooks.cr @@ -26,7 +26,7 @@ module Spectator::DSL {% end %} ::Spectator::DSL::Builder.{{type.id}}( - ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}) + ::Spectator::Location.new(\{{block.filename}}, \{{block.line_number}}, \{{block.end_line_number}}) ) do {% if block %} %wrapper do |*args| From 94d5c96e7defeab2865f2d7907c3b0d579e7ac4a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 17 Aug 2021 14:09:37 -0600 Subject: [PATCH 375/399] Implement an example filter that supports matching groups Addresses https://gitlab.com/arctic-fox/spectator/-/issues/25 and https://github.com/icy-arctic-fox/spectator/issues/24 --- src/spectator/config.cr | 4 +- src/spectator/filtered_example_iterator.cr | 85 ++++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 src/spectator/filtered_example_iterator.cr diff --git a/src/spectator/config.cr b/src/spectator/config.cr index cd7175a..ca691b0 100644 --- a/src/spectator/config.cr +++ b/src/spectator/config.cr @@ -1,7 +1,7 @@ require "./config/*" require "./node_filter" require "./example_group" -require "./example_iterator" +require "./filtered_example_iterator" require "./formatting/formatter" require "./run_flags" @@ -86,7 +86,7 @@ module Spectator # Creates an iterator configured to select the filtered examples. def iterator(group : ExampleGroup) - ExampleIterator.new(group).select(@node_filter) + FilteredExampleIterator.new(group, @node_filter) end # Retrieves the configured random number generator. diff --git a/src/spectator/filtered_example_iterator.cr b/src/spectator/filtered_example_iterator.cr new file mode 100644 index 0000000..3286f3b --- /dev/null +++ b/src/spectator/filtered_example_iterator.cr @@ -0,0 +1,85 @@ +require "./example" +require "./node" +require "./node_filter" +require "./node_iterator" + +module Spectator + # Iterates through selected nodes in a group and its nested groups. + # Nodes are iterated in pre-order. + class FilteredExampleIterator + include Iterator(Example) + + # A stack is used to track where in the tree this iterator is. + @stack = Deque(Node).new(1) + + # A queue stores forced examples that have been matched by the a parent group. + @queue = Deque(Example).new + + # Creates a new iterator. + # The *group* is the example group to iterate through. + # The *filter* selects which examples (and groups) to iterate through. + def initialize(@group : Node, @filter : NodeFilter) + @stack.push(@group) + end + + # Retrieves the next selected `Example`. + # If there are none left, then `Iterator::Stop` is returned. + def next + # Return items from the queue first before continuing to the stack. + return @queue.shift unless @queue.empty? + + # Keep going until either: + # a. a suitable example is found. + # b. the stack is empty. + until @stack.empty? + # Retrieve the next node. + node = @stack.pop + + # If the node is a group, conditionally traverse it. + if node.is_a?(Indexable(Node)) + # To traverse, a child node or the group itself must match the filter. + return node if node = next_group_match(node) + elsif node.is_a?(Example) && @filter.includes?(node) + return node + end + end + + # Nothing left to iterate. + stop + end + + # Restart the iterator at the beginning. + def rewind + @stack.clear + @stack.push(@group) + @queue.clear + self + end + + # Attempts to find the next matching example in a group. + # If any child in the group matches, then traversal on the stack (down the tree) continues. + # However, if no children match, but the group itself does, then all examples in the group match. + # In the latter scenario, the examples are added to the queue, and the next item from the queue returned. + # Stack iteration should continue if nil is returned. + private def next_group_match(group : Indexable(Node)) : Example? + # Look for any children that match. + iterator = NodeIterator.new(group) + + # Check if any children match. + # Skip first node because its the group being checked. + if iterator.skip(1).any?(@filter) + # Add the group's direct children to the queue + # in reverse order so that the tree is traversed in pre-order. + group.reverse_each { |node| @stack.push(node) } + + # Check if the group matches, but no children match. + elsif @filter.includes?(group) + # Add examples from the group to the queue. + # Return the next example from the queue. + iterator.rewind.select(Example).each { |node| @queue.push(node) } + @queue.shift unless @queue.empty? + # If the queue is empty (group has no examples), go to next loop iteration of the stack. + end + end + end +end From 38ea2e7f96f9781f9de1d11b6d9970feae3eac32 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 17 Aug 2021 15:02:34 -0600 Subject: [PATCH 376/399] Address Ameba issue --- src/spectator/node_iterator.cr | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/spectator/node_iterator.cr b/src/spectator/node_iterator.cr index 48610a7..0dbc2bb 100644 --- a/src/spectator/node_iterator.cr +++ b/src/spectator/node_iterator.cr @@ -18,25 +18,20 @@ module Spectator # Retrieves the next `Node`. # If there are none left, then `Iterator::Stop` is returned. def next - # Keep going until either: - # a. a node is found. - # b. the stack is empty. - until @stack.empty? - # Retrieve the next node. - node = @stack.pop + # Nothing left to iterate. + return stop if @stack.empty? - # If the node is a group, add its direct children to the queue - # in reverse order so that the tree is traversed in pre-order. - if node.is_a?(Indexable(Node)) - node.reverse_each { |child| @stack.push(child) } - end + # Retrieve the next node. + node = @stack.pop - # Return the current node. - return node + # If the node is a group, add its direct children to the queue + # in reverse order so that the tree is traversed in pre-order. + if node.is_a?(Indexable(Node)) + node.reverse_each { |child| @stack.push(child) } end - # Nothing left to iterate. - stop + # Return the current node. + node end # Restart the iterator at the beginning. From b79dd4361e7ae002deae2126ea6d768a7a364b5f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 17 Aug 2021 20:52:06 -0600 Subject: [PATCH 377/399] Initial implementation of tag filtering --- src/spectator/config/cli_arguments_applicator.cr | 13 +++++++++++++ src/spectator/includes.cr | 1 + src/spectator/tag_node_filter.cr | 16 ++++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 src/spectator/tag_node_filter.cr diff --git a/src/spectator/config/cli_arguments_applicator.cr b/src/spectator/config/cli_arguments_applicator.cr index 6580fba..b760565 100644 --- a/src/spectator/config/cli_arguments_applicator.cr +++ b/src/spectator/config/cli_arguments_applicator.cr @@ -5,6 +5,7 @@ require "../line_node_filter" require "../location" require "../location_node_filter" require "../name_node_filter" +require "../tag_node_filter" module Spectator class Config @@ -105,6 +106,7 @@ module Spectator example_option(parser, builder) line_option(parser, builder) location_option(parser, builder) + tag_option(parser, builder) end # Adds the example filter option to the parser. @@ -135,6 +137,17 @@ module Spectator end end + # Adds the tag filter option to the parser. + private def tag_option(parser, builder) + parser.on("--tag TAG", "run examples with the specified TAG, or exclude examples by adding ~ before the TAG.") do |tag| + negated = tag.starts_with?('~') + tag = tag.lchop('~') + Log.debug { "Filtering for example with tag #{tag}" } + filter = TagNodeFilter.new(tag) + builder.add_node_filter(filter) + end + end + # Adds options to the parser for changing output. private def output_parser_options(parser, builder) verbose_option(parser, builder) diff --git a/src/spectator/includes.cr b/src/spectator/includes.cr index d6c3628..d649827 100644 --- a/src/spectator/includes.cr +++ b/src/spectator/includes.cr @@ -51,6 +51,7 @@ require "./runner_events" require "./runner" require "./spec_builder" require "./spec" +require "./tag_node_filter" require "./test_context" require "./value" require "./wrapper" diff --git a/src/spectator/tag_node_filter.cr b/src/spectator/tag_node_filter.cr new file mode 100644 index 0000000..0226439 --- /dev/null +++ b/src/spectator/tag_node_filter.cr @@ -0,0 +1,16 @@ +require "./node_filter" + +module Spectator + # Filter that matches nodes with a given tag. + class TagNodeFilter < NodeFilter + # Creates the filter. + # The *tag* indicates which tag the node must have in its metadata. + def initialize(@tag : String) + end + + # Checks whether the node satisfies the filter. + def includes?(node) : Bool + node.metadata.each_key.any? { |key| key.to_s == @tag } + end + end +end From a9a46c76ad09027b75764ff69541bfd05e3858e3 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 17 Aug 2021 21:34:26 -0600 Subject: [PATCH 378/399] Support tag filtering with value --- src/spectator/config/cli_arguments_applicator.cr | 9 +++++++-- src/spectator/tag_node_filter.cr | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/spectator/config/cli_arguments_applicator.cr b/src/spectator/config/cli_arguments_applicator.cr index b760565..8a33bbf 100644 --- a/src/spectator/config/cli_arguments_applicator.cr +++ b/src/spectator/config/cli_arguments_applicator.cr @@ -139,11 +139,16 @@ module Spectator # Adds the tag filter option to the parser. private def tag_option(parser, builder) - parser.on("--tag TAG", "run examples with the specified TAG, or exclude examples by adding ~ before the TAG.") do |tag| + parser.on("--tag TAG[:VALUE]", "Run examples with the specified TAG, or exclude examples by adding ~ before the TAG.") do |tag| negated = tag.starts_with?('~') tag = tag.lchop('~') Log.debug { "Filtering for example with tag #{tag}" } - filter = TagNodeFilter.new(tag) + parts = tag.split(':', 2, remove_empty: true) + if parts.size > 1 + tag = parts.first + value = parts.last + end + filter = TagNodeFilter.new(tag, value) builder.add_node_filter(filter) end end diff --git a/src/spectator/tag_node_filter.cr b/src/spectator/tag_node_filter.cr index 0226439..d360712 100644 --- a/src/spectator/tag_node_filter.cr +++ b/src/spectator/tag_node_filter.cr @@ -5,12 +5,12 @@ module Spectator class TagNodeFilter < NodeFilter # Creates the filter. # The *tag* indicates which tag the node must have in its metadata. - def initialize(@tag : String) + def initialize(@tag : String, @value : String? = nil) end # Checks whether the node satisfies the filter. def includes?(node) : Bool - node.metadata.each_key.any? { |key| key.to_s == @tag } + node.metadata.any? { |key, value| key.to_s == @tag && (!@value || value == @value) } end end end From ffb99a21d5d9094e9dd86d3929da01a3627556ec Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 17 Aug 2021 21:44:15 -0600 Subject: [PATCH 379/399] Update changelog to mention group matching --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7ebaa4..2035a59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support custom messages for failed expectations. [#28](https://gitlab.com/arctic-fox/spectator/-/issues/28) - Allow named arguments and assignments for `provided` (`given`) block. - Add `aggregate_failures` to capture and report multiple failed expectations. [#24](https://gitlab.com/arctic-fox/spectator/-/issues/24) +- Supports matching groups. [#25](https://gitlab.com/arctic-fox/spectator/-/issues/25) [#24](https://github.com/icy-arctic-fox/spectator/issues/24) ### Changed - `given` (now `provided`) blocks changed to produce a single example. `it` can no longer be nested in a `provided` block. From 8d366bf63706434f120f01c8901efc9917b814fd Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 17 Aug 2021 21:54:01 -0600 Subject: [PATCH 380/399] Fully implemented test filtering by tags Addresses https://gitlab.com/arctic-fox/spectator/-/issues/16 --- src/spectator/config.cr | 6 +++++- src/spectator/config/builder.cr | 17 +++++++++++++++++ .../config/cli_arguments_applicator.cr | 7 ++++++- src/spectator/null_node_filter.cr | 7 ++++++- 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/spectator/config.cr b/src/spectator/config.cr index ca691b0..efbdb6a 100644 --- a/src/spectator/config.cr +++ b/src/spectator/config.cr @@ -20,6 +20,9 @@ module Spectator # Filter used to select which examples to run. getter node_filter : NodeFilter + # Filter used to select which examples to _not_ run. + getter node_reject : NodeFilter + # List of hooks to run before all examples in the test suite. protected getter before_suite_hooks : Deque(ExampleGroupHook) @@ -49,6 +52,7 @@ module Spectator @run_flags = source.run_flags @random_seed = source.random_seed @node_filter = source.node_filter + @node_reject = source.node_reject @before_suite_hooks = source.before_suite_hooks @before_all_hooks = source.before_all_hooks @@ -86,7 +90,7 @@ module Spectator # Creates an iterator configured to select the filtered examples. def iterator(group : ExampleGroup) - FilteredExampleIterator.new(group, @node_filter) + FilteredExampleIterator.new(group, @node_filter).reject(@node_reject) end # Retrieves the configured random number generator. diff --git a/src/spectator/config/builder.cr b/src/spectator/config/builder.cr index f669923..99ce3c0 100644 --- a/src/spectator/config/builder.cr +++ b/src/spectator/config/builder.cr @@ -19,6 +19,7 @@ module Spectator @primary_formatter : Formatting::Formatter? @additional_formatters = [] of Formatting::Formatter @filters = [] of NodeFilter + @rejects = [] of NodeFilter # List of hooks to run before all examples in the test suite. protected getter before_suite_hooks = Deque(ExampleGroupHook).new @@ -263,6 +264,11 @@ module Spectator @filters << filter end + # Adds a filter to prevent examples from running. + def add_node_reject(filter : NodeFilter) + @rejects << filter + end + # Retrieves a filter that determines which examples can run. # If no filters were added with `#add_node_filter`, # then the returned filter will allow all examples to be run. @@ -273,6 +279,17 @@ module Spectator else CompositeNodeFilter.new(filters) end end + + # Retrieves a filter that prevents examples from running. + # If no filters were added with `#add_node_reject`, + # then the returned filter will allow all examples to be run. + protected def node_reject + case (filters = @rejects) + when .empty? then NullNodeFilter.new(false) + when .one? then filters.first + else CompositeNodeFilter.new(filters) + end + end end end end diff --git a/src/spectator/config/cli_arguments_applicator.cr b/src/spectator/config/cli_arguments_applicator.cr index 8a33bbf..1e2dae0 100644 --- a/src/spectator/config/cli_arguments_applicator.cr +++ b/src/spectator/config/cli_arguments_applicator.cr @@ -148,8 +148,13 @@ module Spectator tag = parts.first value = parts.last end + filter = TagNodeFilter.new(tag, value) - builder.add_node_filter(filter) + if negated + builder.add_node_reject(filter) + else + builder.add_node_filter(filter) + end end end diff --git a/src/spectator/null_node_filter.cr b/src/spectator/null_node_filter.cr index 3ff5f7f..cee8736 100644 --- a/src/spectator/null_node_filter.cr +++ b/src/spectator/null_node_filter.cr @@ -3,9 +3,14 @@ require "./node_filter" module Spectator # Filter that matches all nodes. class NullNodeFilter < NodeFilter + # Creates the filter. + # The *match* flag indicates whether all examples should match or not. + def initialize(@match : Bool = true) + end + # Checks whether the node satisfies the filter. def includes?(_node) : Bool - true + @match end end end From c209fe72593189962619b7327523a453177e3f94 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 17 Aug 2021 22:10:01 -0600 Subject: [PATCH 381/399] Add ascend methods --- src/spectator/example.cr | 9 +++++++++ src/spectator/example_group.cr | 12 +++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index eacf2e1..f9efc42 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -151,6 +151,15 @@ module Spectator klass.cast(@context) end + # Yields this example and all parent groups. + def ascend + node = self + while node + yield node + node = node.group? + end + end + # Constructs the full name or description of the example. # This prepends names of groups this example is part of. def to_s(io) diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index 1186ea0..e57b2df 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -72,11 +72,13 @@ module Spectator delegate size, unsafe_fetch, to: @nodes - # Creates a child that is attched to the group. - # Yields zero or more times to create the child. - # The group the child should be attached to is provided as a block argument. - def create_child - yield self + # Yields this group and all parent groups. + def ascend + group = self + while group + yield group + group = group.group? + end end # Removes the specified *node* from the group. From 1e82608500aa27b8446da3ffe24d627959888f23 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 17 Aug 2021 23:40:58 -0600 Subject: [PATCH 382/399] Add methods matching RSpec for configuring include and exclude tags --- src/spectator/config/builder.cr | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/spectator/config/builder.cr b/src/spectator/config/builder.cr index 99ce3c0..476da4d 100644 --- a/src/spectator/config/builder.cr +++ b/src/spectator/config/builder.cr @@ -3,6 +3,7 @@ require "../node_filter" require "../formatting" require "../null_node_filter" require "../run_flags" +require "../tag_node_filter" module Spectator class Config @@ -264,11 +265,23 @@ module Spectator @filters << filter end + # Specifies one or more tags to constrain running examples to. + def filter_run_including(*tags : Symbol, **values) + tags.each { |tag| @filters << TagNodeFilter.new(tag) } + values.each { |tag, value| @filters << TagNodeFilter.new(tag, value.to_s) } + end + # Adds a filter to prevent examples from running. def add_node_reject(filter : NodeFilter) @rejects << filter end + # Specifies one or more tags to exclude from running examples. + def filter_run_excluding(*tags : Symbol, **values) + tags.each { |tag| @rejects << TagNodeFilter.new(tag) } + values.each { |tag, value| @rejects << TagNodeFilter.new(tag, value.to_s) } + end + # Retrieves a filter that determines which examples can run. # If no filters were added with `#add_node_filter`, # then the returned filter will allow all examples to be run. From fd4812207a370f54e129eb090366fcf13fabcb23 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 18 Aug 2021 11:27:06 -0600 Subject: [PATCH 383/399] Whitespace --- src/spectator/config/builder.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/config/builder.cr b/src/spectator/config/builder.cr index 476da4d..522734f 100644 --- a/src/spectator/config/builder.cr +++ b/src/spectator/config/builder.cr @@ -300,7 +300,7 @@ module Spectator case (filters = @rejects) when .empty? then NullNodeFilter.new(false) when .one? then filters.first - else CompositeNodeFilter.new(filters) + else CompositeNodeFilter.new(filters) end end end From 2b27ea5a01e3a80147969ae4ea45e6add793315c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 18 Aug 2021 13:46:54 -0600 Subject: [PATCH 384/399] Quick implementation of `filter_run_when_matching` Needed for focus tests. --- src/spectator/config.cr | 20 +++++++++++++++++++- src/spectator/config/builder.cr | 9 +++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/spectator/config.cr b/src/spectator/config.cr index efbdb6a..66e6501 100644 --- a/src/spectator/config.cr +++ b/src/spectator/config.cr @@ -3,6 +3,7 @@ require "./node_filter" require "./example_group" require "./filtered_example_iterator" require "./formatting/formatter" +require "./node_iterator" require "./run_flags" module Spectator @@ -23,6 +24,9 @@ module Spectator # Filter used to select which examples to _not_ run. getter node_reject : NodeFilter + # Tags to filter on if they're present in a spec. + protected getter match_filters : Metadata + # List of hooks to run before all examples in the test suite. protected getter before_suite_hooks : Deque(ExampleGroupHook) @@ -53,6 +57,7 @@ module Spectator @random_seed = source.random_seed @node_filter = source.node_filter @node_reject = source.node_reject + @match_filters = source.match_filters @before_suite_hooks = source.before_suite_hooks @before_all_hooks = source.before_all_hooks @@ -90,7 +95,20 @@ module Spectator # Creates an iterator configured to select the filtered examples. def iterator(group : ExampleGroup) - FilteredExampleIterator.new(group, @node_filter).reject(@node_reject) + match_filter = match_filter(group) + iterator = FilteredExampleIterator.new(group, @node_filter) + iterator = iterator.select(match_filter) if match_filter + iterator.reject(@node_reject) + end + + # Creates a node filter if any conditionally matching filters apply to an example group. + private def match_filter(group : ExampleGroup) : NodeFilter? + iterator = NodeIterator.new(group) + filters = @match_filters.compact_map do |key, value| + filter = TagNodeFilter.new(key.to_s, value) + filter.as(NodeFilter) if iterator.rewind.any?(filter) + end + CompositeNodeFilter.new(filters) unless filters.empty? end # Retrieves the configured random number generator. diff --git a/src/spectator/config/builder.cr b/src/spectator/config/builder.cr index 522734f..7866fc6 100644 --- a/src/spectator/config/builder.cr +++ b/src/spectator/config/builder.cr @@ -1,6 +1,7 @@ require "../composite_node_filter" require "../node_filter" require "../formatting" +require "../metadata" require "../null_node_filter" require "../run_flags" require "../tag_node_filter" @@ -17,6 +18,8 @@ module Spectator # Toggles indicating how the test spec should execute. property run_flags = RunFlags::None + protected getter match_filters : Metadata = {:focus => nil.as(String?)} + @primary_formatter : Formatting::Formatter? @additional_formatters = [] of Formatting::Formatter @filters = [] of NodeFilter @@ -282,6 +285,12 @@ module Spectator values.each { |tag, value| @rejects << TagNodeFilter.new(tag, value.to_s) } end + # Specifies one or more tags to filter on only if they're present in the spec. + def filter_run_when_matching(*tags : Symbol, **values) + tags.each { |tag| @match_filters[tag] = nil } + values.each { |tag, value| @match_filters[tag] = value.to_s } + end + # Retrieves a filter that determines which examples can run. # If no filters were added with `#add_node_filter`, # then the returned filter will allow all examples to be run. From 21d14bd814d1b768a1967ea9e55d4eef5aa14230 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 18 Aug 2021 13:55:21 -0600 Subject: [PATCH 385/399] Add f-prefix variants of groups and examples For instance: `fit` for `it "...", :focus` --- CHANGELOG.md | 3 +++ src/spectator/dsl/examples.cr | 6 ++++++ src/spectator/dsl/groups.cr | 13 +++++++++++++ 3 files changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2035a59..9b6c053 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow named arguments and assignments for `provided` (`given`) block. - Add `aggregate_failures` to capture and report multiple failed expectations. [#24](https://gitlab.com/arctic-fox/spectator/-/issues/24) - Supports matching groups. [#25](https://gitlab.com/arctic-fox/spectator/-/issues/25) [#24](https://github.com/icy-arctic-fox/spectator/issues/24) +- Add `filter_run_including`, `filter_run_excluding`, and `filter_run_when_matching` to config block. +- By default, only run tests when any are marked with `focus: true`. +- Add "f-prefix" blocks for examples and groups (`fit`, `fdescribe`, etc.) as a short-hand for specifying `focus: true`. ### Changed - `given` (now `provided`) blocks changed to produce a single example. `it` can no longer be nested in a `provided` block. diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 38c62f0..03484ae 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -137,6 +137,12 @@ module Spectator::DSL define_example :specify + define_example :fexample, focus: true + + define_example :fit, focus: true + + define_example :fspecify, focus: true + @[Deprecated("Behavior of pending blocks will change in Spectator v0.11.0. Use `skip` instead.")] define_pending_example :pending diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 002860c..8f47b27 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -181,6 +181,12 @@ module Spectator::DSL define_example_group :xcontext, skip: "Temporarily skipped with xcontext" + define_example_group :fexample_group, focus: true + + define_example_group :fdescribe, focus: true + + define_example_group :fcontext, focus: true + # Defines a new iterative example group. # This type of group duplicates its contents for each element in *collection*. # @@ -197,6 +203,8 @@ module Spectator::DSL # :ditto: define_iterative_group :xsample, skip: "Temporarily skipped with xsample" + define_iterative_group :fsample, focus: true + # Defines a new iterative example group. # This type of group duplicates its contents for each element in *collection*. # This is the same as `#sample` except that the items are shuffled. @@ -218,5 +226,10 @@ module Spectator::DSL define_iterative_group :xrandom_sample, skip: "Temporarily skipped with xrandom_sample" do |collection| collection.to_a.shuffle(::Spectator.random) end + + # :ditto: + define_iterative_group :frandom_sample, focus: true do |collection| + collection.to_a.shuffle(::Spectator.random) + end end end From c49522a791982a3911fa7a2f147407ce0004b7d5 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 18 Aug 2021 15:57:22 -0600 Subject: [PATCH 386/399] Allow tags on top-level groups --- src/spectator/dsl/top.cr | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/spectator/dsl/top.cr b/src/spectator/dsl/top.cr index 467b415..e70dd8f 100644 --- a/src/spectator/dsl/top.cr +++ b/src/spectator/dsl/top.cr @@ -10,16 +10,21 @@ module Spectator::DSL # It can be any Crystal expression, # but is typically a class name or feature string. # The block should contain all of the examples for what is being described. + # # Example: # ``` # Spectator.describe Foo do # # Your examples for `Foo` go here. # end # ``` + # + # Tags can be specified by adding symbols (keywords) after the first argument. + # Key-value pairs can also be specified. + # # NOTE: Inside the block, the `Spectator` prefix _should not_ be used. - macro {{method.id}}(description, &block) + macro {{method.id}}(description, *tags, **metadata, &block) class ::SpectatorTestContext - {{method.id}}(\{{description}}) \{{block}} + {{method.id}}(\{{description}}, \{{tags.splat(", ")}} \{{metadata.double_splat}}) \{{block}} end end {% end %} From 01d1a8736ee4404cac3cd538499f3eb9d46956fc Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 18 Aug 2021 15:57:39 -0600 Subject: [PATCH 387/399] Mark slow compilation tests --- spec/runtime_example_spec.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/runtime_example_spec.cr b/spec/runtime_example_spec.cr index 324f7ea..01ae9a3 100644 --- a/spec/runtime_example_spec.cr +++ b/spec/runtime_example_spec.cr @@ -9,7 +9,7 @@ require "./spec_helper" # Some specs are too complex to be ran normally. # Additionally, this allows examples to easily check specific failure cases. # Plus, it makes testing user-reported issues easy. -Spectator.describe "Runtime compilation" do +Spectator.describe "Runtime compilation", :slow, :compile do given_example passing_example do it "does something" do expect(true).to be_true From f4fc599a1d0c32eacb17cbd7794aaabfa9957e58 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 18 Aug 2021 17:47:20 -0600 Subject: [PATCH 388/399] Add display_name convenience method --- src/spectator/node.cr | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/spectator/node.cr b/src/spectator/node.cr index 4979f6e..c5a64b6 100644 --- a/src/spectator/node.cr +++ b/src/spectator/node.cr @@ -59,10 +59,15 @@ module Spectator Tags.new(metadata.keys) end + # Non-nil name used to show the node name. + def display_name + @name || "" + end + # Constructs the full name or description of the node. # This prepends names of groups this node is part of. def to_s(io) - (@name || "").to_s(io) + display_name.to_s(io) end # Exposes information about the node useful for debugging. From dd0ef01369475d1d5d99b31dd57ff7abf584515c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 18 Aug 2021 17:50:09 -0600 Subject: [PATCH 389/399] 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 From 18b42304c993741ccf8324a271353cf3337ad5d9 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 18 Aug 2021 18:18:18 -0600 Subject: [PATCH 390/399] Use lists and fix some issues --- src/spectator/formatting/html/body.ecr | 45 +++++++++++++------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/src/spectator/formatting/html/body.ecr b/src/spectator/formatting/html/body.ecr index 4266fb5..239c6b4 100644 --- a/src/spectator/formatting/html/body.ecr +++ b/src/spectator/formatting/html/body.ecr @@ -1,43 +1,43 @@

Test Results

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

Failures

+

Failures

+
+ <%- end -%> <%- if report.counts.pending > 0 -%> -
-

Pending

+

Pending

+
+ <%- end -%> -
-

Examples

+

Examples

+
    <%- report.examples.each do |example| -%> -
    +
  • <% escape(example) %>

    <%= example.result %> <% escape(runtime(example.result.elapsed)) %> - <% if result = example.result.as?(PendingResult) %><% escape(result.reason) -%> + <% if result = example.result.as?(PendingResult) %><% escape(result.reason) %> <%- elsif result = example.result.as?(ErrorResult) -%> @@ -59,10 +59,11 @@ <%- if example.result.expectations.empty? -%> No expectations reported <%- else -%> -
    +

    Expectations

    +
      <%- example.result.expectations.each do |expectation| -%> -
      "> -

      <% escape(expectation.description) %>

      +
    1. "> +
      <% escape(expectation.description) %>
      <%- if expectation.satisfied? -%> pass <%- else -%> @@ -76,10 +77,10 @@ <%- end -%> <% if location = expectation.location? %><% escape(location) %><% end %> -
    2. + <%- end -%> -
    + <%- end -%> -
  • + <%- end -%> -
+ From babc7ebc3a1898e8ba93b6e8dd99469358596836 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 18 Aug 2021 22:15:50 -0600 Subject: [PATCH 391/399] Tweaks --- src/spectator/formatting/html/body.ecr | 10 +++++----- src/spectator/formatting/html/head.ecr | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/spectator/formatting/html/body.ecr b/src/spectator/formatting/html/body.ecr index 239c6b4..c239d50 100644 --- a/src/spectator/formatting/html/body.ecr +++ b/src/spectator/formatting/html/body.ecr @@ -10,7 +10,7 @@ <%- report.failures.each do |example| -%>
  • -

    <% escape(example) %>

    + <% escape(example) %>
  • <%- end -%> @@ -23,7 +23,7 @@ <%- report.pending.each do |example| -%>
  • -

    <% escape(example) %>

    + <% escape(example) %>
  • <%- end -%> @@ -37,6 +37,7 @@

    <% escape(example) %>

    <%= example.result %> <% escape(runtime(example.result.elapsed)) %> + <% if location = example.location? %><% escape(location) %><% end %> <% if result = example.result.as?(PendingResult) %><% escape(result.reason) %> <%- elsif result = example.result.as?(ErrorResult) -%> @@ -63,12 +64,12 @@
      <%- example.result.expectations.each do |expectation| -%>
    1. "> -
      <% escape(expectation.description) %>
      + title="<% escape(location) %>"<% end %>><% escape(expectation.description) %> <%- if expectation.satisfied? -%> pass <%- else -%> fail - <% escape(expectation.failure_message) %> +

      <% escape(expectation.failure_message) %>

      <%- expectation.values.each do |key, value| -%>
      <% escape(key) %>
      @@ -76,7 +77,6 @@ <%- end -%>
      <%- end -%> - <% if location = expectation.location? %><% escape(location) %><% end %>
    2. <%- end -%>
    diff --git a/src/spectator/formatting/html/head.ecr b/src/spectator/formatting/html/head.ecr index 68570b3..118085a 100644 --- a/src/spectator/formatting/html/head.ecr +++ b/src/spectator/formatting/html/head.ecr @@ -4,9 +4,9 @@ + Test Results - From 708fd692aec6546faed6ac752eea49eca907af36 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 19 Aug 2021 11:46:07 -0600 Subject: [PATCH 392/399] Add actual label to match data description --- src/spectator/matchers/all_matcher.cr | 2 +- src/spectator/matchers/array_matcher.cr | 8 ++++---- src/spectator/matchers/attributes_matcher.cr | 8 ++++---- .../matchers/change_exact_matcher.cr | 14 ++++++------- src/spectator/matchers/change_from_matcher.cr | 12 +++++------ src/spectator/matchers/change_matcher.cr | 8 ++++---- .../matchers/change_relative_matcher.cr | 6 +++--- src/spectator/matchers/change_to_matcher.cr | 6 +++--- src/spectator/matchers/contain_matcher.cr | 8 ++++---- src/spectator/matchers/end_with_matcher.cr | 16 +++++++-------- src/spectator/matchers/exception_matcher.cr | 20 +++++++++---------- src/spectator/matchers/have_matcher.cr | 16 +++++++-------- src/spectator/matchers/matcher.cr | 12 +++++++++++ src/spectator/matchers/predicate_matcher.cr | 8 ++++---- src/spectator/matchers/respond_matcher.cr | 8 ++++---- src/spectator/matchers/standard_matcher.cr | 8 ++++---- src/spectator/matchers/start_with_matcher.cr | 16 +++++++-------- .../matchers/unordered_array_matcher.cr | 8 ++++---- 18 files changed, 98 insertions(+), 86 deletions(-) diff --git a/src/spectator/matchers/all_matcher.cr b/src/spectator/matchers/all_matcher.cr index becefad..404b66d 100644 --- a/src/spectator/matchers/all_matcher.cr +++ b/src/spectator/matchers/all_matcher.cr @@ -26,7 +26,7 @@ module Spectator::Matchers match_data = matcher.match(element) break match_data unless match_data.matched? end - found || SuccessfulMatchData.new(description) + found || SuccessfulMatchData.new(match_data_description(actual)) end # Negated matching for this matcher is not supported. diff --git a/src/spectator/matchers/array_matcher.cr b/src/spectator/matchers/array_matcher.cr index bb355a3..6adf442 100644 --- a/src/spectator/matchers/array_matcher.cr +++ b/src/spectator/matchers/array_matcher.cr @@ -32,10 +32,10 @@ module Spectator::Matchers if missing.empty? && extra.empty? # Contents are identical. - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) else # Content differs. - FailedMatchData.new(description, "#{actual.label} does not contain exactly #{expected.label}", + FailedMatchData.new(match_data_description(actual), "#{actual.label} does not contain exactly #{expected.label}", expected: expected_elements.inspect, actual: actual_elements.inspect, missing: missing.empty? ? "None" : missing.inspect, @@ -56,13 +56,13 @@ module Spectator::Matchers if missing.empty? && extra.empty? # Contents are identical. - FailedMatchData.new(description, "#{actual.label} contains exactly #{expected.label}", + FailedMatchData.new(match_data_description(actual), "#{actual.label} contains exactly #{expected.label}", expected: "Not #{expected_elements.inspect}", actual: actual_elements.inspect ) else # Content differs. - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) end end diff --git a/src/spectator/matchers/attributes_matcher.cr b/src/spectator/matchers/attributes_matcher.cr index 86349b4..30ddb38 100644 --- a/src/spectator/matchers/attributes_matcher.cr +++ b/src/spectator/matchers/attributes_matcher.cr @@ -28,9 +28,9 @@ module Spectator::Matchers def match(actual : Expression(T)) : MatchData forall T snapshot = snapshot_values(actual.value) if match?(snapshot) - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) else - FailedMatchData.new(description, "#{actual.label} does not have attributes #{expected.label}", values(snapshot).to_a) + FailedMatchData.new(match_data_description(actual), "#{actual.label} does not have attributes #{expected.label}", values(snapshot).to_a) end end @@ -39,9 +39,9 @@ module Spectator::Matchers def negated_match(actual : Expression(T)) : MatchData forall T snapshot = snapshot_values(actual.value) if match?(snapshot) - FailedMatchData.new(description, "#{actual.label} has attributes #{expected.label}", negated_values(snapshot).to_a) + FailedMatchData.new(match_data_description(actual), "#{actual.label} has attributes #{expected.label}", negated_values(snapshot).to_a) else - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) end end diff --git a/src/spectator/matchers/change_exact_matcher.cr b/src/spectator/matchers/change_exact_matcher.cr index 94c391a..2f0880e 100644 --- a/src/spectator/matchers/change_exact_matcher.cr +++ b/src/spectator/matchers/change_exact_matcher.cr @@ -30,21 +30,21 @@ module Spectator::Matchers before, after = change(actual) if expected_before == before if before == after - FailedMatchData.new(description, "#{actual.label} did not change #{expression.label}", + FailedMatchData.new(match_data_description(actual), "#{actual.label} did not change #{expression.label}", before: before.inspect, after: after.inspect ) elsif expected_after == after - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) else - FailedMatchData.new(description, "#{actual.label} did not change #{expression.label} to #{expected_after.inspect}", + FailedMatchData.new(match_data_description(actual), "#{actual.label} did not change #{expression.label} to #{expected_after.inspect}", before: before.inspect, after: after.inspect, expected: expected_after.inspect ) end else - FailedMatchData.new(description, "#{expression.label} was not initially #{expected_before.inspect}", + FailedMatchData.new(match_data_description(actual), "#{expression.label} was not initially #{expected_before.inspect}", expected: expected_before.inspect, actual: before.inspect, ) @@ -57,15 +57,15 @@ module Spectator::Matchers before, after = change(actual) if expected_before == before if expected_after == after - FailedMatchData.new(description, "#{actual.label} changed #{expression.label} from #{expected_before.inspect} to #{expected_after.inspect}", + FailedMatchData.new(match_data_description(actual), "#{actual.label} changed #{expression.label} from #{expected_before.inspect} to #{expected_after.inspect}", before: before.inspect, after: after.inspect ) else - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) end else - FailedMatchData.new(description, "#{expression.label} was not initially #{expected_before.inspect}", + FailedMatchData.new(match_data_description(actual), "#{expression.label} was not initially #{expected_before.inspect}", expected: expected_before.inspect, actual: before.inspect, ) diff --git a/src/spectator/matchers/change_from_matcher.cr b/src/spectator/matchers/change_from_matcher.cr index cfbea50..e684e00 100644 --- a/src/spectator/matchers/change_from_matcher.cr +++ b/src/spectator/matchers/change_from_matcher.cr @@ -27,18 +27,18 @@ module Spectator::Matchers def match(actual : Expression(T)) : MatchData forall T before, after = change(actual) if expected != before - FailedMatchData.new(description, "#{expression.label} was not initially #{expected}", + FailedMatchData.new(match_data_description(actual), "#{expression.label} was not initially #{expected}", expected: expected.inspect, actual: before.inspect, ) elsif before == after - FailedMatchData.new(description, "#{actual.label} did not change #{expression.label} from #{expected}", + FailedMatchData.new(match_data_description(actual), "#{actual.label} did not change #{expression.label} from #{expected}", before: before.inspect, after: after.inspect, expected: "Not #{expected.inspect}" ) else - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) end end @@ -47,14 +47,14 @@ module Spectator::Matchers def negated_match(actual : Expression(T)) : MatchData forall T before, after = change(actual) if expected != before - FailedMatchData.new(description, "#{expression.label} was not initially #{expected}", + FailedMatchData.new(match_data_description(actual), "#{expression.label} was not initially #{expected}", expected: expected.inspect, actual: before.inspect ) elsif before == after - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) else - FailedMatchData.new(description, "#{actual.label} changed #{expression.label} from #{expected}", + FailedMatchData.new(match_data_description(actual), "#{actual.label} changed #{expression.label} from #{expected}", before: before.inspect, after: after.inspect, expected: expected.inspect diff --git a/src/spectator/matchers/change_matcher.cr b/src/spectator/matchers/change_matcher.cr index 1f60ac5..3be8111 100644 --- a/src/spectator/matchers/change_matcher.cr +++ b/src/spectator/matchers/change_matcher.cr @@ -25,12 +25,12 @@ module Spectator::Matchers def match(actual : Expression(T)) : MatchData forall T before, after = change(actual) if before == after - FailedMatchData.new(description, "#{actual.label} did not change #{expression.label}", + FailedMatchData.new(match_data_description(actual), "#{actual.label} did not change #{expression.label}", before: before.inspect, after: after.inspect ) else - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) end end @@ -39,9 +39,9 @@ module Spectator::Matchers def negated_match(actual : Expression(T)) : MatchData forall T before, after = change(actual) if before == after - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) else - FailedMatchData.new(description, "#{actual.label} changed #{expression.label}", + FailedMatchData.new(match_data_description(actual), "#{actual.label} changed #{expression.label}", before: before.inspect, after: after.inspect ) diff --git a/src/spectator/matchers/change_relative_matcher.cr b/src/spectator/matchers/change_relative_matcher.cr index 539c971..8511dd4 100644 --- a/src/spectator/matchers/change_relative_matcher.cr +++ b/src/spectator/matchers/change_relative_matcher.cr @@ -25,14 +25,14 @@ module Spectator::Matchers def match(actual : Expression(T)) : MatchData forall T before, after = change(actual) if before == after - FailedMatchData.new(description, "#{actual.label} did not change #{expression.label}", + FailedMatchData.new(match_data_description(actual), "#{actual.label} did not change #{expression.label}", before: before.inspect, after: after.inspect ) elsif @evaluator.call(before, after) - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) else - FailedMatchData.new(description, "#{actual.label} did not change #{expression.label} #{@relativity}", + FailedMatchData.new(match_data_description(actual), "#{actual.label} did not change #{expression.label} #{@relativity}", before: before.inspect, after: after.inspect ) diff --git a/src/spectator/matchers/change_to_matcher.cr b/src/spectator/matchers/change_to_matcher.cr index 51504ec..57deff5 100644 --- a/src/spectator/matchers/change_to_matcher.cr +++ b/src/spectator/matchers/change_to_matcher.cr @@ -27,15 +27,15 @@ module Spectator::Matchers def match(actual : Expression(T)) : MatchData forall T before, after = change(actual) if before == after - FailedMatchData.new(description, "#{actual.label} did not change #{expression.label}", + FailedMatchData.new(match_data_description(actual), "#{actual.label} did not change #{expression.label}", before: before.inspect, after: after.inspect, expected: expected.inspect ) elsif expected == after - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) else - FailedMatchData.new(description, "#{actual.label} did not change #{expression.label} to #{expected}", + FailedMatchData.new(match_data_description(actual), "#{actual.label} did not change #{expression.label} to #{expected}", before: before.inspect, after: after.inspect, expected: expected.inspect diff --git a/src/spectator/matchers/contain_matcher.cr b/src/spectator/matchers/contain_matcher.cr index fe21182..7ebe221 100644 --- a/src/spectator/matchers/contain_matcher.cr +++ b/src/spectator/matchers/contain_matcher.cr @@ -29,10 +29,10 @@ module Spectator::Matchers if missing.empty? # Contents are present. - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) else # Content is missing. - FailedMatchData.new(description, "#{actual.label} does not contain #{expected.label}", + FailedMatchData.new(match_data_description(actual), "#{actual.label} does not contain #{expected.label}", expected: expected.value.inspect, actual: actual_value.inspect, missing: missing.inspect, @@ -52,13 +52,13 @@ module Spectator::Matchers if satisfied # Contents are present. - FailedMatchData.new(description, "#{actual.label} contains #{expected.label}", + FailedMatchData.new(match_data_description(actual), "#{actual.label} contains #{expected.label}", expected: "Not #{expected.value.inspect}", actual: actual_value.inspect ) else # Content is missing. - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) end end diff --git a/src/spectator/matchers/end_with_matcher.cr b/src/spectator/matchers/end_with_matcher.cr index 7ff3cd8..039d182 100644 --- a/src/spectator/matchers/end_with_matcher.cr +++ b/src/spectator/matchers/end_with_matcher.cr @@ -46,9 +46,9 @@ module Spectator::Matchers # This method expects (and uses) the `#ends_with?` method on the value. private def match_ends_with(actual_value, actual_label) if actual_value.ends_with?(expected.value) - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual_label)) else - FailedMatchData.new(description, "#{actual_label} does not end with #{expected.label} (using #ends_with?)", + FailedMatchData.new(match_data_description(actual_label), "#{actual_label} does not end with #{expected.label} (using #ends_with?)", expected: expected.value.inspect, actual: actual_value.inspect ) @@ -62,9 +62,9 @@ module Spectator::Matchers last = list.last if expected.value === last - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual_label)) else - FailedMatchData.new(description, "#{actual_label} does not end with #{expected.label} (using expected === last)", + FailedMatchData.new(match_data_description(actual_label), "#{actual_label} does not end with #{expected.label} (using expected === last)", expected: expected.value.inspect, actual: last.inspect, list: list.inspect @@ -76,12 +76,12 @@ module Spectator::Matchers # This method expects (and uses) the `#ends_with?` method on the value. private def negated_match_ends_with(actual_value, actual_label) if actual_value.ends_with?(expected.value) - FailedMatchData.new(description, "#{actual_label} ends with #{expected.label} (using #ends_with?)", + FailedMatchData.new(match_data_description(actual_label), "#{actual_label} ends with #{expected.label} (using #ends_with?)", expected: "Not #{expected.value.inspect}", actual: actual_value.inspect ) else - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual_label)) end end @@ -92,13 +92,13 @@ module Spectator::Matchers last = list.last if expected.value === last - FailedMatchData.new(description, "#{actual_label} ends with #{expected.label} (using expected === last)", + FailedMatchData.new(match_data_description(actual_label), "#{actual_label} ends with #{expected.label} (using expected === last)", expected: "Not #{expected.value.inspect}", actual: last.inspect, list: list.inspect ) else - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual_label)) end end end diff --git a/src/spectator/matchers/exception_matcher.cr b/src/spectator/matchers/exception_matcher.cr index a27412b..adec663 100644 --- a/src/spectator/matchers/exception_matcher.cr +++ b/src/spectator/matchers/exception_matcher.cr @@ -33,16 +33,16 @@ module Spectator::Matchers def match(actual : Expression(T)) : MatchData forall T exception = capture_exception { actual.value } if exception.nil? - FailedMatchData.new(description, "#{actual.label} did not raise", expected: ExceptionType.inspect) + FailedMatchData.new(match_data_description(actual), "#{actual.label} did not raise", expected: ExceptionType.inspect) else if exception.is_a?(ExceptionType) if (value = expected.value).nil? - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) else if value === exception.message - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) else - FailedMatchData.new(description, "#{actual.label} raised #{exception.class}, but the message is not #{expected.label}", + FailedMatchData.new(match_data_description(actual), "#{actual.label} raised #{exception.class}, but the message is not #{expected.label}", "expected type": ExceptionType.inspect, "actual type": exception.class.inspect, "expected message": value.inspect, @@ -51,7 +51,7 @@ module Spectator::Matchers end end else - FailedMatchData.new(description, "#{actual.label} did not raise #{ExceptionType}", + FailedMatchData.new(match_data_description(actual), "#{actual.label} did not raise #{ExceptionType}", expected: ExceptionType.inspect, actual: exception.class.inspect ) @@ -64,28 +64,28 @@ module Spectator::Matchers def negated_match(actual : Expression(T)) : MatchData forall T exception = capture_exception { actual.value } if exception.nil? - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) else if exception.is_a?(ExceptionType) if (value = expected.value).nil? - FailedMatchData.new(description, "#{actual.label} raised #{exception.class}", + FailedMatchData.new(match_data_description(actual), "#{actual.label} raised #{exception.class}", expected: "Not #{ExceptionType}", actual: exception.class.inspect ) else if value === exception.message - FailedMatchData.new(description, "#{actual.label} raised #{exception.class} with message matching #{expected.label}", + FailedMatchData.new(match_data_description(actual), "#{actual.label} raised #{exception.class} with message matching #{expected.label}", "expected type": ExceptionType.inspect, "actual type": exception.class.inspect, "expected message": value.inspect, "actual message": exception.message.to_s ) else - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) end end else - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) end end end diff --git a/src/spectator/matchers/have_matcher.cr b/src/spectator/matchers/have_matcher.cr index 46cd446..9c16c9f 100644 --- a/src/spectator/matchers/have_matcher.cr +++ b/src/spectator/matchers/have_matcher.cr @@ -39,10 +39,10 @@ module Spectator::Matchers if missing.empty? # Contents are present. - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual_label)) else # Content is missing. - FailedMatchData.new(description, "#{actual_label} does not have #{expected.label}", + FailedMatchData.new(match_data_description(actual_label), "#{actual_label} does not have #{expected.label}", expected: expected.value.inspect, actual: actual_value.inspect, missing: missing.inspect, @@ -58,9 +58,9 @@ module Spectator::Matchers end if missing.empty? - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual_label)) else - FailedMatchData.new(description, "#{actual_label} does not have #{expected.label}", + FailedMatchData.new(match_data_description(actual_label), "#{actual_label} does not have #{expected.label}", expected: expected.value.inspect, actual: actual_value.inspect, missing: missing.inspect, @@ -89,13 +89,13 @@ module Spectator::Matchers if satisfied # Contents are present. - FailedMatchData.new(description, "#{actual_label} has #{expected.label}", + FailedMatchData.new(match_data_description(actual_label), "#{actual_label} has #{expected.label}", expected: "Not #{expected.value.inspect}", actual: actual_value.inspect ) else # Content is missing. - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual_label)) end end @@ -107,9 +107,9 @@ module Spectator::Matchers end if satisfied - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual_label)) else - FailedMatchData.new(description, "#{actual_label} does not have #{expected.label}", + FailedMatchData.new(match_data_description(actual_label), "#{actual_label} does not have #{expected.label}", expected: expected.value.inspect, actual: actual_value.inspect, missing: missing.inspect, diff --git a/src/spectator/matchers/matcher.cr b/src/spectator/matchers/matcher.cr index 898d319..e54e55e 100644 --- a/src/spectator/matchers/matcher.cr +++ b/src/spectator/matchers/matcher.cr @@ -21,5 +21,17 @@ module Spectator::Matchers # Performs the test against the expression (value or block), but inverted. # A successful match with `#match` should normally fail for this method, and vice-versa. abstract def negated_match(actual : Expression(T)) : MatchData forall T + + private def match_data_description(actual : Expression(T)) : String forall T + match_data_description(actual.label) + end + + private def match_data_description(actual_label : String | Symbol) : String + "#{actual_label} #{description}" + end + + private def match_data_description(actual_label : Nil) : String + description + end end end diff --git a/src/spectator/matchers/predicate_matcher.cr b/src/spectator/matchers/predicate_matcher.cr index da77dc8..ebf1d87 100644 --- a/src/spectator/matchers/predicate_matcher.cr +++ b/src/spectator/matchers/predicate_matcher.cr @@ -24,9 +24,9 @@ module Spectator::Matchers def match(actual : Expression(T)) : MatchData forall T snapshot = snapshot_values(actual.value) if match?(snapshot) - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) else - FailedMatchData.new(description, "#{actual.label} is not #{expected.label}", values(snapshot).to_a) + FailedMatchData.new(match_data_description(actual), "#{actual.label} is not #{expected.label}", values(snapshot).to_a) end end @@ -35,9 +35,9 @@ module Spectator::Matchers def negated_match(actual : Expression(T)) : MatchData forall T snapshot = snapshot_values(actual.value) if match?(snapshot) - FailedMatchData.new(description, "#{actual.label} is #{expected.label}", values(snapshot).to_a) + FailedMatchData.new(match_data_description(actual), "#{actual.label} is #{expected.label}", values(snapshot).to_a) else - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) end end diff --git a/src/spectator/matchers/respond_matcher.cr b/src/spectator/matchers/respond_matcher.cr index 1f73f57..9712da5 100644 --- a/src/spectator/matchers/respond_matcher.cr +++ b/src/spectator/matchers/respond_matcher.cr @@ -17,9 +17,9 @@ module Spectator::Matchers def match(actual : Expression(T)) : MatchData forall T snapshot = snapshot_values(actual.value) if snapshot.values.all? - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) else - FailedMatchData.new(description, "#{actual.label} does not respond to #{label}", values(snapshot).to_a) + FailedMatchData.new(match_data_description(actual), "#{actual.label} does not respond to #{label}", values(snapshot).to_a) end end @@ -29,9 +29,9 @@ module Spectator::Matchers snapshot = snapshot_values(actual.value) # Intentionally check truthiness of each value. if snapshot.values.any? # ameba:disable Performance/AnyInsteadOfEmpty - FailedMatchData.new(description, "#{actual.label} responds to #{label}", values(snapshot).to_a) + FailedMatchData.new(match_data_description(actual), "#{actual.label} responds to #{label}", values(snapshot).to_a) else - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) end end diff --git a/src/spectator/matchers/standard_matcher.cr b/src/spectator/matchers/standard_matcher.cr index 5b9f026..43992ec 100644 --- a/src/spectator/matchers/standard_matcher.cr +++ b/src/spectator/matchers/standard_matcher.cr @@ -25,9 +25,9 @@ module Spectator::Matchers # Additionally, `#failure_message` and `#values` are called for a failed match. def match(actual : Expression(T)) : MatchData forall T if match?(actual) - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) else - FailedMatchData.new(description, failure_message(actual), values(actual).to_a) + FailedMatchData.new(match_data_description(actual), failure_message(actual), values(actual).to_a) end end @@ -41,9 +41,9 @@ module Spectator::Matchers def negated_match(actual : Expression(T)) : MatchData forall T # TODO: Invert description. if does_not_match?(actual) - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) else - FailedMatchData.new(description, failure_message_when_negated(actual), negated_values(actual).to_a) + FailedMatchData.new(match_data_description(actual), failure_message_when_negated(actual), negated_values(actual).to_a) end end diff --git a/src/spectator/matchers/start_with_matcher.cr b/src/spectator/matchers/start_with_matcher.cr index e8d0059..4bfc6c8 100644 --- a/src/spectator/matchers/start_with_matcher.cr +++ b/src/spectator/matchers/start_with_matcher.cr @@ -45,9 +45,9 @@ module Spectator::Matchers # This method expects (and uses) the `#starts_with?` method on the value. private def match_starts_with(actual_value, actual_label) if actual_value.starts_with?(expected.value) - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual_label)) else - FailedMatchData.new(description, "#{actual_label} does not start with #{expected.label} (using #starts_with?)", + FailedMatchData.new(match_data_description(actual_label), "#{actual_label} does not start with #{expected.label} (using #starts_with?)", expected: expected.value.inspect, actual: actual_value.inspect ) @@ -61,9 +61,9 @@ module Spectator::Matchers first = list.first if expected.value === first - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual_label)) else - FailedMatchData.new(description, "#{actual_label} does not start with #{expected.label} (using expected === first)", + FailedMatchData.new(match_data_description(actual_label), "#{actual_label} does not start with #{expected.label} (using expected === first)", expected: expected.value.inspect, actual: first.inspect, list: list.inspect @@ -75,12 +75,12 @@ module Spectator::Matchers # This method expects (and uses) the `#starts_with?` method on the value. private def negated_match_starts_with(actual_value, actual_label) if actual_value.starts_with?(expected.value) - FailedMatchData.new(description, "#{actual_label} starts with #{expected.label} (using #starts_with?)", + FailedMatchData.new(match_data_description(actual_label), "#{actual_label} starts with #{expected.label} (using #starts_with?)", expected: "Not #{expected.value.inspect}", actual: actual_value.inspect ) else - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual_label)) end end @@ -91,13 +91,13 @@ module Spectator::Matchers first = list.first if expected.value === first - FailedMatchData.new(description, "#{actual_label} starts with #{expected.label} (using expected === first)", + FailedMatchData.new(match_data_description(actual_label), "#{actual_label} starts with #{expected.label} (using expected === first)", expected: "Not #{expected.value.inspect}", actual: first.inspect, list: list.inspect ) else - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual_label)) end end end diff --git a/src/spectator/matchers/unordered_array_matcher.cr b/src/spectator/matchers/unordered_array_matcher.cr index aeb8b2b..510465f 100644 --- a/src/spectator/matchers/unordered_array_matcher.cr +++ b/src/spectator/matchers/unordered_array_matcher.cr @@ -28,9 +28,9 @@ module Spectator::Matchers missing, extra = array_diff(expected_elements, actual_elements) if missing.empty? && extra.empty? - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) else - FailedMatchData.new(description, "#{actual.label} does not contain #{expected.label} (unordered)", + FailedMatchData.new(match_data_description(actual), "#{actual.label} does not contain #{expected.label} (unordered)", expected: expected_elements.inspect, actual: actual_elements.inspect, missing: missing.inspect, @@ -50,12 +50,12 @@ module Spectator::Matchers missing, extra = array_diff(expected_elements, actual_elements) if missing.empty? && extra.empty? - FailedMatchData.new(description, "#{actual.label} contains #{expected.label} (unordered)", + FailedMatchData.new(match_data_description(actual), "#{actual.label} contains #{expected.label} (unordered)", expected: "Not #{expected_elements.inspect}", actual: actual_elements.inspect, ) else - SuccessfulMatchData.new(description) + SuccessfulMatchData.new(match_data_description(actual)) end end From efca1409c68347e08add8b9c5db689e7604dfb8d Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 19 Aug 2021 13:00:51 -0600 Subject: [PATCH 393/399] Fix dumb --- src/spectator/formatting/components/totals.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/formatting/components/totals.cr b/src/spectator/formatting/components/totals.cr index bd65ead..941b6ee 100644 --- a/src/spectator/formatting/components/totals.cr +++ b/src/spectator/formatting/components/totals.cr @@ -34,11 +34,11 @@ module Spectator::Formatting::Components def to_s(io) io << @examples << " examples, " << @failures << " failures" - if @errors > 1 + if @errors > 0 io << " (" << @errors << " errors)" end - if @pending > 1 + if @pending > 0 io << ", " << @pending << " pending" end end From f5ec9ccff620912dc3542d5cee024c05642f3827 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 19 Aug 2021 13:07:15 -0600 Subject: [PATCH 394/399] CSS! The HTML report looks beautiful. --- src/spectator/formatting/html/body.ecr | 34 ++--- src/spectator/formatting/html/head.ecr | 201 +++++++++++++++++++++++++ 2 files changed, 218 insertions(+), 17 deletions(-) diff --git a/src/spectator/formatting/html/body.ecr b/src/spectator/formatting/html/body.ecr index c239d50..05c683f 100644 --- a/src/spectator/formatting/html/body.ecr +++ b/src/spectator/formatting/html/body.ecr @@ -5,8 +5,8 @@ <%- if report.counts.fail > 0 -%> -

    Failures

    -
      +

      Failures (<%= report.counts.fail %>)

      +
        <%- report.failures.each do |example| -%>
      1. @@ -14,12 +14,12 @@
      2. <%- end -%> -
    + <%- end -%> <%- if report.counts.pending > 0 -%> -

    Pending

    -
      +

      Pending (<%= report.counts.pending %>)

      +
        <%- report.pending.each do |example| -%>
      1. @@ -27,24 +27,24 @@
      2. <%- end -%> -
    + <%- end -%> -

    Examples

    +

    Examples (<%= report.counts.total %>)

      <%- report.examples.each do |example| -%>
    • <% escape(example) %>

      <%= example.result %> - <% escape(runtime(example.result.elapsed)) %> + Took <% escape(runtime(example.result.elapsed)) %> <% if location = example.location? %><% escape(location) %><% end %> - <% if result = example.result.as?(PendingResult) %><% escape(result.reason) %> + <% if result = example.result.as?(PendingResult) %>

      <% escape(result.reason) %>

      <%- elsif result = example.result.as?(ErrorResult) -%> - - <% escape(result.error.class) %> +

      + <% escape(result.error.class) %> <% escape(result.error.message) %> - +

      <%- if backtrace = result.error.backtrace? -%>
      <%- backtrace.each do |line| -%> @@ -54,7 +54,7 @@ <%- end -%> <%- elsif result = example.result.as?(FailResult) -%> - <% escape(result.error.message) %> +

      <% escape(result.error.message) %> <%- end -%> <%- if example.result.expectations.empty? -%> @@ -63,12 +63,12 @@

      Expectations

        <%- example.result.expectations.each do |expectation| -%> -
      1. "> - title="<% escape(location) %>"<% end %>><% escape(expectation.description) %> +
      2. "<% if location = expectation.location? %> title="<% escape(location) %>"<% end %>> + <% escape(expectation.description) %> <%- if expectation.satisfied? -%> - pass + pass <%- else -%> - fail + fail

        <% escape(expectation.failure_message) %>

        <%- expectation.values.each do |key, value| -%> diff --git a/src/spectator/formatting/html/head.ecr b/src/spectator/formatting/html/head.ecr index 118085a..a311aaa 100644 --- a/src/spectator/formatting/html/head.ecr +++ b/src/spectator/formatting/html/head.ecr @@ -7,6 +7,207 @@ Test Results + + From e037e42fa74a73a77ef2f338d234a3a37171149c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 19 Aug 2021 13:14:53 -0600 Subject: [PATCH 395/399] Fix links to examples not working --- src/spectator/formatting/html/body.ecr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/formatting/html/body.ecr b/src/spectator/formatting/html/body.ecr index 05c683f..9537921 100644 --- a/src/spectator/formatting/html/body.ecr +++ b/src/spectator/formatting/html/body.ecr @@ -33,8 +33,8 @@

        Examples (<%= report.counts.total %>)

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

          <% escape(example) %>

          +
        • +

          <% escape(example) %>

          <%= example.result %> Took <% escape(runtime(example.result.elapsed)) %> <% if location = example.location? %><% escape(location) %><% end %> From 2f6ef4b57805267dcf0535b7fb300e7c2d6305d3 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 19 Aug 2021 13:16:50 -0600 Subject: [PATCH 396/399] Fix accidental check-in from testing --- spec/line_number_spec.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/line_number_spec.cr b/spec/line_number_spec.cr index 90491a5..5439ea0 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__ - 3) + expect(location.line).to eq(__LINE__ - 2) # 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. From 5d90a99d8e505ea2ee93f272028617b6316bcbd4 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 19 Aug 2021 13:35:15 -0600 Subject: [PATCH 397/399] Update README and CHANGELOG with recent changes Attempt to clarify confusion around https://github.com/icy-arctic-fox/spectator/issues/22 --- CHANGELOG.md | 1 + README.md | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b6c053..4dfbdaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `filter_run_including`, `filter_run_excluding`, and `filter_run_when_matching` to config block. - By default, only run tests when any are marked with `focus: true`. - Add "f-prefix" blocks for examples and groups (`fit`, `fdescribe`, etc.) as a short-hand for specifying `focus: true`. +- Add HTML formatter. Operates the same as the JUnit formatter. Specify `--html_output=DIR` to use. [#22](https://gitlab.com/arctic-fox/spectator/-/issues/22) [#3](https://github.com/icy-arctic-fox/spectator/issues/3) ### Changed - `given` (now `provided`) blocks changed to produce a single example. `it` can no longer be nested in a `provided` block. diff --git a/README.md b/README.md index 7fea4f5..a366c9b 100644 --- a/README.md +++ b/README.md @@ -277,13 +277,14 @@ For details on mocks and doubles, see the [wiki](https://gitlab.com/arctic-fox/s Spectator matches Crystal's default Spec output with some minor changes. JUnit and TAP are also supported output formats. -There is also a highly detailed JSON output. +There are also highly detailed JSON and HTML outputs. Development ----------- -This shard is still under development and is not recommended for production use (same as Crystal). -However, feel free to play around with it and use it for non-critical projects. +This shard is still in active development. +New features are being added and existing functionality improved. +Spectator is well-tested, but may have some yet-to-be-found bugs. ### Feature Progress @@ -344,20 +345,20 @@ Items not marked as completed may have partial implementations. - [ ] Message ordering - `expect().to receive().ordered` - [X] Null doubles - [X] Verifying doubles -- [ ] Runner +- [X] Runner - [X] Fail fast - - [ ] Test filtering - by name, context, and tags + - [X] Test filtering - by name, context, and tags - [X] Fail on no tests - [X] Randomize test order - [X] Dry run - for validation and checking formatted output - [X] Config block in `spec_helper.cr` - [X] Config file - `.spectator` -- [ ] Reporter and formatting +- [X] Reporter and formatting - [X] RSpec/Crystal Spec default - [X] JSON - [X] JUnit - [X] TAP - - [ ] HTML + - [X] HTML ### How it Works (in a nutshell) @@ -383,9 +384,10 @@ The CI build checks for properly formatted code. Documentation is automatically generated and published to GitLab pages. It can be found here: https://arctic-fox.gitlab.io/spectator -This project is developed on [GitLab](https://gitlab.com/arctic-fox/spectator), -and mirrored to [GitHub](https://github.com/icy-arctic-fox/spectator). -Issues and PRs/MRs are accepted on both. +This project's home is (and primarily developed) on [GitLab](https://gitlab.com/arctic-fox/spectator). +A mirror is maintained to [GitHub](https://github.com/icy-arctic-fox/spectator). +Issues, pull requests (merge requests), and discussion are welcome on both. +Maintainers will ensure your contributions make it in. ### Testing From 09414e611b53b27802398716c4412662f7c08133 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 19 Aug 2021 13:45:22 -0600 Subject: [PATCH 398/399] Brighten yellow for pending --- src/spectator/formatting/html/head.ecr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/formatting/html/head.ecr b/src/spectator/formatting/html/head.ecr index a311aaa..f34fe42 100644 --- a/src/spectator/formatting/html/head.ecr +++ b/src/spectator/formatting/html/head.ecr @@ -34,7 +34,7 @@ body { } #summary.pending, #pending-list, #example-list li.example.pending { - border-left: #aaaa00 solid 0.5em; + border-left: #eeee00 solid 0.5em; } .totals { @@ -54,7 +54,7 @@ body { } .result.pending { - color: #aaaa00; + color: #666666; } span { From 09dcbdb383c5f20ac27d2605ac5fe014a252da6f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 19 Aug 2021 13:55:58 -0600 Subject: [PATCH 399/399] Fix dumb with JUnit and HTML output --- src/spectator/config/cli_arguments_applicator.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/spectator/config/cli_arguments_applicator.cr b/src/spectator/config/cli_arguments_applicator.cr index a775568..496fc1a 100644 --- a/src/spectator/config/cli_arguments_applicator.cr +++ b/src/spectator/config/cli_arguments_applicator.cr @@ -166,6 +166,7 @@ module Spectator json_option(parser, builder) tap_option(parser, builder) junit_option(parser, builder) + html_option(parser, builder) no_color_option(parser, builder) end @@ -219,7 +220,7 @@ module Spectator end # Adds the HTML output option to the parser. - private def junit_option(parser, builder) + private def html_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)