From a49f8eaa71af70e1af3af56827c3605d03e349d8 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 26 Aug 2019 21:04:54 -0600 Subject: [PATCH 001/205] Add Mocks and Doubles to README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index a828f2a..ae6e0e5 100644 --- a/README.md +++ b/README.md @@ -309,6 +309,12 @@ Items not marked as completed may have partial implementations. - [X] `change[.by|.from[.to]|.to|.by_at_least|.by_at_most]` - [X] `have_attributes` - [ ] Compound - `and`, `or` +- [ ] Mocks and Doubles + - [ ] Mocks (Stub real types) + - [ ] Doubles (Stand-ins for real types) + - [ ] Method stubs + - [ ] Spies + - [ ] Null doubles - [ ] Runner - [X] Fail fast - [ ] Test filtering - by name, context, and tags From 4abf97139b007454532ed90297a896365c7d39c1 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 26 Aug 2019 21:06:06 -0600 Subject: [PATCH 002/205] Experimenting with auto-mocking methods --- src/spectator/dsl/mock_dsl.cr | 5 +++++ src/spectator/dsl/structure_dsl.cr | 18 ++++++++++++++++++ src/spectator/includes.cr | 2 ++ src/spectator/mock.cr | 30 ++++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+) create mode 100644 src/spectator/dsl/mock_dsl.cr create mode 100644 src/spectator/mock.cr diff --git a/src/spectator/dsl/mock_dsl.cr b/src/spectator/dsl/mock_dsl.cr new file mode 100644 index 0000000..92b02da --- /dev/null +++ b/src/spectator/dsl/mock_dsl.cr @@ -0,0 +1,5 @@ +module Spectator::DSL + module MockDSL + + end +end diff --git a/src/spectator/dsl/structure_dsl.cr b/src/spectator/dsl/structure_dsl.cr index 1957d03..d88f4d6 100644 --- a/src/spectator/dsl/structure_dsl.cr +++ b/src/spectator/dsl/structure_dsl.cr @@ -1419,6 +1419,24 @@ module Spectator::DSL end end + macro mock(type) + {% real_type = type.resolve %} + {% if real_type < ::Reference %} + class ::{{real_type.name.id}} + include ::Spectator::Mock + end + {% elsif real_type < ::Value %} + struct ::{{real_type.name.id}} + include ::Spectator::Mock + end + {% else %} + module ::{{real_type.name.id}} + include ::Spectator::Mock + end + {% end %} + {% debug %} + end + # Creates an example, or a test case. # The *what* argument describes "what" is being tested or asserted. # The block contains the code to run the test. diff --git a/src/spectator/includes.cr b/src/spectator/includes.cr index 9861c89..ef5d06f 100644 --- a/src/spectator/includes.cr +++ b/src/spectator/includes.cr @@ -30,6 +30,8 @@ require "./example_group" require "./nested_example_group" require "./root_example_group" +require "./mock" + require "./config" require "./config_builder" require "./config_source" diff --git a/src/spectator/mock.cr b/src/spectator/mock.cr new file mode 100644 index 0000000..76df55b --- /dev/null +++ b/src/spectator/mock.cr @@ -0,0 +1,30 @@ +module Spectator + module Mock + macro included + {% for meth in @type.methods %} + {% if meth.visibility != :public %}{{meth.visibility.id}} {% end %}def {{meth.name.id}}( + {% for arg, i in meth.args %} + {% if meth.splat_index && i == meth.splat_index %} + *{{arg}}{% if i + (meth.accepts_block? ? 0 : 1) < meth.args.size %},{% end %} + {% else %} + {{arg}}{% if i + (meth.accepts_block? ? 0 : 1) < meth.args.size %},{% end %} + {% end %} + {% end %} + {% if meth.accepts_block? %}&{% if meth.block_arg %}{{meth.block_arg}}{% else %}__spec_block{% end %}{% end %} + ){% if meth.return_type %} : {{meth.return_type}}{% end %} + previous_def( + {% for arg, i in meth.args %} + {% if !meth.splat_index || i < meth.splat_index %} + {{arg.name.id}}{% if i + (meth.accepts_block? ? 0 : 1) < meth.args.size %},{% end %} + {% elsif meth.splat_index && i > meth.splat_index %} + {{arg.name.id}}: {{arg.name}}{% if i + (meth.accepts_block? ? 0 : 1) < meth.args.size %},{% end %} + {% end %} + {% end %} + {% if meth.accepts_block? %}&{% if meth.block_arg %}{{meth.block_arg}}{% else %}__spec_block{% end %}{% end %} + ) + end + {% end %} + {% debug %} + end + end +end From b32421c6bba143b603dff97506176b76337b26a4 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 26 Aug 2019 21:07:00 -0600 Subject: [PATCH 003/205] Update version to 0.9.0 --- shard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shard.yml b/shard.yml index 9065082..540d719 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: spectator -version: 0.8.2 +version: 0.9.0 description: | A feature-rich spec testing framework for Crystal with similarities to RSpec. From 579c52f28ce2003258de2442b5203858ae2ab2e5 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 26 Aug 2019 21:08:11 -0600 Subject: [PATCH 004/205] Update to newer Ameba --- shard.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shard.yml b/shard.yml index 540d719..77b397c 100644 --- a/shard.yml +++ b/shard.yml @@ -12,5 +12,5 @@ license: MIT development_dependencies: ameba: - github: veelenga/ameba - version: ~> 0.9 + github: crystal-ameba/ameba + version: ~> 0.10 From 70e01364cefe2d882afda338fdaf8d42b8af1780 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 26 Aug 2019 21:11:21 -0600 Subject: [PATCH 005/205] Address Ameba issue --- src/spectator/dsl/mock_dsl.cr | 1 - 1 file changed, 1 deletion(-) diff --git a/src/spectator/dsl/mock_dsl.cr b/src/spectator/dsl/mock_dsl.cr index 92b02da..8b165ba 100644 --- a/src/spectator/dsl/mock_dsl.cr +++ b/src/spectator/dsl/mock_dsl.cr @@ -1,5 +1,4 @@ module Spectator::DSL module MockDSL - end end From 3462bdea1a93d1f6877fbe303ddb7c1da47575e1 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 26 Aug 2019 21:49:47 -0600 Subject: [PATCH 006/205] Some initial double code --- src/spectator/double.cr | 9 +++++++++ src/spectator/dsl/structure_dsl.cr | 16 ++++++++++++++++ src/spectator/includes.cr | 1 + 3 files changed, 26 insertions(+) create mode 100644 src/spectator/double.cr diff --git a/src/spectator/double.cr b/src/spectator/double.cr new file mode 100644 index 0000000..cea1123 --- /dev/null +++ b/src/spectator/double.cr @@ -0,0 +1,9 @@ +module Spectator + module Double + macro stub(definition) + def {{definition.name.id}} + {{definition.block.body}} + end + end + end +end diff --git a/src/spectator/dsl/structure_dsl.cr b/src/spectator/dsl/structure_dsl.cr index d88f4d6..1e4ac0c 100644 --- a/src/spectator/dsl/structure_dsl.cr +++ b/src/spectator/dsl/structure_dsl.cr @@ -1437,6 +1437,22 @@ module Spectator::DSL {% debug %} end + macro double(name, &block) + {% if block.is_a?(Nop) %} + # Create an instance of the double. + Double{{name.id}}.new + {% else %} + # Define a double. + struct Double{{name.id}} # TODO: Use fresh variable %double + include ::Spectator::Double + + {{block.body}} + end + # TODO: Register double in current context. + {% end %} + {% debug %} + end + # Creates an example, or a test case. # The *what* argument describes "what" is being tested or asserted. # The block contains the code to run the test. diff --git a/src/spectator/includes.cr b/src/spectator/includes.cr index ef5d06f..65e97c8 100644 --- a/src/spectator/includes.cr +++ b/src/spectator/includes.cr @@ -31,6 +31,7 @@ require "./nested_example_group" require "./root_example_group" require "./mock" +require "./double" require "./config" require "./config_builder" From 6e9633d001c3e658b6081e8e3438378b8ee8c4c6 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 26 Aug 2019 22:48:15 -0600 Subject: [PATCH 007/205] Hacked in double instance creation --- src/spectator/double.cr | 2 +- src/spectator/dsl/builder.cr | 5 +++++ src/spectator/dsl/double_factory.cr | 17 +++++++++++++++++ src/spectator/dsl/example_group_builder.cr | 6 ++++++ .../dsl/nested_example_group_builder.cr | 2 +- src/spectator/dsl/root_example_group_builder.cr | 2 +- .../dsl/sample_example_group_builder.cr | 4 ++-- src/spectator/dsl/structure_dsl.cr | 8 ++++---- src/spectator/example.cr | 4 +++- src/spectator/example_group.cr | 6 +++++- src/spectator/internals/harness.cr | 4 ++++ src/spectator/nested_example_group.cr | 4 ++-- 12 files changed, 51 insertions(+), 13 deletions(-) create mode 100644 src/spectator/dsl/double_factory.cr diff --git a/src/spectator/double.cr b/src/spectator/double.cr index cea1123..a50be76 100644 --- a/src/spectator/double.cr +++ b/src/spectator/double.cr @@ -1,5 +1,5 @@ module Spectator - module Double + abstract struct Double macro stub(definition) def {{definition.name.id}} {{definition.block.body}} diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr index a836559..beb56ff 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -101,6 +101,11 @@ module Spectator::DSL current_group.add_post_condition(block) end + def add_double(id : Symbol, double_type : Double.class) : Nil + double_factory = DoubleFactory.new(double_type) + current_group.add_double(id, double_factory) + 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 diff --git a/src/spectator/dsl/double_factory.cr b/src/spectator/dsl/double_factory.cr new file mode 100644 index 0000000..e55c2f3 --- /dev/null +++ b/src/spectator/dsl/double_factory.cr @@ -0,0 +1,17 @@ +require "../double" + +module Spectator::DSL + # Creates instances of doubles from a specified type. + class DoubleFactory + # Creates the factory. + # The type passed to this constructor must be a double. + def initialize(@double_type : Double.class) + end + + # Constructs a new double instance and returns it. + # The *sample_values* are passed to `Double#initialize`. + def build(sample_values : Internals::SampleValues) : Double + @double_type.new + end + end +end diff --git a/src/spectator/dsl/example_group_builder.cr b/src/spectator/dsl/example_group_builder.cr index cc4e662..f8cf8d0 100644 --- a/src/spectator/dsl/example_group_builder.cr +++ b/src/spectator/dsl/example_group_builder.cr @@ -7,6 +7,8 @@ module Spectator::DSL # and the root example group can't be a child. alias Child = ExampleFactory | NestedExampleGroupBuilder + private getter doubles = {} of Symbol => DoubleFactory + # Factories and builders for all examples and groups. @children = [] of Child @@ -63,6 +65,10 @@ module Spectator::DSL @post_conditions << block end + def add_double(id : Symbol, double_factory : DoubleFactory) : Nil + @doubles[id] = double_factory + end + # Constructs an `ExampleHooks` instance with all the hooks defined for this group. # This method should be called only when the group is being built, # otherwise some hooks may be missing. diff --git a/src/spectator/dsl/nested_example_group_builder.cr b/src/spectator/dsl/nested_example_group_builder.cr index a711d74..31aef42 100644 --- a/src/spectator/dsl/nested_example_group_builder.cr +++ b/src/spectator/dsl/nested_example_group_builder.cr @@ -26,7 +26,7 @@ module Spectator::DSL # The *parent* should be the group that contains this group. # The *sample_values* will be given to all of the examples (and groups) nested in this group. def build(parent : ExampleGroup, sample_values : Internals::SampleValues) : NestedExampleGroup - NestedExampleGroup.new(@what, parent, hooks, conditions).tap do |group| + NestedExampleGroup.new(@what, parent, hooks, conditions, doubles).tap do |group| # Set the group's children to built versions of the children from this instance. group.children = @children.map do |child| # Build the child and up-cast to prevent type errors. diff --git a/src/spectator/dsl/root_example_group_builder.cr b/src/spectator/dsl/root_example_group_builder.cr index d6df2fd..8345000 100644 --- a/src/spectator/dsl/root_example_group_builder.cr +++ b/src/spectator/dsl/root_example_group_builder.cr @@ -6,7 +6,7 @@ module Spectator::DSL # Creates a `RootExampleGroup` which can have instances of `Example` and `ExampleGroup` nested in it. # The *sample_values* will be given to all of the examples (and groups) nested in this group. def build(sample_values : Internals::SampleValues) : RootExampleGroup - RootExampleGroup.new(hooks, conditions).tap do |group| + RootExampleGroup.new(hooks, conditions, doubles).tap do |group| # Set the group's children to built versions of the children from this instance. group.children = @children.map do |child| # Build the child and up-cast to prevent type errors. diff --git a/src/spectator/dsl/sample_example_group_builder.cr b/src/spectator/dsl/sample_example_group_builder.cr index 62c0746..5a03798 100644 --- a/src/spectator/dsl/sample_example_group_builder.cr +++ b/src/spectator/dsl/sample_example_group_builder.cr @@ -43,7 +43,7 @@ module Spectator::DSL # This creates the container for the sub-groups. # The hooks are defined here, instead of repeating for each sub-group. - NestedExampleGroup.new(@what, parent, hooks, conditions).tap do |group| + NestedExampleGroup.new(@what, parent, hooks, conditions, doubles).tap do |group| # Set the container group's children to be sub-groups for each item in the collection. group.children = collection.map do |value| # Create a sub-group for each item in the collection. @@ -61,7 +61,7 @@ module Spectator::DSL private def build_sub_group(parent : ExampleGroup, sample_values : Internals::SampleValues, value : T) : NestedExampleGroup # Add the value to sample values for this sub-group. sub_values = sample_values.add(@symbol, @name, value) - NestedExampleGroup.new(value.to_s, parent, ExampleHooks.empty, ExampleConditions.empty).tap do |group| + NestedExampleGroup.new(value.to_s, parent, ExampleHooks.empty, ExampleConditions.empty, {} of Symbol => DoubleFactory).tap do |group| # Set the sub-group's children to built versions of the children from this instance. group.children = @children.map do |child| # Build the child and up-cast to prevent type errors. diff --git a/src/spectator/dsl/structure_dsl.cr b/src/spectator/dsl/structure_dsl.cr index 1e4ac0c..7ac7221 100644 --- a/src/spectator/dsl/structure_dsl.cr +++ b/src/spectator/dsl/structure_dsl.cr @@ -1440,15 +1440,15 @@ module Spectator::DSL macro double(name, &block) {% if block.is_a?(Nop) %} # Create an instance of the double. - Double{{name.id}}.new + ::Spectator::Internals::Harness.current.double({{name.id.symbolize}}).as(Double{{name.id}}) {% else %} # Define a double. - struct Double{{name.id}} # TODO: Use fresh variable %double - include ::Spectator::Double + struct Double{{name.id}} < ::Spectator::Double # TODO: Use fresh variable %double + #include {{@type.id}} # Include parent type to allow contextual methods in the double. {{block.body}} end - # TODO: Register double in current context. + ::Spectator::DSL::Builder.add_double({{name.id.symbolize}}, Double{{name.id}}) {% end %} {% debug %} end diff --git a/src/spectator/example.cr b/src/spectator/example.cr index d12363e..4c18cf8 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -16,6 +16,8 @@ module Spectator # Source where the example originated from. abstract def source : Source + protected getter sample_values : Internals::SampleValues + # Runs the example code. # A result is returned, which represents the outcome of the test. # An example can be run only once. @@ -32,7 +34,7 @@ module Spectator # Creates the base of the example. # The group should be the example group the example belongs to. # The *sample_values* are passed to the example code. - def initialize(@group, sample_values : Internals::SampleValues) + def initialize(@group, @sample_values) end # Indicates there is only one example to run. diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index 6e25a93..21732dd 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -17,7 +17,7 @@ module Spectator # Creates the example group. # The hooks are stored to be triggered later. - def initialize(@hooks : ExampleHooks, @conditions : ExampleConditions) + def initialize(@hooks : ExampleHooks, @conditions : ExampleConditions, @doubles : Hash(Symbol, DSL::DoubleFactory)) @before_all_hooks_run = false @after_all_hooks_run = false end @@ -40,6 +40,10 @@ module Spectator @example_count = children.sum(&.example_count) end + def double(id, sample_values) + @doubles[id].build(sample_values) + end + # Yields each direct descendant. def each children.each do |child| diff --git a/src/spectator/internals/harness.cr b/src/spectator/internals/harness.cr index b9de70b..5207b37 100644 --- a/src/spectator/internals/harness.cr +++ b/src/spectator/internals/harness.cr @@ -46,6 +46,10 @@ module Spectator::Internals @reporter.expectations end + def double(id) + example.group.double(id, example.sample_values) + end + # Creates a new harness. # The example the harness is for should be passed in. private def initialize(@example) diff --git a/src/spectator/nested_example_group.cr b/src/spectator/nested_example_group.cr index 0a90b86..a32d30c 100644 --- a/src/spectator/nested_example_group.cr +++ b/src/spectator/nested_example_group.cr @@ -18,8 +18,8 @@ module Spectator # The parent's children must contain this group, # otherwise there may be unexpected behavior. # The *hooks* are stored to be triggered later. - def initialize(@what, @parent, hooks : ExampleHooks, conditions : ExampleConditions) - super(hooks, conditions) + def initialize(@what, @parent, hooks : ExampleHooks, conditions : ExampleConditions, doubles : Hash(Symbol, DSL::DoubleFactory)) + super(hooks, conditions, doubles) end # Indicates wheter the group references a type. From 64045171c21e73e25ca835227bd81861cbeeb514 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 30 Aug 2019 12:39:03 -0600 Subject: [PATCH 008/205] Change contexts from modules to classes --- src/spectator.cr | 4 +-- src/spectator/dsl/structure_dsl.cr | 47 ++++++++---------------------- 2 files changed, 14 insertions(+), 37 deletions(-) diff --git a/src/spectator.cr b/src/spectator.cr index 5f17921..aa590d8 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -36,8 +36,8 @@ module Spectator # # For more information on how the DSL works, see the `DSL` module. - # Root-level module that contains all examples and example groups. - module SpectatorExamples + # Root-level class that contains all examples and example groups. + class SpectatorExamples # Include the DSL for creating groups, example, and more. include ::Spectator::DSL::StructureDSL diff --git a/src/spectator/dsl/structure_dsl.cr b/src/spectator/dsl/structure_dsl.cr index 1957d03..1d3b421 100644 --- a/src/spectator/dsl/structure_dsl.cr +++ b/src/spectator/dsl/structure_dsl.cr @@ -238,13 +238,9 @@ module Spectator::DSL # it demonstrates how contexts can reuse code. # Contexts also make it clearer how a scenario is setup. macro context(what, &block) - # Module for the context. - # The module uses a generated unique name. - module Context%context - # Include the parent module. - # Since `@type` resolves immediately, - # this will reference the parent type. - include {{@type.id}} + # Class for the context. + # The class uses a generated unique name. + class Context%context < {{@type.id}} # Check if `what` looks like a type. # If it is, add the `#described_class` and `subject` methods. @@ -507,10 +503,7 @@ module Spectator::DSL # This has to be a class that includes the parent module. # The collection could reference a helper method # or anything else in the parent scope. - class Sample%sample - # Include the parent module. - include {{@type.id}} - + class Sample%sample < {{@type.id}} # Placeholder initializer. # This is needed because examples and groups call super in their initializer. # Those initializers pass the sample values upward through their hierarchy. @@ -536,14 +529,9 @@ module Spectator::DSL end end - # Module for the context. - # The module uses a generated unique name. - module Context%sample - # Include the parent module. - # Since @type resolves immediately, - # this will reference the parent type. - include {{@type.id}} - + # Class for the context. + # The class uses a generated unique name. + class Context%sample < {{@type.id}} # Value wrapper for the current element. @%wrapper : ::Spectator::Internals::ValueWrapper @@ -642,10 +630,7 @@ module Spectator::DSL # This has to be a class that includes the parent module. # The collection could reference a helper method # or anything else in the parent scope. - class Sample%sample - # Include the parent module. - include {{@type.id}} - + class Sample%sample < {{@type.id}} # Method that returns an array containing the collection. # This method should be called only once. # The framework stores the collection as an array for a couple of reasons. @@ -657,14 +642,9 @@ module Spectator::DSL end end - # Module for the context. - # The module uses a generated unique name. - module Context%sample - # Include the parent module. - # Since @type resolves immediately, - # this will reference the parent type. - include {{@type.id}} - + # Class for the context. + # The class uses a generated unique name. + class Context%sample < {{@type.id}} # Value wrapper for the current element. @%wrapper : ::Spectator::Internals::ValueWrapper @@ -1596,13 +1576,10 @@ module Spectator::DSL # The block passed to this macro is the actual test code. private macro _spectator_test(class_name, run_method_name) # Wrapper class for isolating the test code. - class {{class_name.id}} + class {{class_name.id}} < {{@type.id}} # Mix in methods and macros specifically for example DSL. include ::Spectator::DSL::ExampleDSL - # Include the parent (example group) context. - include {{@type.id}} - # Initializer that accepts sample values. # The sample values are passed upward to the group modules. # Any module that adds sample values can pull their values from this instance. From 4ecc2c600416acc58f85b3a5344e238a168907ab Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 30 Aug 2019 13:14:48 -0600 Subject: [PATCH 009/205] Include ExampleDSL at root Will need to check if methods can run in that scope. --- src/spectator.cr | 4 ++-- src/spectator/dsl/structure_dsl.cr | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/spectator.cr b/src/spectator.cr index aa590d8..cf21730 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -38,8 +38,8 @@ module Spectator # Root-level class that contains all examples and example groups. class SpectatorExamples - # Include the DSL for creating groups, example, and more. - include ::Spectator::DSL::StructureDSL + include ::Spectator::DSL::StructureDSL # Include the DSL for creating groups, example, and more. + include ::Spectator::DSL::ExampleDSL # Mix in methods and macros specifically for example DSL. # Placeholder initializer. # This is needed because examples and groups call super in their initializer. diff --git a/src/spectator/dsl/structure_dsl.cr b/src/spectator/dsl/structure_dsl.cr index 1d3b421..df9d5e6 100644 --- a/src/spectator/dsl/structure_dsl.cr +++ b/src/spectator/dsl/structure_dsl.cr @@ -241,7 +241,6 @@ module Spectator::DSL # Class for the context. # The class uses a generated unique name. class Context%context < {{@type.id}} - # Check if `what` looks like a type. # If it is, add the `#described_class` and `subject` methods. # At the time of writing this code, @@ -1577,9 +1576,6 @@ module Spectator::DSL private macro _spectator_test(class_name, run_method_name) # Wrapper class for isolating the test code. class {{class_name.id}} < {{@type.id}} - # Mix in methods and macros specifically for example DSL. - include ::Spectator::DSL::ExampleDSL - # Initializer that accepts sample values. # The sample values are passed upward to the group modules. # Any module that adds sample values can pull their values from this instance. From bfe641e07e58f8f0c1382572780492951ab6ca4c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 30 Aug 2019 14:46:24 -0600 Subject: [PATCH 010/205] Move sample values to group instance The goal is to construct examples with a parameterless initializer. This doesn't quite work, since the sample values are retrieved via the harness, which doesn't have an active example when the sample values are created. --- src/spectator.cr | 6 ----- src/spectator/dsl/example_factory.cr | 6 ++--- .../dsl/nested_example_group_builder.cr | 2 +- .../dsl/root_example_group_builder.cr | 2 +- .../dsl/sample_example_group_builder.cr | 6 ++--- src/spectator/dsl/structure_dsl.cr | 26 +++++-------------- src/spectator/example.cr | 3 +-- src/spectator/example_group.cr | 4 ++- src/spectator/internals/harness.cr | 5 ++++ src/spectator/nested_example_group.cr | 4 +-- 10 files changed, 25 insertions(+), 39 deletions(-) diff --git a/src/spectator.cr b/src/spectator.cr index cf21730..7a0b0bd 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -41,12 +41,6 @@ module Spectator include ::Spectator::DSL::StructureDSL # Include the DSL for creating groups, example, and more. include ::Spectator::DSL::ExampleDSL # Mix in methods and macros specifically for example DSL. - # Placeholder initializer. - # This is needed because examples and groups call super in their initializer. - # Those initializers pass the sample values upward through their hierarchy. - def initialize(_sample_values : ::Spectator::Internals::SampleValues) - end - # Pass off the "what" argument and block to `DSL::StructureDSL.describe`. # That method will handle creating a new group for this spec. describe({{what}}) {{block}} diff --git a/src/spectator/dsl/example_factory.cr b/src/spectator/dsl/example_factory.cr index 91ee4b6..6271683 100644 --- a/src/spectator/dsl/example_factory.cr +++ b/src/spectator/dsl/example_factory.cr @@ -7,9 +7,9 @@ module Spectator::DSL end # Constructs a new example instance and returns it. - # The *group* and *sample_values* are passed to `Example#initialize`. - def build(group : ExampleGroup, sample_values : Internals::SampleValues) : Example - @example_type.new(group, sample_values) + # The *group* is passed to `Example#initialize`. + def build(group : ExampleGroup, _sample_values : Internals::SampleValues) : Example + @example_type.new(group) end end end diff --git a/src/spectator/dsl/nested_example_group_builder.cr b/src/spectator/dsl/nested_example_group_builder.cr index a711d74..efee6d1 100644 --- a/src/spectator/dsl/nested_example_group_builder.cr +++ b/src/spectator/dsl/nested_example_group_builder.cr @@ -26,7 +26,7 @@ module Spectator::DSL # The *parent* should be the group that contains this group. # The *sample_values* will be given to all of the examples (and groups) nested in this group. def build(parent : ExampleGroup, sample_values : Internals::SampleValues) : NestedExampleGroup - NestedExampleGroup.new(@what, parent, hooks, conditions).tap do |group| + NestedExampleGroup.new(@what, parent, hooks, conditions, sample_values).tap do |group| # Set the group's children to built versions of the children from this instance. group.children = @children.map do |child| # Build the child and up-cast to prevent type errors. diff --git a/src/spectator/dsl/root_example_group_builder.cr b/src/spectator/dsl/root_example_group_builder.cr index d6df2fd..03b13fb 100644 --- a/src/spectator/dsl/root_example_group_builder.cr +++ b/src/spectator/dsl/root_example_group_builder.cr @@ -6,7 +6,7 @@ module Spectator::DSL # Creates a `RootExampleGroup` which can have instances of `Example` and `ExampleGroup` nested in it. # The *sample_values* will be given to all of the examples (and groups) nested in this group. def build(sample_values : Internals::SampleValues) : RootExampleGroup - RootExampleGroup.new(hooks, conditions).tap do |group| + RootExampleGroup.new(hooks, conditions, sample_values).tap do |group| # Set the group's children to built versions of the children from this instance. group.children = @children.map do |child| # Build the child and up-cast to prevent type errors. diff --git a/src/spectator/dsl/sample_example_group_builder.cr b/src/spectator/dsl/sample_example_group_builder.cr index 62c0746..9f1e144 100644 --- a/src/spectator/dsl/sample_example_group_builder.cr +++ b/src/spectator/dsl/sample_example_group_builder.cr @@ -39,11 +39,11 @@ module Spectator::DSL # The *parent* should be the group that contains this group. # The *sample_values* will be given to all of the examples (and groups) nested in this group. def build(parent : ExampleGroup, sample_values : Internals::SampleValues) : NestedExampleGroup - collection = @collection_builder.call(@collection_type.new(sample_values)) + collection = @collection_builder.call(@collection_type.new) # This creates the container for the sub-groups. # The hooks are defined here, instead of repeating for each sub-group. - NestedExampleGroup.new(@what, parent, hooks, conditions).tap do |group| + NestedExampleGroup.new(@what, parent, hooks, conditions, sample_values).tap do |group| # Set the container group's children to be sub-groups for each item in the collection. group.children = collection.map do |value| # Create a sub-group for each item in the collection. @@ -61,7 +61,7 @@ module Spectator::DSL private def build_sub_group(parent : ExampleGroup, sample_values : Internals::SampleValues, value : T) : NestedExampleGroup # Add the value to sample values for this sub-group. sub_values = sample_values.add(@symbol, @name, value) - NestedExampleGroup.new(value.to_s, parent, ExampleHooks.empty, ExampleConditions.empty).tap do |group| + NestedExampleGroup.new(value.to_s, parent, ExampleHooks.empty, ExampleConditions.empty, sub_values).tap do |group| # Set the sub-group's children to built versions of the children from this instance. group.children = @children.map do |child| # Build the child and up-cast to prevent type errors. diff --git a/src/spectator/dsl/structure_dsl.cr b/src/spectator/dsl/structure_dsl.cr index df9d5e6..a43a378 100644 --- a/src/spectator/dsl/structure_dsl.cr +++ b/src/spectator/dsl/structure_dsl.cr @@ -503,13 +503,6 @@ module Spectator::DSL # The collection could reference a helper method # or anything else in the parent scope. class Sample%sample < {{@type.id}} - # Placeholder initializer. - # This is needed because examples and groups call super in their initializer. - # Those initializers pass the sample values upward through their hierarchy. - def initialize(_sample_values : ::Spectator::Internals::SampleValues) - super - end - # Method that returns an array containing the collection. # This method should be called only once. # The framework stores the collection as an array for a couple of reasons. @@ -543,9 +536,9 @@ module Spectator::DSL end # Initializer to extract current element of the collection from sample values. - def initialize(sample_values : ::Spectator::Internals::SampleValues) + def initialize super - @%wrapper = sample_values.get_wrapper(:%sample) + @%wrapper = ::Spectator::Internals::Harness.current.group.sample_values.get_wrapper(:%sample) end # Start a new example group. @@ -656,9 +649,9 @@ module Spectator::DSL end # Initializer to extract current element of the collection from sample values. - def initialize(sample_values : ::Spectator::Internals::SampleValues) + def initialize super - @%wrapper = sample_values.get_wrapper(:%sample) + @%wrapper = ::Spectator::Internals::Harness.current.group.sample_values.get_wrapper(:%sample) end # Start a new example group. @@ -1576,13 +1569,6 @@ module Spectator::DSL private macro _spectator_test(class_name, run_method_name) # Wrapper class for isolating the test code. class {{class_name.id}} < {{@type.id}} - # Initializer that accepts sample values. - # The sample values are passed upward to the group modules. - # Any module that adds sample values can pull their values from this instance. - def initialize(sample_values : ::Spectator::Internals::SampleValues) - super - end - # Generated method for actually running the test code. def {{run_method_name.id}} {{yield}} @@ -1614,9 +1600,9 @@ module Spectator::DSL # Stores the group the example belongs to # and sample values specific to this instance of the test. # This method's signature must match the one used in `ExampleFactory#build`. - def initialize(group : ::Spectator::ExampleGroup, sample_values : ::Spectator::Internals::SampleValues) + def initialize(group : ::Spectator::ExampleGroup) super - @instance = {{test_class_name.id}}.new(sample_values) + @instance = {{test_class_name.id}}.new end # Retrieves the underlying, wrapped test code. diff --git a/src/spectator/example.cr b/src/spectator/example.cr index d12363e..d8185ef 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -31,8 +31,7 @@ module Spectator # Creates the base of the example. # The group should be the example group the example belongs to. - # The *sample_values* are passed to the example code. - def initialize(@group, sample_values : Internals::SampleValues) + def initialize(@group) end # Indicates there is only one example to run. diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index 6e25a93..c4708b2 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -15,9 +15,11 @@ module Spectator include Enumerable(ExampleComponent) include Iterable(ExampleComponent) + getter sample_values : Internals::SampleValues + # Creates the example group. # The hooks are stored to be triggered later. - def initialize(@hooks : ExampleHooks, @conditions : ExampleConditions) + def initialize(@hooks : ExampleHooks, @conditions : ExampleConditions, @sample_values) @before_all_hooks_run = false @after_all_hooks_run = false end diff --git a/src/spectator/internals/harness.cr b/src/spectator/internals/harness.cr index b9de70b..8f70045 100644 --- a/src/spectator/internals/harness.cr +++ b/src/spectator/internals/harness.cr @@ -34,6 +34,11 @@ module Spectator::Internals # Retrieves the current running example. getter example : Example + # Retrieves the group for the current running example. + def group + example.group + 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 diff --git a/src/spectator/nested_example_group.cr b/src/spectator/nested_example_group.cr index 0a90b86..2bd1e09 100644 --- a/src/spectator/nested_example_group.cr +++ b/src/spectator/nested_example_group.cr @@ -18,8 +18,8 @@ module Spectator # The parent's children must contain this group, # otherwise there may be unexpected behavior. # The *hooks* are stored to be triggered later. - def initialize(@what, @parent, hooks : ExampleHooks, conditions : ExampleConditions) - super(hooks, conditions) + def initialize(@what, @parent, hooks : ExampleHooks, conditions : ExampleConditions, sample_values) + super(hooks, conditions, sample_values) end # Indicates wheter the group references a type. From 752b7f0ffaa68a6d7feecce688f5a4d4261cb3c2 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 31 Aug 2019 10:14:06 -0600 Subject: [PATCH 011/205] Update version constant --- src/spectator.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator.cr b/src/spectator.cr index 7a0b0bd..5cb84d7 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -5,7 +5,7 @@ module Spectator extend self # Current version of the Spectator library. - VERSION = "0.8.2" + VERSION = "0.9.0" # Top-level describe method. # All specs in a file must be wrapped in this call. From 00a249502791117abfa6e332103489d9e1df8a4e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 31 Aug 2019 13:11:59 -0600 Subject: [PATCH 012/205] Change test root to SpectatorTest --- src/spectator.cr | 8 +++----- src/spectator_test.cr | 10 ++++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 src/spectator_test.cr diff --git a/src/spectator.cr b/src/spectator.cr index 5cb84d7..3a088c4 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -1,4 +1,5 @@ require "./spectator/includes" +require "./spectator_test" # Module that contains all functionality related to Spectator. module Spectator @@ -32,15 +33,12 @@ module Spectator # 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 `SpectatorExamples` module. + # 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 SpectatorExamples - include ::Spectator::DSL::StructureDSL # Include the DSL for creating groups, example, and more. - include ::Spectator::DSL::ExampleDSL # Mix in methods and macros specifically for example DSL. - + class SpectatorTest # Pass off the "what" argument and block to `DSL::StructureDSL.describe`. # That method will handle creating a new group for this spec. describe({{what}}) {{block}} diff --git a/src/spectator_test.cr b/src/spectator_test.cr new file mode 100644 index 0000000..439ceb9 --- /dev/null +++ b/src/spectator_test.cr @@ -0,0 +1,10 @@ +require "./spectator/dsl/example_dsl" +require "./spectator/dsl/structure_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::StructureDSL # Include the DSL for creating groups, example, and more. + include ::Spectator::DSL::ExampleDSL # Mix in methods and macros specifically for example DSL. +end From b8e125e38f9436106cdbcd2e2ddedc8380b0e2b5 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 31 Aug 2019 13:12:17 -0600 Subject: [PATCH 013/205] Add test wrapper --- src/spectator/test_wrapper.cr | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/spectator/test_wrapper.cr diff --git a/src/spectator/test_wrapper.cr b/src/spectator/test_wrapper.cr new file mode 100644 index 0000000..761d4f4 --- /dev/null +++ b/src/spectator/test_wrapper.cr @@ -0,0 +1,30 @@ +require "../spectator_test" +require "./source" + +module Spectator + # Stores information about a end-user test. + # Used to instantiate tests and run them. + struct TestWrapper + # Location of the test in source code. + getter source : Source + + # Description the user provided for the test. + getter description : String + + # Creates a wrapper for the test. + # The *builder* creates an instance of the test. + # The *runner* takes the test created by *builder* and runs it. + def initialize(@description, @source, @builder : -> SpectatorTest, @runner : SpectatorTest ->) + end + + # Instantiates and runs the test. + # This method yields twice - before and after the test. + # The test instance is yielded. + def run : Nil + test = @builder.call + yield test + @runner.call(test) + yield test + end + end +end From 19913a28d1a7633522a7e0fb83f31b77c93df4d2 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 31 Aug 2019 13:12:40 -0600 Subject: [PATCH 014/205] Gut factories and example structure code --- src/spectator/dsl/example_factory.cr | 15 --- src/spectator/dsl/example_group_builder.cr | 87 ---------------- .../dsl/nested_example_group_builder.cr | 38 ------- .../dsl/root_example_group_builder.cr | 18 ---- .../dsl/sample_example_group_builder.cr | 73 -------------- src/spectator/dsl/structure_dsl.cr | 99 +------------------ 6 files changed, 2 insertions(+), 328 deletions(-) delete mode 100644 src/spectator/dsl/example_factory.cr delete mode 100644 src/spectator/dsl/example_group_builder.cr delete mode 100644 src/spectator/dsl/nested_example_group_builder.cr delete mode 100644 src/spectator/dsl/root_example_group_builder.cr delete mode 100644 src/spectator/dsl/sample_example_group_builder.cr diff --git a/src/spectator/dsl/example_factory.cr b/src/spectator/dsl/example_factory.cr deleted file mode 100644 index 6271683..0000000 --- a/src/spectator/dsl/example_factory.cr +++ /dev/null @@ -1,15 +0,0 @@ -module Spectator::DSL - # Creates instances of examples from a specified class. - class ExampleFactory - # Creates the factory. - # The type passed to this constructor must be a sub-type of `Example`. - def initialize(@example_type : Example.class) - end - - # Constructs a new example instance and returns it. - # The *group* is passed to `Example#initialize`. - def build(group : ExampleGroup, _sample_values : Internals::SampleValues) : Example - @example_type.new(group) - end - end -end diff --git a/src/spectator/dsl/example_group_builder.cr b/src/spectator/dsl/example_group_builder.cr deleted file mode 100644 index cc4e662..0000000 --- a/src/spectator/dsl/example_group_builder.cr +++ /dev/null @@ -1,87 +0,0 @@ -module Spectator::DSL - # Base class for building all example groups. - abstract class ExampleGroupBuilder - # Type alias for valid children of example groups. - # NOTE: `NestedExampleGroupBuilder` is used instead of `ExampleGroupBuilder`. - # That is because `RootExampleGroupBuilder` also inherits from this class, - # and the root example group can't be a child. - alias Child = ExampleFactory | NestedExampleGroupBuilder - - # Factories and builders for all examples and groups. - @children = [] of Child - - # Hooks added to the group so far. - @before_all_hooks = [] of -> - @before_each_hooks = [] of -> - @after_all_hooks = [] of -> - @after_each_hooks = [] of -> - @around_each_hooks = [] of Proc(Nil) -> - - # Pre and post conditions so far. - @pre_conditions = [] of -> - @post_conditions = [] of -> - - # Adds a new example factory or group builder to this group. - def add_child(child : Child) - @children << child - end - - # Adds a hook to run before all examples (and nested examples) in this group. - def add_before_all_hook(block : ->) : Nil - @before_all_hooks << block - end - - # Adds a hook to run before each example (and nested example) in this group. - def add_before_each_hook(block : ->) : Nil - @before_each_hooks << block - end - - # Adds a hook to run after all examples (and nested examples) in this group. - def add_after_all_hook(block : ->) : Nil - @after_all_hooks << block - end - - # Adds a hook to run after each example (and nested example) in this group. - def add_after_each_hook(block : ->) : Nil - @after_each_hooks << block - end - - # Adds a hook to run around each example (and nested example) in this group. - # The block of code will be given another proc as an argument. - # It is expected that the block will call the proc. - def add_around_each_hook(block : Proc(Nil) ->) : Nil - @around_each_hooks << block - end - - # Adds a pre-condition to run at the start of every example in this group. - def add_pre_condition(block : ->) : Nil - @pre_conditions << block - end - - # Adds a post-condition to run at the end of every example in this group. - def add_post_condition(block : ->) : Nil - @post_conditions << block - end - - # Constructs an `ExampleHooks` instance with all the hooks defined for this group. - # This method should be called only when the group is being built, - # otherwise some hooks may be missing. - private def hooks - ExampleHooks.new( - @before_all_hooks, - @before_each_hooks, - @after_all_hooks, - @after_each_hooks, - @around_each_hooks - ) - end - - # Constructs an `ExampleConditions` instance - # with all the pre- and post-conditions defined for this group. - # This method should be called only when the group is being built, - # otherwise some conditions may be missing. - private def conditions - ExampleConditions.new(@pre_conditions, @post_conditions) - end - end -end diff --git a/src/spectator/dsl/nested_example_group_builder.cr b/src/spectator/dsl/nested_example_group_builder.cr deleted file mode 100644 index efee6d1..0000000 --- a/src/spectator/dsl/nested_example_group_builder.cr +++ /dev/null @@ -1,38 +0,0 @@ -module Spectator::DSL - # Standard example group builder. - # Creates groups of examples and nested groups. - class NestedExampleGroupBuilder < ExampleGroupBuilder - # Creates a new group builder. - # The value for *what* should be the context for the group. - # - # For example, in these samples: - # ``` - # describe String do - # # ... - # context "with an empty string" do - # # ... - # end - # end - # ``` - # The value would be "String" for the describe block - # and "with an empty string" for the context block. - # Use a `Symbol` when referencing a type name. - def initialize(@what : Symbol | String) - end - - # Builds the example group. - # A new `NestedExampleGroup` will be returned - # which can have instances of `Example` and `ExampleGroup` nested in it. - # The *parent* should be the group that contains this group. - # The *sample_values* will be given to all of the examples (and groups) nested in this group. - def build(parent : ExampleGroup, sample_values : Internals::SampleValues) : NestedExampleGroup - NestedExampleGroup.new(@what, parent, hooks, conditions, sample_values).tap do |group| - # Set the group's children to built versions of the children from this instance. - group.children = @children.map do |child| - # Build the child and up-cast to prevent type errors. - child.build(group, sample_values).as(ExampleComponent) - end - end - end - end -end diff --git a/src/spectator/dsl/root_example_group_builder.cr b/src/spectator/dsl/root_example_group_builder.cr deleted file mode 100644 index 03b13fb..0000000 --- a/src/spectator/dsl/root_example_group_builder.cr +++ /dev/null @@ -1,18 +0,0 @@ -module Spectator::DSL - # Top-level example group builder. - # There should only be one instance of this class, - # and it should be at the top of the spec "tree". - class RootExampleGroupBuilder < ExampleGroupBuilder - # Creates a `RootExampleGroup` which can have instances of `Example` and `ExampleGroup` nested in it. - # The *sample_values* will be given to all of the examples (and groups) nested in this group. - def build(sample_values : Internals::SampleValues) : RootExampleGroup - RootExampleGroup.new(hooks, conditions, sample_values).tap do |group| - # Set the group's children to built versions of the children from this instance. - group.children = @children.map do |child| - # Build the child and up-cast to prevent type errors. - child.build(group, sample_values).as(ExampleComponent) - end - end - end - end -end diff --git a/src/spectator/dsl/sample_example_group_builder.cr b/src/spectator/dsl/sample_example_group_builder.cr deleted file mode 100644 index 9f1e144..0000000 --- a/src/spectator/dsl/sample_example_group_builder.cr +++ /dev/null @@ -1,73 +0,0 @@ -require "./nested_example_group_builder" - -module Spectator::DSL - # Specialized example group builder for "sample" groups. - # The type parameter `C` is the type to instantiate to create the collection. - # The type parameter `T` should be the type of each element in the sample collection. - # This builder creates a container group with groups inside for each item in the collection. - # The hooks are only defined for the container group. - # By doing so, the hooks are defined once, are inherited, and use less memory. - class SampleExampleGroupBuilder(C, T) < NestedExampleGroupBuilder - # Creates a new group builder. - # The value for *what* should be the text the user specified for the collection. - # The *collection_type* is the type to create that will produce the items. - # The *collection_builder* is a proc that takes an instance of *collection_type* - # and returns an actual array of items to create examples for. - # The *name* is the variable name that the user accesses the current collection item with. - # - # In this code: - # ``` - # sample random_integers do |integer| - # # ... - # end - # ``` - # The *what* would be "random_integers" - # and the collection would contain the items returned by calling *random_integers*. - # The *name* would be "integer". - # - # The *symbol* is passed along to the sample values - # so that the example code can retrieve the current item from the collection. - # The symbol should be unique. - def initialize(what : String, @collection_type : C.class, @collection_builder : C -> Array(T), - @name : String, @symbol : Symbol) - super(what) - end - - # Builds the example group. - # A new `NestedExampleGroup` will be returned - # which can have instances of `Example` and `ExampleGroup` nested in it. - # The *parent* should be the group that contains this group. - # The *sample_values* will be given to all of the examples (and groups) nested in this group. - def build(parent : ExampleGroup, sample_values : Internals::SampleValues) : NestedExampleGroup - collection = @collection_builder.call(@collection_type.new) - - # This creates the container for the sub-groups. - # The hooks are defined here, instead of repeating for each sub-group. - NestedExampleGroup.new(@what, parent, hooks, conditions, sample_values).tap do |group| - # Set the container group's children to be sub-groups for each item in the collection. - group.children = collection.map do |value| - # Create a sub-group for each item in the collection. - build_sub_group(group, sample_values, value).as(ExampleComponent) - end - end - end - - # Builds a sub-group for one item in the collection. - # The *parent* should be the container group currently being built by the `#build` call. - # The *sample_values* should be the same as what was passed to the `#build` call. - # The *value* is the current item in the collection. - # The value will be added to the sample values for the sub-group, - # so it shouldn't be added prior to calling this method. - private def build_sub_group(parent : ExampleGroup, sample_values : Internals::SampleValues, value : T) : NestedExampleGroup - # Add the value to sample values for this sub-group. - sub_values = sample_values.add(@symbol, @name, value) - NestedExampleGroup.new(value.to_s, parent, ExampleHooks.empty, ExampleConditions.empty, sub_values).tap do |group| - # Set the sub-group's children to built versions of the children from this instance. - group.children = @children.map do |child| - # Build the child and up-cast to prevent type errors. - child.build(group, sub_values).as(ExampleComponent) - end - end - end - end -end diff --git a/src/spectator/dsl/structure_dsl.cr b/src/spectator/dsl/structure_dsl.cr index a43a378..dce58d4 100644 --- a/src/spectator/dsl/structure_dsl.cr +++ b/src/spectator/dsl/structure_dsl.cr @@ -1426,21 +1426,7 @@ module Spectator::DSL _spectator_test(Test%example, %run) {{block}} {% end %} - # Create a class derived from `RunnableExample` to run the test code. - _spectator_example(Example%example, Test%example, ::Spectator::RunnableExample, {{what}}) do - # Source where the example originated from. - def source - ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - end - - # Implement abstract method to run the wrapped example block. - protected def run_instance - @instance.%run - end - end - - # Add the example to the current group. - ::Spectator::DSL::Builder.add_example(Example%example) + # TODO end # Creates an example, or a test case. @@ -1500,19 +1486,7 @@ module Spectator::DSL # By creating a `#pending` test, the code will be referenced. # Thus, forcing the compiler to at least process the code, even if it isn't run. macro pending(what, _source_file = __FILE__, _source_line = __LINE__, &block) - # Create the wrapper class for the test code. - _spectator_test(Test%example, %run) {{block}} - - # Create a class derived from `PendingExample` to skip the test code. - _spectator_example(Example%example, Test%example, ::Spectator::PendingExample, {{what}}) do - # Source where the example originated from. - def source - ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - end - end - - # Add the example to the current group. - ::Spectator::DSL::Builder.add_example(Example%example) + # TODO end # Creates an example, or a test case, that does not run. @@ -1553,74 +1527,5 @@ module Spectator::DSL macro xit(&block) pending({{block.body.stringify}}) {{block}} end - - # Creates a wrapper class for test code. - # The class serves multiple purposes, mostly dealing with scope. - # 1. Include the parent modules as mix-ins. - # 2. Enable DSL specific to examples. - # 3. Isolate methods in `Example` from the test code. - # - # Since the names are generated, and macros can't return values, - # the names for everything must be passed in as arguments. - # The *class_name* argument is the name of the class to define. - # The *run_method_name* argument is the name of the method in the wrapper class - # that will actually run the test code. - # The block passed to this macro is the actual test code. - private macro _spectator_test(class_name, run_method_name) - # Wrapper class for isolating the test code. - class {{class_name.id}} < {{@type.id}} - # Generated method for actually running the test code. - def {{run_method_name.id}} - {{yield}} - end - end - end - - # Creates an example class. - # Since the names are generated, and macros can't return values, - # the names for everything must be passed in as arguments. - # The *example_class_name* argument is the name of the class to define. - # The *test_class_name* argument is the name of the wrapper class to reference. - # This must be the same as `class_name` for `#_spectator_example_wrapper`. - # The *base_class* argument specifies which type of example class the new class should derive from. - # This should typically be `RunnableExample` or `PendingExample`. - # The *what* argument is the description passed to the `#it` or `#pending` block. - # And lastly, the block specified is additional content to put in the class. - # For instance, to define a method in the class, do it in the block. - # ``` - # _spectator_example(Example123, Test123, RunnableExample, "does something") do - # def something - # # This method is defined in the Example123 class. - # end - # end - # ``` - private macro _spectator_example(example_class_name, test_class_name, base_class, what, &block) - # Example class containing meta information and instructions for running the test. - class {{example_class_name.id}} < {{base_class.id}} - # Stores the group the example belongs to - # and sample values specific to this instance of the test. - # This method's signature must match the one used in `ExampleFactory#build`. - def initialize(group : ::Spectator::ExampleGroup) - super - @instance = {{test_class_name.id}}.new - end - - # Retrieves the underlying, wrapped test code. - getter instance - - # Indicates whether the example references a method. - def symbolic? - {{what.is_a?(StringLiteral) && what.starts_with?('#') ? true : false}} - end - - # Add the block's content. - {{block.body}} - - # Description for the test. - def what - {{what.is_a?(StringLiteral) ? what : what.stringify}} - end - end - end end end From 1c5b46b981361875491db5cb806a23796ed57a2f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 1 Sep 2019 00:54:04 -0600 Subject: [PATCH 015/205] Use fresh variables --- src/spectator/dsl/example_dsl.cr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/spectator/dsl/example_dsl.cr b/src/spectator/dsl/example_dsl.cr index 5fea5a7..95f4a1c 100644 --- a/src/spectator/dsl/example_dsl.cr +++ b/src/spectator/dsl/example_dsl.cr @@ -23,9 +23,9 @@ module Spectator::DSL # 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) + %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. From 0706a9986c0ae5c2755aa4a48def12968d53eec3 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 6 Sep 2019 13:08:41 -0600 Subject: [PATCH 016/205] Ensure example is set to finished after running --- src/spectator/example.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index d8185ef..934931a 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -22,8 +22,9 @@ module Spectator # An exception is raised if an attempt is made to run it more than once. def run : Result raise "Attempted to run example more than once (#{self})" if finished? - @finished = true run_impl + ensure + @finished = true end # Implementation-specific for running the example code. From f25eb9164f479edf3e55c26fce442e0e19695f54 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Sep 2019 09:58:15 -0600 Subject: [PATCH 017/205] Some test creation rewrite --- src/spectator/dsl/structure_dsl.cr | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/spectator/dsl/structure_dsl.cr b/src/spectator/dsl/structure_dsl.cr index dce58d4..05113b6 100644 --- a/src/spectator/dsl/structure_dsl.cr +++ b/src/spectator/dsl/structure_dsl.cr @@ -1416,17 +1416,20 @@ module Spectator::DSL # Create the wrapper class for the test code. {% if block.is_a?(Nop) %} {% if what.is_a?(Call) %} - _spectator_test(Test%example, %run) do + def %run {{what}} end {% else %} {% raise "Unrecognized syntax: `it #{what}`" %} {% end %} {% else %} - _spectator_test(Test%example, %run) {{block}} + def %run + {{block}} + end {% end %} - # TODO + %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) + ::Spectator::DSL::Builder.add_example({{what.stringify}}, %source, {{@type.name}}) { |test| test.as({{@type.name}}).%run } end # Creates an example, or a test case. From e304224bd6ba563e10c3eede16892f6cc128609f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Sep 2019 09:59:25 -0600 Subject: [PATCH 018/205] Move stack handling to its own type --- src/spectator/builders/example_group_stack.cr | 22 ++++++++ src/spectator/dsl/builder.cr | 52 +++++++------------ 2 files changed, 40 insertions(+), 34 deletions(-) create mode 100644 src/spectator/builders/example_group_stack.cr diff --git a/src/spectator/builders/example_group_stack.cr b/src/spectator/builders/example_group_stack.cr new file mode 100644 index 0000000..cf91a45 --- /dev/null +++ b/src/spectator/builders/example_group_stack.cr @@ -0,0 +1,22 @@ +module Spectator::Builders + struct ExampleGroupStack + getter root = RootExampleGroupBuilder.new + + @stack = Deque(ExampleGroupBuilder).new(1, @root) + + 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/dsl/builder.cr b/src/spectator/dsl/builder.cr index a836559..b815dce 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -6,26 +6,7 @@ module Spectator::DSL module Builder extend self - # Root group that contains all examples and groups in the spec. - private class_getter root_group = RootExampleGroupBuilder.new - - # Stack for tracking the current group the spec is working in. - # The last item (top of the stack) is the current group. - # The first item (bottom of the stack) is the root group (`#root_group`). - # The root group should never be popped. - @@group_stack = Array(ExampleGroupBuilder).new(1, root_group) - - # Retrieves the current group the spec is working in. - private def current_group - @@group_stack.last - end - - # Adds a new group to the stack. - # Calling this method indicates the spec has entered a nested group. - private def push_group(group : NestedExampleGroupBuilder) - current_group.add_child(group) - @@group_stack.push(group) - end + @@stack = Builders::ExampleGroupStack.new # Begins a new nested group in the spec. # A corresponding `#end_group` call must be made @@ -34,7 +15,7 @@ module Spectator::DSL # as arguments to this method are passed directly to it. def start_group(*args) : Nil group = NestedExampleGroupBuilder.new(*args) - push_group(group) + @@stack.push(group) end # Begins a new sample group in the spec - @@ -45,7 +26,7 @@ module Spectator::DSL # as arguments to this method are passed directly to it. def start_sample_group(*args) : Nil group = SampleExampleGroupBuilder.new(*args) - push_group(group) + @@stack.push(group) end # Marks the end of a group in the spec. @@ -53,58 +34,61 @@ module Spectator::DSL # It is also important to line up the start and end calls. # Otherwise examples might get placed into wrong groups. def end_group : Nil - @@group_stack.pop + @@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(example_type : Example.class) : Nil - factory = ExampleFactory.new(example_type) - current_group.add_child(factory) + def add_example(description : String, source : Source, + example_type : ::SpectatorTest.class, &runner : ::SpectatorTest ->) : Nil + builder = ->{ example_type.new.as(::SpectatorTest) } + wrapper = TestWrapper.new(description, source, builder, runner) + example = Example.new(current_group, wrapper) + # TODO: Add to stack. end # Adds a block of code to run before all examples in the current group. def add_before_all_hook(&block : ->) : Nil - current_group.add_before_all_hook(block) + @@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 : ->) : Nil - current_group.add_before_each_hook(block) + @@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 - current_group.add_after_all_hook(block) + @@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 : ->) : Nil - current_group.add_after_each_hook(block) + @@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 proc as an argument. # It is expected that the block will call the proc. def add_around_each_hook(&block : Proc(Nil) ->) : Nil - current_group.add_around_each_hook(block) + @@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 : ->) : Nil - current_group.add_pre_condition(block) + @@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 : ->) : Nil - current_group.add_post_condition(block) + @@stack.current.add_post_condition(block) 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 = root_group.build(Internals::SampleValues.empty) + group = @@stack.root.build(Internals::SampleValues.empty) TestSuite.new(group, filter) end end From de8f298676b65390eb26c423850363b15d4dd358 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Sep 2019 10:28:02 -0600 Subject: [PATCH 019/205] Remove hooks, conditions, and sample vlues (for now) Removed only from examples and example groups. --- src/spectator/example_group.cr | 74 +-------------------------- src/spectator/nested_example_group.cr | 66 +----------------------- src/spectator/runnable_example.cr | 20 -------- 3 files changed, 2 insertions(+), 158 deletions(-) diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index c4708b2..2d6b28e 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -15,13 +15,8 @@ module Spectator include Enumerable(ExampleComponent) include Iterable(ExampleComponent) - getter sample_values : Internals::SampleValues - # Creates the example group. - # The hooks are stored to be triggered later. - def initialize(@hooks : ExampleHooks, @conditions : ExampleConditions, @sample_values) - @before_all_hooks_run = false - @after_all_hooks_run = false + def initialize end # Retrieves the children in the group. @@ -123,72 +118,5 @@ module Spectator def finished? children.all?(&.finished?) end - - # Runs all of the "before-each" and "before-all" hooks. - # This should run prior to every example in the group. - def run_before_hooks - run_before_all_hooks - run_before_each_hooks - end - - # Runs all of the "before-all" hooks. - # This should run prior to any examples in the group. - # The hooks will be run only once. - # Subsequent calls to this method will do nothing. - protected def run_before_all_hooks : Nil - return if @before_all_hooks_run - @hooks.run_before_all - @before_all_hooks_run = true - end - - # Runs all of the "before-each" hooks. - # This method should run prior to every example in the group. - protected def run_before_each_hooks : Nil - @hooks.run_before_each - end - - # Runs all of the "after-all" and "after-each" hooks. - # This should run following every example in the group. - def run_after_hooks - run_after_each_hooks - run_after_all_hooks - end - - # Runs all of the "after-all" hooks. - # This should run following all examples in the group. - # The hooks will be run only once, - # and only after all examples in the group have finished. - # Subsequent calls after the hooks have been run will do nothing. - protected def run_after_all_hooks(ignore_unfinished = false) : Nil - return if @after_all_hooks_run - return unless ignore_unfinished || finished? - - @hooks.run_after_all - @after_all_hooks_run = true - end - - # Runs all of the "after-each" hooks. - # This method should run following every example in the group. - protected def run_after_each_hooks : Nil - @hooks.run_after_each - 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_hooks(&block : ->) : -> - @hooks.wrap_around_each(&block) - end - - # Runs all of the pre-conditions for an example. - def run_pre_conditions - @conditions.run_pre_conditions - end - - # Runs all of the post-conditions for an example. - def run_post_conditions - @conditions.run_post_conditions - end end end diff --git a/src/spectator/nested_example_group.cr b/src/spectator/nested_example_group.cr index 2bd1e09..00f16e9 100644 --- a/src/spectator/nested_example_group.cr +++ b/src/spectator/nested_example_group.cr @@ -18,8 +18,7 @@ module Spectator # The parent's children must contain this group, # otherwise there may be unexpected behavior. # The *hooks* are stored to be triggered later. - def initialize(@what, @parent, hooks : ExampleHooks, conditions : ExampleConditions, sample_values) - super(hooks, conditions, sample_values) + def initialize(@what, @parent) end # Indicates wheter the group references a type. @@ -27,69 +26,6 @@ module Spectator @what.is_a?(Symbol) end - # Runs all of the "before-all" hooks. - # This should run prior to any examples in the group. - # The hooks will be run only once. - # Subsequent calls to this method will do nothing. - # Parent "before-all" hooks will be run first. - protected def run_before_all_hooks : Nil - parent.run_before_all_hooks - super - end - - # Runs all of the "before-each" hooks. - # This method should run prior to every example in the group. - # Parent "before-each" hooks will be run first. - protected def run_before_each_hooks : Nil - parent.run_before_each_hooks - super - end - - # Runs all of the "after-all" hooks. - # This should run following all examples in the group. - # The hooks will be run only once, - # and only after all examples in the group have finished. - # Subsequent calls after the hooks have been run will do nothing. - # Parent "after-all" hooks will be run last. - protected def run_after_all_hooks(ignore_unfinished = false) : Nil - super - parent.run_after_all_hooks(ignore_unfinished) - end - - # Runs all of the "after-each" hooks. - # This method should run following every example in the group. - # Parent "after-each" hooks will be run last. - protected def run_after_each_hooks : Nil - super - parent.run_after_each_hooks - 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. - # Parent "around-each" hooks will be in the outermost wrappings. - def wrap_around_each_hooks(&block : ->) : -> - wrapper = super(&block) - parent.wrap_around_each_hooks(&wrapper) - end - - # Runs all of the pre-condition checks. - # This method should run prior to every example in the group. - # Parent pre-conditions will be checked first. - def run_pre_conditions : Nil - parent.run_pre_conditions - super - end - - # Runs all of the post-condition checks. - # This method should run following every example in the group. - # Parent post-conditions will be checked last. - def run_post_conditions : Nil - super - parent.run_post_conditions - end - # Creates a string representation of the group. # The string consists of `#what` appended to the parent. # This results in a string like: diff --git a/src/spectator/runnable_example.cr b/src/spectator/runnable_example.cr index 2879cb1..0809291 100644 --- a/src/spectator/runnable_example.cr +++ b/src/spectator/runnable_example.cr @@ -17,22 +17,6 @@ module Spectator # Runs the actual test code. private abstract def run_instance - # Runs the hooks that should be performed before starting the test code. - private def run_before_hooks - group.run_before_hooks - rescue ex - # If an error occurs in the before hooks, skip running the example. - raise Exception.new("Error encountered while running before hooks", ex) - end - - # Runs the hooks that should be performed after the test code finishes. - private def run_after_hooks - group.run_after_hooks - rescue ex - # If an error occurs in the after hooks, elevate it to abort testing. - raise Exception.new("Error encountered while running after hooks", ex) - end - # Runs all hooks and the example code. # A captured result is returned. private def capture_result @@ -40,9 +24,7 @@ module Spectator # Get the proc that will call around-each hooks and the example. wrapper = wrap_run_example(result) - run_before_hooks run_wrapper(wrapper) - run_after_hooks end end @@ -70,9 +52,7 @@ module Spectator # Capture how long it takes to run the test code. result.elapsed = Time.measure do begin - group.run_pre_conditions run_instance # Actually run the example code. - group.run_post_conditions rescue ex # Catch all errors and handle them later. result.error = ex end From a178db05ac42e11fd5806ade67c8e6f4af7c4b31 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Sep 2019 10:38:26 -0600 Subject: [PATCH 020/205] Use TestWrapper in Example classes --- src/spectator/example.cr | 14 ++++++++++---- src/spectator/example_component.cr | 1 + src/spectator/pending_example.cr | 2 +- src/spectator/runnable_example.cr | 11 +++-------- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index d8185ef..d7c2d18 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -2,7 +2,7 @@ require "./example_component" module Spectator # Base class for all types of examples. - # Concrete types must implement the `#run_impl, `#what`, `#instance`, and `#source` methods. + # Concrete types must implement the `#run_impl` method. abstract class Example < ExampleComponent # Indicates whether the example has already been run. getter? finished = false @@ -11,10 +11,16 @@ module Spectator getter group : ExampleGroup # Retrieves the internal wrapped instance. - abstract def instance + private getter @test_wrapper : TestWrapper # Source where the example originated from. - abstract def source : Source + def source + @test_wrapper.source + end + + def what + @test_wrapper.description + end # Runs the example code. # A result is returned, which represents the outcome of the test. @@ -31,7 +37,7 @@ module Spectator # Creates the base of the example. # The group should be the example group the example belongs to. - def initialize(@group) + def initialize(@group, @test_wrapper) end # Indicates there is only one example to run. diff --git a/src/spectator/example_component.cr b/src/spectator/example_component.cr index 9c9fa95..a6aa9d9 100644 --- a/src/spectator/example_component.cr +++ b/src/spectator/example_component.cr @@ -3,6 +3,7 @@ module Spectator # This is used as the base node type for the composite design pattern. abstract class ExampleComponent # Text that describes the context or test. + # TODO: Rename to description. abstract def what : String # Indicates whether the example (or group) has been completely run. diff --git a/src/spectator/pending_example.cr b/src/spectator/pending_example.cr index f485ec4..30dc99d 100644 --- a/src/spectator/pending_example.cr +++ b/src/spectator/pending_example.cr @@ -3,7 +3,7 @@ require "./example" module Spectator # Common class for all examples marked as pending. # This class will not run example code. - abstract class PendingExample < Example + class PendingExample < Example # Returns a pending result. private def run_impl PendingResult.new(self) diff --git a/src/spectator/runnable_example.cr b/src/spectator/runnable_example.cr index 0809291..7964ff1 100644 --- a/src/spectator/runnable_example.cr +++ b/src/spectator/runnable_example.cr @@ -1,11 +1,9 @@ require "./example" module Spectator - # Common base for all examples that can be run. - # This class includes all the logic for running example hooks, + # Includes all the logic for running example hooks, # the example code, and capturing a result. - # Sub-classes need to implement the `#what` and `#run_instance` methods. - abstract class RunnableExample < Example + class RunnableExample < Example # Runs the example, hooks, and captures the result # and translates to a usable result. def run_impl @@ -14,9 +12,6 @@ module Spectator translate_result(result, expectations) end - # Runs the actual test code. - private abstract def run_instance - # Runs all hooks and the example code. # A captured result is returned. private def capture_result @@ -52,7 +47,7 @@ module Spectator # Capture how long it takes to run the test code. result.elapsed = Time.measure do begin - run_instance # Actually run the example code. + test_wrapper.run {} # Actually run the example code. rescue ex # Catch all errors and handle them later. result.error = ex end From 64166d1c363e3fe277173dc6e1ad7a7b3171b59d Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 11 Sep 2019 22:21:06 -0600 Subject: [PATCH 021/205] Some fixes --- src/spectator/example.cr | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 78712ed..3b5cbb2 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -1,4 +1,5 @@ require "./example_component" +require "./test_wrapper" module Spectator # Base class for all types of examples. @@ -11,7 +12,7 @@ module Spectator getter group : ExampleGroup # Retrieves the internal wrapped instance. - private getter @test_wrapper : TestWrapper + private getter test_wrapper : TestWrapper # Source where the example originated from. def source @@ -22,6 +23,11 @@ module Spectator @test_wrapper.description end + def symbolic? + description = @test_wrapper.description + description.start_with?('#') || description.start_with?('.') + end + # Runs the example code. # A result is returned, which represents the outcome of the test. # An example can be run only once. From 08ee03b1de303a62969acdb8f9b6edf6601e9bf1 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 12 Sep 2019 20:16:41 -0600 Subject: [PATCH 022/205] Quickly get things somewhat working again --- src/spectator/builders/example_builder.cr | 10 +++++++++ .../builders/example_group_builder.cr | 13 +++++++++++ src/spectator/builders/example_group_stack.cr | 10 +++++++-- .../builders/nested_example_group_builder.cr | 16 ++++++++++++++ .../builders/root_example_group_builder.cr | 13 +++++++++++ src/spectator/dsl/builder.cr | 6 +++-- src/spectator/example.cr | 7 +++--- src/spectator/example_group.cr | 4 ---- src/spectator/runnable_example.cr | 22 ------------------- src/spectator/runner.cr | 2 +- src/spectator/test_wrapper.cr | 4 +++- 11 files changed, 71 insertions(+), 36 deletions(-) create mode 100644 src/spectator/builders/example_builder.cr create mode 100644 src/spectator/builders/example_group_builder.cr create mode 100644 src/spectator/builders/nested_example_group_builder.cr create mode 100644 src/spectator/builders/root_example_group_builder.cr diff --git a/src/spectator/builders/example_builder.cr b/src/spectator/builders/example_builder.cr new file mode 100644 index 0000000..a181216 --- /dev/null +++ b/src/spectator/builders/example_builder.cr @@ -0,0 +1,10 @@ +module Spectator::Builders + class ExampleBuilder + def initialize(@wrapper : TestWrapper) + end + + def build(group) + RunnableExample.new(group, @wrapper).as(ExampleComponent) + end + end +end diff --git a/src/spectator/builders/example_group_builder.cr b/src/spectator/builders/example_group_builder.cr new file mode 100644 index 0000000..159b9ac --- /dev/null +++ b/src/spectator/builders/example_group_builder.cr @@ -0,0 +1,13 @@ +require "./example_builder" + +module Spectator::Builders + abstract class ExampleGroupBuilder + alias Child = NestedExampleGroupBuilder | ExampleBuilder + + private getter children = [] of Child + + def add_child(child : Child) + @children << child + end + end +end diff --git a/src/spectator/builders/example_group_stack.cr b/src/spectator/builders/example_group_stack.cr index cf91a45..0a9b792 100644 --- a/src/spectator/builders/example_group_stack.cr +++ b/src/spectator/builders/example_group_stack.cr @@ -1,8 +1,14 @@ +require "./root_example_group_builder" +require "./nested_example_group_builder" + module Spectator::Builders struct ExampleGroupStack - getter root = RootExampleGroupBuilder.new + getter root - @stack = Deque(ExampleGroupBuilder).new(1, @root) + def initialize + @root = RootExampleGroupBuilder.new + @stack = Deque(ExampleGroupBuilder).new(1, @root) + end def current @stack.last diff --git a/src/spectator/builders/nested_example_group_builder.cr b/src/spectator/builders/nested_example_group_builder.cr new file mode 100644 index 0000000..d2c748b --- /dev/null +++ b/src/spectator/builders/nested_example_group_builder.cr @@ -0,0 +1,16 @@ +require "./example_group_builder" + +module Spectator::Builders + class NestedExampleGroupBuilder < ExampleGroupBuilder + def initialize(@what : String | Symbol) + end + + def build(group) + NestedExampleGroup.new(@what, group).tap do |group| + group.children = children.map do |child| + child.build(group).as(ExampleComponent) + end + end + end + end +end diff --git a/src/spectator/builders/root_example_group_builder.cr b/src/spectator/builders/root_example_group_builder.cr new file mode 100644 index 0000000..fb3e27f --- /dev/null +++ b/src/spectator/builders/root_example_group_builder.cr @@ -0,0 +1,13 @@ +require "./example_group_builder" + +module Spectator::Builders + class RootExampleGroupBuilder < ExampleGroupBuilder + def build + RootExampleGroup.new.tap do |group| + group.children = children.map do |child| + child.build(group).as(ExampleComponent) + end + end + end + end +end diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr index b815dce..569bea5 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -1,3 +1,5 @@ +require "../builders/example_group_stack" + module Spectator::DSL # Global builder used to create the runtime instance of the spec. # The DSL methods call into this module to generate parts of the spec. @@ -14,7 +16,7 @@ module Spectator::DSL # 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) + group = Builders::NestedExampleGroupBuilder.new(*args) @@stack.push(group) end @@ -88,7 +90,7 @@ module Spectator::DSL # 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(Internals::SampleValues.empty) + group = @@stack.root.build TestSuite.new(group, filter) end end diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 3b5cbb2..56af0c7 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -25,9 +25,11 @@ module Spectator def symbolic? description = @test_wrapper.description - description.start_with?('#') || description.start_with?('.') + 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. @@ -39,9 +41,6 @@ module Spectator @finished = true end - # Implementation-specific for running the example code. - private abstract def run_impl : Result - # Creates the base of the example. # The group should be the example group the example belongs to. def initialize(@group, @test_wrapper) diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index 2d6b28e..b703cd7 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -15,10 +15,6 @@ module Spectator include Enumerable(ExampleComponent) include Iterable(ExampleComponent) - # Creates the example group. - def initialize - end - # 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. diff --git a/src/spectator/runnable_example.cr b/src/spectator/runnable_example.cr index 7964ff1..9dca2d1 100644 --- a/src/spectator/runnable_example.cr +++ b/src/spectator/runnable_example.cr @@ -16,28 +16,6 @@ module Spectator # A captured result is returned. private def capture_result ResultCapture.new.tap do |result| - # Get the proc that will call around-each hooks and the example. - wrapper = wrap_run_example(result) - - run_wrapper(wrapper) - end - end - - private def run_wrapper(wrapper) - wrapper.call - rescue ex - # If an error occurs calling the wrapper, - # it means it came from the "around-each" hooks. - # This is because the test code is completely wrapped with a begin/rescue block. - raise Exception.new("Error encountered while running around hooks", ex) - end - - # Creates a proc that runs the test code - # and captures the result. - private def wrap_run_example(result) - # Wrap the method that runs and captures - # the test code with the around-each hooks. - group.wrap_around_each_hooks do run_example(result) end end diff --git a/src/spectator/runner.cr b/src/spectator/runner.cr index 306f837..afd517c 100644 --- a/src/spectator/runner.cr +++ b/src/spectator/runner.cr @@ -35,7 +35,7 @@ module Spectator result = run_example(example).as(Result) results << result if @config.fail_fast? && result.is_a?(FailedResult) - example.group.run_after_all_hooks(ignore_unfinished: true) + # TODO: example.group.run_after_all_hooks(ignore_unfinished: true) break end end diff --git a/src/spectator/test_wrapper.cr b/src/spectator/test_wrapper.cr index 761d4f4..77c097b 100644 --- a/src/spectator/test_wrapper.cr +++ b/src/spectator/test_wrapper.cr @@ -2,6 +2,8 @@ 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 @@ -14,7 +16,7 @@ module Spectator # Creates a wrapper for the test. # The *builder* creates an instance of the test. # The *runner* takes the test created by *builder* and runs it. - def initialize(@description, @source, @builder : -> SpectatorTest, @runner : SpectatorTest ->) + def initialize(@description, @source, @builder : -> ::SpectatorTest, @runner : TestMethod) end # Instantiates and runs the test. From 5d86b4f50e20486b44b96697b3d92bffcaa5773e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 12 Sep 2019 20:26:54 -0600 Subject: [PATCH 023/205] Get examples working again --- src/spectator/dsl/builder.cr | 4 ++-- src/spectator/dsl/structure_dsl.cr | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr index 569bea5..718191a 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -46,8 +46,8 @@ module Spectator::DSL example_type : ::SpectatorTest.class, &runner : ::SpectatorTest ->) : Nil builder = ->{ example_type.new.as(::SpectatorTest) } wrapper = TestWrapper.new(description, source, builder, runner) - example = Example.new(current_group, wrapper) - # TODO: Add to stack. + factory = Builders::ExampleBuilder.new(wrapper) + @@stack.current.add_child(factory) end # Adds a block of code to run before all examples in the current group. diff --git a/src/spectator/dsl/structure_dsl.cr b/src/spectator/dsl/structure_dsl.cr index 05113b6..30a75f5 100644 --- a/src/spectator/dsl/structure_dsl.cr +++ b/src/spectator/dsl/structure_dsl.cr @@ -1424,7 +1424,7 @@ module Spectator::DSL {% end %} {% else %} def %run - {{block}} + {{block.body}} end {% end %} From fafe5606e6aa019d42f03a770ec2ec809e9c8de6 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 13 Sep 2019 14:10:25 -0600 Subject: [PATCH 024/205] Rename DSL::Builder to SpecBuilder Place all builders under SpecBuilder. --- src/spectator.cr | 2 +- src/spectator/dsl/structure_dsl.cr | 28 +++++++++---------- src/spectator/includes.cr | 1 + .../{dsl/builder.cr => spec_builder.cr} | 12 ++++---- .../example_builder.cr | 2 +- .../example_group_builder.cr | 2 +- .../example_group_stack.cr | 2 +- .../nested_example_group_builder.cr | 2 +- .../root_example_group_builder.cr | 2 +- 9 files changed, 27 insertions(+), 26 deletions(-) rename src/spectator/{dsl/builder.cr => spec_builder.cr} (93%) rename src/spectator/{builders => spec_builder}/example_builder.cr (85%) rename src/spectator/{builders => spec_builder}/example_group_builder.cr (89%) rename src/spectator/{builders => spec_builder}/example_group_stack.cr (94%) rename src/spectator/{builders => spec_builder}/nested_example_group_builder.cr (92%) rename src/spectator/{builders => spec_builder}/root_example_group_builder.cr (90%) diff --git a/src/spectator.cr b/src/spectator.cr index 3a088c4..bcfeda6 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -101,7 +101,7 @@ module Spectator # Builds the tests and runs the framework. private def run # Build the test suite and run it. - suite = ::Spectator::DSL::Builder.build(config.example_filter) + suite = ::Spectator::SpecBuilder.build(config.example_filter) Runner.new(suite, config).run rescue ex # Catch all unhandled exceptions here. diff --git a/src/spectator/dsl/structure_dsl.cr b/src/spectator/dsl/structure_dsl.cr index 30a75f5..a2799e1 100644 --- a/src/spectator/dsl/structure_dsl.cr +++ b/src/spectator/dsl/structure_dsl.cr @@ -263,7 +263,7 @@ module Spectator::DSL {% end %} # Start a new group. - ::Spectator::DSL::Builder.start_group( + ::Spectator::SpecBuilder.start_group( {% if what.is_a?(StringLiteral) %} {% if what.starts_with?("#") || what.starts_with?(".") %} {{what.id.symbolize}} @@ -279,7 +279,7 @@ module Spectator::DSL {{block.body}} # End the current group. - ::Spectator::DSL::Builder.end_group + ::Spectator::SpecBuilder.end_group end end @@ -543,7 +543,7 @@ module Spectator::DSL # Start a new example group. # Sample groups require additional configuration. - ::Spectator::DSL::Builder.start_sample_group( + ::Spectator::SpecBuilder.start_sample_group( {{collection.stringify}}, # String representation of the collection. Sample%sample, # Type that can construct the elements. ->(s : Sample%sample) { s.%to_a }, # Proc to build the array of elements in the collection. @@ -555,7 +555,7 @@ module Spectator::DSL {{block.body}} # End the current group. - ::Spectator::DSL::Builder.end_group + ::Spectator::SpecBuilder.end_group end end @@ -656,7 +656,7 @@ module Spectator::DSL # Start a new example group. # Sample groups require additional configuration. - ::Spectator::DSL::Builder.start_sample_group( + ::Spectator::SpecBuilder.start_sample_group( {{collection.stringify}}, # String representation of the collection. Sample%sample, # All elements in the collection. {{name.stringify}}, # Name for the current element. @@ -667,7 +667,7 @@ module Spectator::DSL {{block.body}} # End the current group. - ::Spectator::DSL::Builder.end_group + ::Spectator::SpecBuilder.end_group end end @@ -990,7 +990,7 @@ module Spectator::DSL # # See also: `#before_each`, `#after_all`, `#after_each`, and `#around_each`. macro before_all(&block) - ::Spectator::DSL::Builder.add_before_all_hook {{block}} + ::Spectator::SpecBuilder.add_before_all_hook {{block}} end # Creates a hook that will run prior to every example in the group. @@ -1051,7 +1051,7 @@ module Spectator::DSL {{yield}} end - ::Spectator::DSL::Builder.add_before_each_hook do + ::Spectator::SpecBuilder.add_before_each_hook do # Get the wrapper instance and cast to current group type. example = ::Spectator::Internals::Harness.current.example instance = example.instance.as({{@type.id}}) @@ -1110,7 +1110,7 @@ module Spectator::DSL # # See also: `#before_all`, `#before_each`, `#after_each`, and `#around_each`. macro after_all(&block) - ::Spectator::DSL::Builder.add_after_all_hook {{block}} + ::Spectator::SpecBuilder.add_after_all_hook {{block}} end # Creates a hook that will run following every example in the group. @@ -1170,7 +1170,7 @@ module Spectator::DSL {{yield}} end - ::Spectator::DSL::Builder.add_after_each_hook do + ::Spectator::SpecBuilder.add_after_each_hook do # Get the wrapper instance and cast to current group type. example = ::Spectator::Internals::Harness.current.example instance = example.instance.as({{@type.id}}) @@ -1266,7 +1266,7 @@ module Spectator::DSL {{yield}} end - ::Spectator::DSL::Builder.add_around_each_hook do |proc| + ::Spectator::SpecBuilder.add_around_each_hook do |proc| # Get the wrapper instance and cast to current group type. example = ::Spectator::Internals::Harness.current.example instance = example.instance.as({{@type.id}}) @@ -1325,7 +1325,7 @@ module Spectator::DSL {{yield}} end - ::Spectator::DSL::Builder.add_pre_condition do + ::Spectator::SpecBuilder.add_pre_condition do example = ::Spectator::Internals::Harness.current.example instance = example.instance.as({{@type.id}}) instance.%condition @@ -1384,7 +1384,7 @@ module Spectator::DSL {{yield}} end - ::Spectator::DSL::Builder.add_post_condition do + ::Spectator::SpecBuilder.add_post_condition do example = ::Spectator::Internals::Harness.current.example instance = example.instance.as({{@type.id}}) instance.%condition @@ -1429,7 +1429,7 @@ module Spectator::DSL {% end %} %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - ::Spectator::DSL::Builder.add_example({{what.stringify}}, %source, {{@type.name}}) { |test| test.as({{@type.name}}).%run } + ::Spectator::SpecBuilder.add_example({{what.stringify}}, %source, {{@type.name}}) { |test| test.as({{@type.name}}).%run } end # Creates an example, or a test case. diff --git a/src/spectator/includes.cr b/src/spectator/includes.cr index 9861c89..db239f1 100644 --- a/src/spectator/includes.cr +++ b/src/spectator/includes.cr @@ -18,6 +18,7 @@ require "./matchers" require "./formatting" # Then all of the top-level types. +require "./spec_builder" require "./example_component" require "./example" require "./runnable_example" diff --git a/src/spectator/dsl/builder.cr b/src/spectator/spec_builder.cr similarity index 93% rename from src/spectator/dsl/builder.cr rename to src/spectator/spec_builder.cr index 718191a..88f1673 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/spec_builder.cr @@ -1,14 +1,14 @@ -require "../builders/example_group_stack" +require "./spec_builder/*" -module Spectator::DSL +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 Builder + module SpecBuilder extend self - @@stack = Builders::ExampleGroupStack.new + @@stack = ExampleGroupStack.new # Begins a new nested group in the spec. # A corresponding `#end_group` call must be made @@ -16,7 +16,7 @@ module Spectator::DSL # See `NestedExampleGroupBuilder#initialize` for the arguments # as arguments to this method are passed directly to it. def start_group(*args) : Nil - group = Builders::NestedExampleGroupBuilder.new(*args) + group = NestedExampleGroupBuilder.new(*args) @@stack.push(group) end @@ -46,7 +46,7 @@ module Spectator::DSL example_type : ::SpectatorTest.class, &runner : ::SpectatorTest ->) : Nil builder = ->{ example_type.new.as(::SpectatorTest) } wrapper = TestWrapper.new(description, source, builder, runner) - factory = Builders::ExampleBuilder.new(wrapper) + factory = ExampleBuilder.new(wrapper) @@stack.current.add_child(factory) end diff --git a/src/spectator/builders/example_builder.cr b/src/spectator/spec_builder/example_builder.cr similarity index 85% rename from src/spectator/builders/example_builder.cr rename to src/spectator/spec_builder/example_builder.cr index a181216..5961467 100644 --- a/src/spectator/builders/example_builder.cr +++ b/src/spectator/spec_builder/example_builder.cr @@ -1,4 +1,4 @@ -module Spectator::Builders +module Spectator::SpecBuilder class ExampleBuilder def initialize(@wrapper : TestWrapper) end diff --git a/src/spectator/builders/example_group_builder.cr b/src/spectator/spec_builder/example_group_builder.cr similarity index 89% rename from src/spectator/builders/example_group_builder.cr rename to src/spectator/spec_builder/example_group_builder.cr index 159b9ac..69d91a8 100644 --- a/src/spectator/builders/example_group_builder.cr +++ b/src/spectator/spec_builder/example_group_builder.cr @@ -1,6 +1,6 @@ require "./example_builder" -module Spectator::Builders +module Spectator::SpecBuilder abstract class ExampleGroupBuilder alias Child = NestedExampleGroupBuilder | ExampleBuilder diff --git a/src/spectator/builders/example_group_stack.cr b/src/spectator/spec_builder/example_group_stack.cr similarity index 94% rename from src/spectator/builders/example_group_stack.cr rename to src/spectator/spec_builder/example_group_stack.cr index 0a9b792..50385bb 100644 --- a/src/spectator/builders/example_group_stack.cr +++ b/src/spectator/spec_builder/example_group_stack.cr @@ -1,7 +1,7 @@ require "./root_example_group_builder" require "./nested_example_group_builder" -module Spectator::Builders +module Spectator::SpecBuilder struct ExampleGroupStack getter root diff --git a/src/spectator/builders/nested_example_group_builder.cr b/src/spectator/spec_builder/nested_example_group_builder.cr similarity index 92% rename from src/spectator/builders/nested_example_group_builder.cr rename to src/spectator/spec_builder/nested_example_group_builder.cr index d2c748b..1693c52 100644 --- a/src/spectator/builders/nested_example_group_builder.cr +++ b/src/spectator/spec_builder/nested_example_group_builder.cr @@ -1,6 +1,6 @@ require "./example_group_builder" -module Spectator::Builders +module Spectator::SpecBuilder class NestedExampleGroupBuilder < ExampleGroupBuilder def initialize(@what : String | Symbol) end diff --git a/src/spectator/builders/root_example_group_builder.cr b/src/spectator/spec_builder/root_example_group_builder.cr similarity index 90% rename from src/spectator/builders/root_example_group_builder.cr rename to src/spectator/spec_builder/root_example_group_builder.cr index fb3e27f..9b1d3df 100644 --- a/src/spectator/builders/root_example_group_builder.cr +++ b/src/spectator/spec_builder/root_example_group_builder.cr @@ -1,6 +1,6 @@ require "./example_group_builder" -module Spectator::Builders +module Spectator::SpecBuilder class RootExampleGroupBuilder < ExampleGroupBuilder def build RootExampleGroup.new.tap do |group| From 3f7c16c888fd54e483716b150d8b275ef14f8b3b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 13 Sep 2019 14:33:46 -0600 Subject: [PATCH 025/205] Move all DSL to one module, but separate files --- src/spectator/dsl.cr | 1 - src/spectator/dsl/example_dsl.cr | 9 +- src/spectator/dsl/matcher_dsl.cr | 5 +- src/spectator/dsl/structure_dsl.cr | 153 +---------------------------- src/spectator_test.cr | 6 +- 5 files changed, 8 insertions(+), 166 deletions(-) diff --git a/src/spectator/dsl.cr b/src/spectator/dsl.cr index 5689a6f..0d13112 100644 --- a/src/spectator/dsl.cr +++ b/src/spectator/dsl.cr @@ -2,7 +2,6 @@ require "./dsl/*" module Spectator # Namespace containing methods representing the spec domain specific language. - # Also contains builders to generate classes and instances to later run the spec. module DSL end end diff --git a/src/spectator/dsl/example_dsl.cr b/src/spectator/dsl/example_dsl.cr index 95f4a1c..f315a63 100644 --- a/src/spectator/dsl/example_dsl.cr +++ b/src/spectator/dsl/example_dsl.cr @@ -2,14 +2,9 @@ require "../expectations/expectation_partial" require "../source" require "../test_block" require "../test_value" -require "./matcher_dsl" - -module Spectator::DSL - # Methods that are available inside test code. - # Basically, inside an `StructureDSL#it` block. - module ExampleDSL - include MatcherDSL +module Spectator + module DSL # Starts an expectation. # This should be followed up with `Spectator::Expectations::ExpectationPartial#to` # or `Spectator::Expectations::ExpectationPartial#to_not`. diff --git a/src/spectator/dsl/matcher_dsl.cr b/src/spectator/dsl/matcher_dsl.cr index bcfbec1..5c55a06 100644 --- a/src/spectator/dsl/matcher_dsl.cr +++ b/src/spectator/dsl/matcher_dsl.cr @@ -2,9 +2,8 @@ require "../matchers" require "../test_block" require "../test_value" -module Spectator::DSL - # Methods for defining matchers for expectations. - module MatcherDSL +module Spectator + module DSL # Indicates that some value should equal another. # The == operator is used for this check. # The value passed to this method is the expected value. diff --git a/src/spectator/dsl/structure_dsl.cr b/src/spectator/dsl/structure_dsl.cr index a2799e1..87f1fb3 100644 --- a/src/spectator/dsl/structure_dsl.cr +++ b/src/spectator/dsl/structure_dsl.cr @@ -1,156 +1,7 @@ require "../example_group" -module Spectator::DSL - # Domain specific language for the main structure of a spec. - # The primary components of this are `#describe`, `#context`, and `#it`. - # - # These macros define modules and classes. - # Those modules and classes are used to create the test cases. - # - # A class is created for every block of code that contains test code. - # An `#it` block creates a class derived from `RunnableExample`. - # A `#pending` block creates a class derived from `PendingExample`. - # The classes are built so that they run the example's code when invoked. - # However, the actual example code is placed into a separate "wrapper" class. - # This is done to avoid overlap with the Spectator namespace. - # The example code ends up having visibility only into itself and the DSL. - # - # Here's some skeleton code to demonstrate this: - # ``` - # it "does something" do - # # Test code goes here... - # end - # ``` - # - # becomes... - # - # ``` - # # Class describing the example - # # and provides a means of running the test. - # # Typically every class, module, and method - # # that the user might see or be able to reference is obscured. - # # Fresh variables from Crystal's macros are used to achive this. - # # It makes debugging Spectator more difficult, - # # but prevents name collision with user code. - # class Example123 < RunnableExample - # def initialize(group, sample_values) - # # group and sample_values are covered later. - # super - # @instance = Test123.new(sample_values) - # end - # - # # Returns the text provided by the user. - # # This isn't stored as a member - # # so that it can be referenced directly in compiled code. - # def what - # "does something" - # end - # - # # This method is called by `RunnableExample` - # # when the example code should be ran. - # def run_instance - # @instance._run123 - # end - # end - # - # # Wrapper class for the example code. - # # This isolates it from Spectator's internals. - # class Test123 - # include Context123 # More on this in a bit. - # include ExampleDSL # Include DSL for the example code. - # - # # Generated method name to avoid conflicts. - # def _run123 - # # Test code goes here... - # end - # end - # ``` - # - # Modules are used to provide context and share methods across examples. - # They are used as mix-ins for the example code. - # The example code wrapper class includes its parent module. - # This allows the example to access anything that was defined in the same context. - # Contexts can be nested, and this is achieved by including the parent module. - # Whenever a module or class is defined, - # it includes its parent so that functionality can be inherited. - # - # For example: - # ``` - # describe "#foo" do - # subject { described_class.foo(value) } - # - # context "when given :bar" do - # let(value) { :bar } - # - # it "does something" do - # # ... - # end - # end - # end - # ``` - # - # becomes... - # - # ``` - # # describe "#foo" - # module Context123 - # # Start a new group. - # # More on this in a bit. - # Builder.start_group("#foo") - # - # def subject - # described_class.foo(value) - # end - # - # # context "when given :bar" - # module Context456 - # include Context123 # Inherit parent module. - # - # # Start a nested group. - # Builder.start_group("when given :bar") - # - # def value - # :bar - # end - # - # # Wrapper class for the test case. - # class Test456 - # include Context456 # Include context. - # - # # Rest of test code... - # end - # - # # Example class for the test case. - # class Example456 < RunnableExample - # # Rest of example code... - # end - # - # # Add example to group. - # Builder.add_example(Example456) - # - # # End "when given :bar" group. - # Builder.end_group - # end - # - # # End "#foo" group. - # Builder.end_group - # end - # ``` - # - # In addition to providing modules as mix-ins, - # example groups are defined with `#describe` and `#context`. - # The DSL makes use of `Builder` to construct the run-time portion of the spec. - # As groups are defined, they are pushed on a stack - # and popped off after everything nested in them is defined. - # `Builder` tracks the current group (top of the stack). - # This way, examples, hooks, nested groups, and other items can be added to it. - # Groups and examples are nested in a parent group. - # The only group that isn't nested is the root group - `RootExampleGroup`. - # - # Some example groups make use of sample values. - # Sample values are a collection of test values that can be used in examples. - # For more information, see `Internals::SampleValues`. - module StructureDSL +module Spectator + module DSL # Creates a new example group to describe a component. # The *what* argument describes "what" is being tested. # Additional example groups and DSL may be nested in the block. diff --git a/src/spectator_test.cr b/src/spectator_test.cr index 439ceb9..8f14454 100644 --- a/src/spectator_test.cr +++ b/src/spectator_test.cr @@ -1,10 +1,8 @@ -require "./spectator/dsl/example_dsl" -require "./spectator/dsl/structure_dsl" +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::StructureDSL # Include the DSL for creating groups, example, and more. - include ::Spectator::DSL::ExampleDSL # Mix in methods and macros specifically for example DSL. + include ::Spectator::DSL end From c94b284ab3cc6710dd23e607cf38fdd24796c2b4 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 15 Sep 2019 09:15:33 -0600 Subject: [PATCH 026/205] Replace large DSL files with smaller, logical groups --- .../dsl/{example_dsl.cr => assertions.cr} | 0 src/spectator/dsl/examples.cr | 29 + src/spectator/dsl/groups.cr | 29 + .../dsl/{matcher_dsl.cr => matchers.cr} | 0 src/spectator/dsl/structure_dsl.cr | 1385 ----------------- 5 files changed, 58 insertions(+), 1385 deletions(-) rename src/spectator/dsl/{example_dsl.cr => assertions.cr} (100%) create mode 100644 src/spectator/dsl/examples.cr create mode 100644 src/spectator/dsl/groups.cr rename src/spectator/dsl/{matcher_dsl.cr => matchers.cr} (100%) delete mode 100644 src/spectator/dsl/structure_dsl.cr diff --git a/src/spectator/dsl/example_dsl.cr b/src/spectator/dsl/assertions.cr similarity index 100% rename from src/spectator/dsl/example_dsl.cr rename to src/spectator/dsl/assertions.cr diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr new file mode 100644 index 0000000..738fb2d --- /dev/null +++ b/src/spectator/dsl/examples.cr @@ -0,0 +1,29 @@ +require "../source" +require "../spec_builder" + +module Spectator + module DSL + macro it(what, _source_file = __FILE__, _source_line = __LINE__, &block) + {% if block.is_a?(Nop) %} + {% if what.is_a?(Call) %} + def %run + {{what}} + end + {% else %} + {% raise "Unrecognized syntax: `it #{what}` 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( + {{what.stringify}}, + %source, + {{@type.name}} + ) { |test| test.as({{@type.name}}).%run } + end + end +end diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr new file mode 100644 index 0000000..60571bf --- /dev/null +++ b/src/spectator/dsl/groups.cr @@ -0,0 +1,29 @@ +require "../spec_builder" + +module Spectator + module DSL + macro context(what, &block) + class Context%context < {{@type.id}} + ::Spectator::SpecBuilder.start_group( + {% if what.is_a?(StringLiteral) %} + {% if what.starts_with?("#") || what.starts_with?(".") %} + {{what.id.symbolize}} + {% else %} + {{what}} + {% end %} + {% else %} + {{what.symbolize}} + {% end %} + ) + + {{block.body}} + + ::Spectator::SpecBuilder.end_group + end + end + + macro describe(what, &block) + context({{what}}) {{block}} + end + end +end diff --git a/src/spectator/dsl/matcher_dsl.cr b/src/spectator/dsl/matchers.cr similarity index 100% rename from src/spectator/dsl/matcher_dsl.cr rename to src/spectator/dsl/matchers.cr diff --git a/src/spectator/dsl/structure_dsl.cr b/src/spectator/dsl/structure_dsl.cr deleted file mode 100644 index 87f1fb3..0000000 --- a/src/spectator/dsl/structure_dsl.cr +++ /dev/null @@ -1,1385 +0,0 @@ -require "../example_group" - -module Spectator - module DSL - # Creates a new example group to describe a component. - # The *what* argument describes "what" is being tested. - # Additional example groups and DSL may be nested in the block. - # - # Typically when testing a method, - # the spec is written like so: - # ``` - # describe "#foo" do - # it "does something" do - # # ... - # end - # end - # ``` - # - # When describing a class (or any other type), - # the *what* parameter doesn't need to be quoted. - # ``` - # describe String do - # it "does something" do - # # ... - # end - # end - # ``` - # - # And when combining the two together: - # ``` - # describe String do - # describe "#size" do - # it "returns the length" do - # # ... - # end - # end - # end - # ``` - # - # The `#describe` and `#context` are identical in terms of functionality. - # However, `#describe` is typically used on classes and methods, - # while `#context` is used for use cases and scenarios. - macro describe(what, &block) - context({{what}}) {{block}} - end - - # Creates a new example group to describe a situation. - # The *what* argument describes the scenario or case being tested. - # Additional example groups and DSL may be nested in the block. - # - # The `#describe` and `#context` are identical in terms of functionality. - # However, `#describe` is typically used on classes and methods, - # while `#context` is used for use cases and scenarios. - # - # Using context blocks in conjunction with hooks, `#let`, and other methods - # provide an easy way to define the scenario in code. - # This also gives each example in the context an identical situation to run in. - # - # For instance: - # ``` - # describe String do - # context "when empty" do - # subject { "" } - # - # it "has a size of zero" do - # expect(subject.size).to eq(0) - # end - # - # it "is blank" do - # expect(subject.blank?).to be_true - # end - # end - # - # context "when not empty" do - # subject { "foobar" } - # - # it "has a non-zero size" do - # expect(subject.size).to_not eq(0) - # end - # - # it "is not blank" do - # expect(subject.blank?).to be_false - # end - # end - # end - # ``` - # - # While this is a somewhat contrived example, - # it demonstrates how contexts can reuse code. - # Contexts also make it clearer how a scenario is setup. - macro context(what, &block) - # Class for the context. - # The class uses a generated unique name. - class Context%context < {{@type.id}} - # Check if `what` looks like a type. - # If it is, add the `#described_class` and `subject` methods. - # At the time of writing this code, - # this is the way (at least that I know of) - # to check if an AST node is a type name. - # - # NOTE: In Crystal 0.27, it looks like `#resolve` can be used. - # Need to investigate, but would also increase minimum version. - {% if what.is_a?(Path) || what.is_a?(Generic) %} - # Returns the type currently being described. - macro described_class - {{what}} - end - - # Implicit subject definition. - # Simply creates a new instance of the described type. - def subject(*args) - described_class.new(*args) - end - {% end %} - - # Start a new group. - ::Spectator::SpecBuilder.start_group( - {% if what.is_a?(StringLiteral) %} - {% if what.starts_with?("#") || what.starts_with?(".") %} - {{what.id.symbolize}} - {% else %} - {{what}} - {% end %} - {% else %} - {{what.symbolize}} - {% end %} - ) - - # Nest the block's content in the module. - {{block.body}} - - # End the current group. - ::Spectator::SpecBuilder.end_group - end - end - - # Creates an example group with a very concise syntax. - # This can be used in scenarios where one or more input values - # change the result of various methods. - # The normal DSL can be used within this context, - # but a shorter syntax provides an easier way to read and write multiple tests. - # - # Here's an example of where this is useful: - # ``` - # describe Int32 do - # subject { described_class.new(value) } - # - # context "when given 5" do - # describe "#odd?" do - # subject { value.odd? } - # - # it "is true" do - # is_expected.to be_true - # end - # - # # NOTE: These could also be the one-liner syntax, - # # but that is still very verbose. - # end - # - # describe "#even?" do - # subject { value.even? } - # - # it "is false" do - # is_expected.to be_false - # end - # end - # end - # - # context "when given 42" do - # describe "#odd?" do - # subject { value.odd? } - # - # it "is false" do - # is_expected.to be_false - # end - # end - # - # describe "#even?" do - # subject { value.even? } - # - # it "is true" do - # is_expected.to be_true - # end - # end - # end - # end - # ``` - # - # There's a lot of repetition and nested groups - # to test a very simple scenario. - # - # Using a `#given` block, this type of scenario becomes much more compact. - # ``` - # describe Int32 do - # subject { described_class.new(value) } - # - # given value = 5 do - # expect(&.odd?).to be_true - # expect(&.even?).to be_false - # end - # - # given value = 42 do - # expect(&.odd?).to be_false - # expect(&.even?).to be_true - # end - # end - # ``` - # - # One or more assignments can be used. - # Each assignment is passed to its own `#let`. - # For example: - # ``` - # given x = 1, y = 2 do - # expect(x + y).to eq(3) - # end - # ``` - # - # Each statement in the block is converted to the one-liner syntax of `#it`. - # For instance: - # ``` - # given x = 1 do - # expect(x).to eq(1) - # end - # ``` - # is converted to: - # ``` - # context "x = 1" do - # let(x) { 1 } - # - # it expect(x).to eq(1) - # end - # ``` - # - # Additionally, the "it" syntax can be used and mixed in. - # This allows for flexibility and a more readable format when needed. - # ``` - # given x = 1 do - # it "is odd" do - # expect(x.odd?).to be_true - # end - # - # it is(&.odd?) - # end - # ``` - macro given(*assignments, &block) - context({{assignments.splat.stringify}}) do - # Create a `let` entry for each assignment. - {% 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 body for given block" - 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 - %} - - # Transform every item in the block to a test case. - {% 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 - - # Creates a new example group to test multiple values with. - # This method takes a collection of values - # and repeats the contents of the block with each value. - # The *collection* argument should be a literal collection, - # such as an array, or a function that returns an enumerable. - # Additionally, a count may be specified to limit the number of values tested. - # - # NOTE: If an infinite enumerable is provided for the collection, - # then a count must be specified. - # Only the first *count* items will be used. - # - # The block can accept an argument. - # If it does, then the argument's name is used to reference - # the current item in the collection. - # If an argument isn't provided, then *value* can be used instead. - # - # Example with a block argument: - # ``` - # sample some_integers do |integer| - # it "sets the value" do - # subject.value = integer - # expect(subject.value).to eq(integer) - # end - # end - # ``` - # - # Same spec, but without a block argument: - # ``` - # sample some_integers do - # it "sets the value" do - # subject.value = value - # expect(subject.value).to eq(value) - # end - # end - # ``` - # - # In the examples above, the test case (`#it` block) - # is repeated for each element in *some_integers*. - # *some_integers* is a ficticous collection. - # The collection will be iterated once. - # `#sample` and `#random_sample` blocks can be nested, - # and work similarly to loops. - # - # A limit can be specified as well. - # After the collection, a count can be added to limit - # the number of items taken from the collection. - # For instance: - # ``` - # sample some_integers, 5 do |integer| - # it "sets the value" do - # subject.value = integer - # expect(subject.value).to eq(integer) - # end - # end - # ``` - # - # See also: `#random_sample` - macro sample(collection, count = nil, &block) - # Figure out the name to use for the current collection element. - # If a block argument is provided, use it, otherwise use "value". - {% name = block.args.empty? ? "value".id : block.args.first %} - - # Method for retrieving the entire collection. - # This simplifies getting the element type. - # The name is uniquely generated to prevent namespace collision. - # This method should be called only once. - def %sample - {{collection}} - end - - # Class for generating an array with the collection's contents. - # This has to be a class that includes the parent module. - # The collection could reference a helper method - # or anything else in the parent scope. - class Sample%sample < {{@type.id}} - # Method that returns an array containing the collection. - # This method should be called only once. - # The framework stores the collection as an array for a couple of reasons. - # 1. The collection may not support multiple iterations. - # 2. The collection might contain randomly generated values. - # Iterating multiple times would generate inconsistent values at runtime. - def %to_a - # If a count was provided, - # only select the first *count* items from the collection. - # Otherwise, select all of them. - {% if count %} - %sample.first({{count}}) - {% else %} - %sample.to_a - {% end %} - end - end - - # Class for the context. - # The class uses a generated unique name. - class Context%sample < {{@type.id}} - # Value wrapper for the current element. - @%wrapper : ::Spectator::Internals::ValueWrapper - - # Retrieves the current element from the collection. - def {{name}} - # Unwrap the value and return it. - # The `Enumerable#first` method has a return type that matches the element type. - # So it is used on the collection method proxy to resolve the type at compile-time. - @%wrapper.as(::Spectator::Internals::TypedValueWrapper(typeof(%sample.first))).value - end - - # Initializer to extract current element of the collection from sample values. - def initialize - super - @%wrapper = ::Spectator::Internals::Harness.current.group.sample_values.get_wrapper(:%sample) - end - - # Start a new example group. - # Sample groups require additional configuration. - ::Spectator::SpecBuilder.start_sample_group( - {{collection.stringify}}, # String representation of the collection. - Sample%sample, # Type that can construct the elements. - ->(s : Sample%sample) { s.%to_a }, # Proc to build the array of elements in the collection. - {{name.stringify}}, # Name for the current element. - :%sample # Unique identifier for retrieving elements for the associated collection. - ) - - # Nest the block's content in the module. - {{block.body}} - - # End the current group. - ::Spectator::SpecBuilder.end_group - end - end - - # Creates a new example group to test multiple random values with. - # This method takes a collection of values and count - # and repeats the contents of the block with each value. - # This method randomly selects *count* items from the collection. - # The *collection* argument should be a literal collection, - # such as an array, or a function that returns an enumerable. - # - # NOTE: If an enumerable is used, it must be finite. - # - # The block can accept an argument. - # If it does, then the argument's name is used to reference - # the current item in the collection. - # If an argument isn't provided, then *value* can be used instead. - # - # Example with a block argument: - # ``` - # random_sample some_integers, 5 do |integer| - # it "sets the value" do - # subject.value = integer - # expect(subject.value).to eq(integer) - # end - # end - # ``` - # - # Same spec, but without a block argument: - # ``` - # random_sample some_integers, 5 do - # it "sets the value" do - # subject.value = value - # expect(subject.value).to eq(value) - # end - # end - # ``` - # - # In the examples above, the test case (`#it` block) - # is repeated for 5 random elements in *some_integers*. - # *some_integers* is a ficticous collection. - # The collection will be iterated once. - # `#sample` and `#random_sample` blocks can be nested, - # and work similarly to loops. - # - # NOTE: If the count is the same or higher - # than the number of elements in the collection, - # then this method if functionaly equivalent to `#sample`. - # - # See also: `#sample` - macro random_sample(collection, count, &block) - # Figure out the name to use for the current collection element. - # If a block argument is provided, use it, otherwise use "value". - {% name = block.args.empty? ? "value".id : block.args.first %} - - # Method for retrieving the entire collection. - # This simplifies getting the element type. - # The name is uniquely generated to prevent namespace collision. - # This method should be called only once. - def %sample - {{collection}} - end - - # Class for generating an array with the collection's contents. - # This has to be a class that includes the parent module. - # The collection could reference a helper method - # or anything else in the parent scope. - class Sample%sample < {{@type.id}} - # Method that returns an array containing the collection. - # This method should be called only once. - # The framework stores the collection as an array for a couple of reasons. - # 1. The collection may not support multiple iterations. - # 2. The collection might contain randomly generated values. - # Iterating multiple times would generate inconsistent values at runtime. - def %to_a - %sample.to_a.sample({{count}}, ::Spectator.random) - end - end - - # Class for the context. - # The class uses a generated unique name. - class Context%sample < {{@type.id}} - # Value wrapper for the current element. - @%wrapper : ::Spectator::Internals::ValueWrapper - - # Retrieves the current element from the collection. - def {{name}} - # Unwrap the value and return it. - # The `Enumerable#first` method has a return type that matches the element type. - # So it is used on the collection method proxy to resolve the type at compile-time. - @%wrapper.as(::Spectator::Internals::TypedValueWrapper(typeof(%sample.first))).value - end - - # Initializer to extract current element of the collection from sample values. - def initialize - super - @%wrapper = ::Spectator::Internals::Harness.current.group.sample_values.get_wrapper(:%sample) - end - - # Start a new example group. - # Sample groups require additional configuration. - ::Spectator::SpecBuilder.start_sample_group( - {{collection.stringify}}, # String representation of the collection. - Sample%sample, # All elements in the collection. - {{name.stringify}}, # Name for the current element. - :%sample # Unique identifier for retrieving elements for the associated collection. - ) - - # Nest the block's content in the module. - {{block.body}} - - # End the current group. - ::Spectator::SpecBuilder.end_group - end - end - - # Explicitly defines the subject being tested. - # The `#subject` method can be used in examples to retrieve the value (basically a method). - # - # This macro expects a block. - # The block should return the value of the subject. - # This can be used to define a value once and reuse it in multiple examples. - # - # For instance: - # ``` - # subject { "foobar" } - # - # it "isn't empty" do - # expect(subject.empty?).to be_false - # end - # - # it "is six characters" do - # expect(subject.size).to eq(6) - # end - # ``` - # - # By using a subject, some of the DSL becomes simpler. - # For example, `ExampleDSL#is_expected` can be used. - # ``` - # subject { "foobar" } - # - # it "isn't empty" do - # is_expected.to_not be_empty # is the same as: - # expect(subject).to_not be_empty - # end - # ``` - # - # This macro is functionaly equivalent to: - # ``` - # let(:subject) { "foo" } - # ``` - # - # The subject is created the first time it is referenced (lazy initialization). - # It is cached so that the same instance is used throughout the test. - # The subject will be recreated for each test it is used in. - # - # ``` - # subject { [0, 1, 2] } - # - # it "modifies the array" do - # subject[0] = 42 - # is_expected.to eq([42, 1, 2]) - # end - # - # it "doesn't carry across tests" do - # subject[1] = 777 - # is_expected.to eq([0, 777, 2]) - # end - # ``` - macro subject(&block) - let(:subject) {{block}} - end - - # Explicitly defines the subject being tested. - # The `#subject` method can be used in examples to retrieve the value (basically a method). - # This also names the subject so that it can be referenced by the name instead. - # - # This macro expects a block. - # The block should return the value of the subject. - # This can be used to define a value once and reuse it in multiple examples. - # - # For instance: - # ``` - # subject(string) { "foobar" } - # - # it "isn't empty" do - # # Refer to it with `subject`. - # expect(subject.empty?).to be_false - # end - # - # it "is six characters" do - # # Refer to it by its name `string`. - # expect(string.size).to eq(6) - # end - # ``` - # - # The subject is created the first time it is referenced (lazy initialization). - # It is cached so that the same instance is used throughout the test. - # The subject will be recreated for each test it is used in. - macro subject(name, &block) - let({{name.id}}) {{block}} - subject { {{name.id}} } - end - - # Defines an expression by name. - # The name can be used in examples to retrieve the value (basically a method). - # This can be used to define a value once and reuse it in multiple examples. - # - # There are two variants - assignment and block. - # Both must be given a name. - # - # For the assignment variant: - # ``` - # let string = "foobar" - # - # it "isn't empty" do - # expect(string.empty?).to be_false - # end - # - # it "is six characters" do - # expect(string.size).to eq(6) - # end - # ``` - # - # The value is evaluated and stored immediately. - # This is different from other `#let` variants that lazily-evaluate. - # - # ``` - # let current_time = Time.utc - # let(lazy_time) { Time.utc } - # - # it "lazy evaluates" do - # sleep 5 - # expect(lazy_time).to_not eq(now) - # end - # ``` - # - # However, the value is not reused across tests. - # Each test will have its own copy. - # - # ``` - # let array = [0, 1, 2] - # - # it "modifies the array" do - # array[0] = 42 - # expect(array).to eq([42, 1, 2]) - # end - # - # it "doesn't carry across tests" do - # array[1] = 777 - # expect(array).to eq([0, 777, 2]) - # end - # ``` - # - # The block variant expects a name and a block. - # The name can be a symbol or a literal - same as `Object#getter`. - # The block should return the value. - # - # For instance: - # ``` - # let(string) { "foobar" } - # - # it "isn't empty" do - # expect(string.empty?).to be_false - # end - # - # it "is six characters" do - # expect(string.size).to eq(6) - # end - # ``` - # - # The value is lazy-evaluated - - # meaning that it is only created on the first reference to it. - # Afterwards, the value is cached, - # so the same value is returned with consecutive calls. - # - # ``` - # let(current_time) { Time.utc } - # - # it "lazy evaluates" do - # now = current_time - # sleep 5 - # expect(current_time).to eq(now) - # end - # ``` - # - # However, the value is not reused across tests. - # It will be reconstructed the first time it is referenced in the next test. - # - # ``` - # let(array) { [0, 1, 2] } - # - # it "modifies the array" do - # array[0] = 42 - # expect(array).to eq([42, 1, 2]) - # end - # - # it "doesn't carry across tests" do - # array[1] = 777 - # expect(array).to eq([0, 777, 2]) - # end - # ``` - macro let(name, &block) - {% if block.is_a?(Nop) %} - # Assignment variant. - @%value = {{name.value}} - - def {{name.target}} - @%value - end - {% else %} - # Block variant. - - # Create a block that returns the value. - let!(%value) {{block}} - - # Wrapper to hold the value. - # This will be nil if the value hasn't been referenced yet. - # After being referenced, the cached value will be stored in a wrapper. - @%wrapper : ::Spectator::Internals::ValueWrapper? - - # Method for returning the value. - def {{name.id}} - # Check if the value is cached. - # The wrapper will be nil if it isn't. - if (wrapper = @%wrapper) - # It is cached, return that value. - # Unwrap it from the wrapper variable. - # Here we use typeof to get around the issue - # that the macro has no idea what type the value is. - wrapper.unsafe_as(::Spectator::Internals::TypedValueWrapper(typeof(%value))).value - else - # The value isn't cached, - # Construct it and store it in the wrapper. - %value.tap do |value| - @%wrapper = ::Spectator::Internals::TypedValueWrapper(typeof(%value)).new(value) - end - end - end - {% end %} - end - - # The noisier sibling to `#let`. - # Defines an expression by giving it a name. - # The name can be used in examples to retrieve the value (basically a method). - # This can be used to define a value once and reuse it in multiple examples. - # - # This macro expects a name and a block. - # The name can be a symbol or a literal - same as `Object#getter`. - # The block should return the value. - # - # For instance: - # ``` - # let!(string) { "foobar" } - # - # it "isn't empty" do - # expect(string.empty?).to be_false - # end - # - # it "is six characters" do - # expect(string.size).to eq(6) - # end - # ``` - # - # The value is lazy-evaluated - - # meaning that it is only created when it is referenced. - # Unlike `#let`, the value is not cached and is recreated on each call. - # - # ``` - # let!(current_time) { Time.utc } - # - # it "lazy evaluates" do - # now = current_time - # sleep 5 - # expect(current_time).to_not eq(now) - # end - # ``` - macro let!(name) - def {{name.id}} - {{yield}} - end - end - - # Creates a hook that will run prior to any example in the group. - # The block of code provided to this macro is used for the hook. - # The hook is executed only once. - # If the hook raises an exception, - # the current example will be skipped and marked as an error. - # - # NOTE: Inside a `#sample` block, the hook is run once, not once per iteration. - # - # This can be useful to initialize something before testing: - # ``` - # before_all { Thing.start } # 1 - # - # it "does something" do - # # 2 - # end - # ``` - # - # The hook cannot use values and methods in the group like examples can. - # This is because the hook is not associated with one example, but many. - # ``` - # let(array) { [1, 2, 3] } - # before_all { array << 4 } # *ERROR!* - # ``` - # - # If multiple `#before_all` blocks are specified, - # then they are run in the order they were defined. - # ``` - # before_all { Thing.first } # 1 - # before_all { Thing.second } # 2 - # ``` - # - # With nested groups, the outer blocks will run first. - # ``` - # describe Something do - # before_all { Something.start } # 1 - # - # describe "#foo" do - # before_all { Something.foo } # 2 - # - # it "does a cool thing" do - # # 3 - # end - # end - # end - # ``` - # - # NOTE: Pre-conditions should not be checked in a `#before_all` or related block. - # Errors that occur in a `#before_all` block will halt testing and abort with an error. - # Use `#pre_condition` instead for pre-test checks. - # - # See also: `#before_each`, `#after_all`, `#after_each`, and `#around_each`. - macro before_all(&block) - ::Spectator::SpecBuilder.add_before_all_hook {{block}} - end - - # Creates a hook that will run prior to every example in the group. - # The block of code provided to this macro is used for the hook. - # The hook is executed once per example in the group (and sub-groups). - # If the hook raises an exception, - # the current example will be skipped and marked as an error. - # - # NOTE: Inside a `#sample` block, the hook is run before every example of every iteration. - # - # This can be useful for setting up environments for tests: - # ``` - # before_each { Thing.start } # 1 - # - # it "does something" do - # # 2 - # end - # ``` - # - # The hook can use values and methods in the group like examples can. - # It is called in the same scope as the example code. - # ``` - # let(array) { [1, 2, 3] } - # before_each { array << 4 } - # ``` - # - # If multiple `#before_each` blocks are specified, - # then they are run in the order they were defined. - # ``` - # before_each { Thing.first } # 1 - # before_each { Thing.second } # 2 - # ``` - # - # With nested groups, the outer blocks will run first. - # ``` - # describe Something do - # before_each { Something.start } # 1 - # - # describe "#foo" do - # before_each { Something.foo } # 2 - # - # it "does a cool thing" do - # # 3 - # end - # end - # end - # ``` - # - # NOTE: Pre-conditions should not be checked in a `#before_each` or related block. - # Errors that occur in a `#before_each` block will halt testing and abort with an error. - # Use `#pre_condition` instead for pre-test checks. - # - # See also: `#before_all`, `#after_all`, `#after_each`, and `#around_each`. - macro before_each - # Before each hook. - # Defined as a method so that it can access the same scope as the example code. - def %hook : Nil - {{yield}} - end - - ::Spectator::SpecBuilder.add_before_each_hook do - # Get the wrapper instance and cast to current group type. - example = ::Spectator::Internals::Harness.current.example - instance = example.instance.as({{@type.id}}) - instance.%hook - end - end - - # Creates a hook that will run following all examples in the group. - # The block of code provided to this macro is used for the hook. - # The hook is executed only once. - # Even if an example fails or raises an error, the hook will run. - # - # NOTE: Inside a `#sample` block, the hook is run once, not once per iteration. - # - # This can be useful to cleanup after testing: - # ``` - # after_all { Thing.stop } # 2 - # - # it "does something" do - # # 1 - # end - # ``` - # - # The hook cannot use values and methods in the group like examples can. - # This is because the hook is not associated with one example, but many. - # ``` - # let(array) { [1, 2, 3] } - # after_all { array << 4 } # *ERROR!* - # ``` - # - # If multiple `#after_all` blocks are specified, - # then they are run in the order they were defined. - # ``` - # after_all { Thing.first } # 1 - # after_all { Thing.second } # 2 - # ``` - # - # With nested groups, the inner blocks will run first. - # ``` - # describe Something do - # after_all { Something.cleanup } # 3 - # - # describe "#foo" do - # after_all { Something.stop } # 2 - # - # it "does a cool thing" do - # # 1 - # end - # end - # end - # ``` - # - # NOTE: Post-conditions should not be checked in an `#after_all` or related block. - # Errors that occur in an `#after_all` block will halt testing and abort with an error. - # Use `#post_condition` instead for post-test checks. - # - # See also: `#before_all`, `#before_each`, `#after_each`, and `#around_each`. - macro after_all(&block) - ::Spectator::SpecBuilder.add_after_all_hook {{block}} - end - - # Creates a hook that will run following every example in the group. - # The block of code provided to this macro is used for the hook. - # The hook is executed once per example in the group (and sub-groups). - # Even if an example fails or raises an error, the hook will run. - # - # NOTE: Inside a `#sample` block, the hook is run after every example of every iteration. - # - # This can be useful for cleaning up environments after tests: - # ``` - # after_each { Thing.stop } # 2 - # - # it "does something" do - # # 1 - # end - # ``` - # - # The hook can use values and methods in the group like examples can. - # It is called in the same scope as the example code. - # ``` - # let(array) { [1, 2, 3] } - # after_each { array << 4 } - # ``` - # - # If multiple `#after_each` blocks are specified, - # then they are run in the order they were defined. - # ``` - # after_each { Thing.first } # 1 - # after_each { Thing.second } # 2 - # ``` - # - # With nested groups, the inner blocks will run first. - # ``` - # describe Something do - # after_each { Something.cleanup } # 3 - # - # describe "#foo" do - # after_each { Something.stop } # 2 - # - # it "does a cool thing" do - # # 1 - # end - # end - # end - # ``` - # - # NOTE: Post-conditions should not be checked in an `#after_each` or related block. - # Errors that occur in an `#after_each` block will halt testing and abort with an error. - # Use `#post_condition` instead for post-test checks. - # - # See also: `#before_all`, `#before_each`, `#after_all`, and `#around_each`. - macro after_each - # After each hook. - # Defined as a method so that it can access the same scope as the example code. - def %hook : Nil - {{yield}} - end - - ::Spectator::SpecBuilder.add_after_each_hook do - # Get the wrapper instance and cast to current group type. - example = ::Spectator::Internals::Harness.current.example - instance = example.instance.as({{@type.id}}) - instance.%hook - end - end - - # Creates a hook that will run for every example in the group. - # This can be used as an alternative to `#before_each` and `#after_each`. - # The block of code provided to this macro is used for the hook. - # The hook is executed once per example in the group (and sub-groups). - # If the hook raises an exception, - # the current example will be skipped and marked as an error. - # - # Sometimes the test code must run in a block: - # ``` - # around_each do |proc| - # Thing.run do - # proc.call - # end - # end - # - # it "does something" do - # # ... - # end - # ``` - # - # The block argument is provided a `Proc`. - # To run the example, that proc must be called. - # Make sure to call it! - # ``` - # around_each do |proc| - # Thing.run - # # Missing proc.call - # end - # - # it "does something" do - # # Whoops! This is never run. - # end - # ``` - # - # The hook can use values and methods in the group like examples can. - # It is called in the same scope as the example code. - # ``` - # let(array) { [1, 2, 3] } - # around_each do |proc| - # array << 4 - # proc.call - # array.pop - # end - # ``` - # - # If multiple `#around_each` blocks are specified, - # then they are run in the order they were defined. - # ``` - # around_each { |p| p.call } # 1 - # around_each { |p| p.call } # 2 - # ``` - # - # With nested groups, the outer blocks will run first. - # But the return from calling the proc will be in the oposite order. - # ``` - # describe Something do - # around_each do |proc| - # Thing.foo # 1 - # proc.call - # Thing.bar # 5 - # end - # - # describe "#foo" do - # around_each do |proc| - # Thing.foo # 2 - # proc.call - # Thing.bar # 4 - # end - # - # it "does a cool thing" do - # # 3 - # end - # end - # end - # ``` - # - # NOTE: Pre- and post-conditions should not be checked in an `#around_each` or similar block. - # Errors that occur in an `#around_each` block will halt testing and abort with an error. - # Use `#pre_condition` and `#post_condition` instead for pre- and post-test checks. - # - # See also: `#before_all`, `#before_each`, `#after_all`, and `#after_each`. - macro around_each(&block) - # Around each hook. - # Defined as a method so that it can access the same scope as the example code. - def %hook({{block.args.splat}}) : Nil - {{yield}} - end - - ::Spectator::SpecBuilder.add_around_each_hook do |proc| - # Get the wrapper instance and cast to current group type. - example = ::Spectator::Internals::Harness.current.example - instance = example.instance.as({{@type.id}}) - instance.%hook(proc) - end - end - - # Defines a block of code to run prior to every example in the group. - # The condition is executed once per example in the group (and sub-groups). - # If the condition fails, then the example fails. - # - # NOTE: Inside a `#sample` block, the condition is checked before every example of every iteration. - # - # This can be useful for ensuring the state before a test. - # ``` - # pre_condition { expect(array).to_not be_nil } - # - # it "is the correct length" do - # expect(array.size).to eq(3) - # end - # ``` - # - # The condition can use values and methods in the group like examples can. - # It is called in the same scope as the example code. - # ``` - # let(array) { [1, 2, 3] } - # pre_condition { expect(array.size).to eq(3) } - # ``` - # - # If multiple `#pre_condition` blocks are specified, - # then they are run in the order they were defined. - # ``` - # pre_condition { expect(array).to_not be_nil } # 1 - # pre_condition { expect(array.size).to eq(3) } # 2 - # ``` - # - # With nested groups, the outer blocks will run first. - # ``` - # describe Something do - # pre_condition { is_expected.to_not be_nil } # 1 - # - # describe "#foo" do - # pre_condition { expect(subject.foo).to_not be_nil } # 2 - # - # it "does a cool thing" do - # # 3 - # end - # end - # end - # ``` - # - # See also: `#post_condition`. - macro pre_condition - # Pre-condition check. - def %condition : Nil - {{yield}} - end - - ::Spectator::SpecBuilder.add_pre_condition do - example = ::Spectator::Internals::Harness.current.example - instance = example.instance.as({{@type.id}}) - instance.%condition - end - end - - # Defines a block of code to run after every example in the group. - # The condition is executed once per example in the group (and sub-groups). - # If the condition fails, then the example fails. - # - # NOTE: Inside a `#sample` block, the condition is checked before every example of every iteration. - # - # This can be useful for ensuring the state after a test. - # ``` - # # The variable x shouldn't be modified if an error is raised. - # post_condition { expect(x).to eq(original_x) } - # - # it "raises on divide by zero" do - # expect_raises { x /= 0 } - # end - # ``` - # - # The condition can use values and methods in the group like examples can. - # It is called in the same scope as the example code. - # ``` - # let(array) { [1, 2, 3] } - # post_condition { expect(array.size).to eq(3) } - # ``` - # - # If multiple `#post_condition` blocks are specified, - # then they are run in the order they were defined. - # ``` - # post_condition { expect(array).to_not be_nil } # 1 - # post_condition { expect(array.size).to eq(3) } # 2 - # ``` - # - # With nested groups, the inner blocks will run first. - # ``` - # describe Something do - # post_condition { is_expected.to_not be_nil } # 3 - # - # describe "#foo" do - # post_condition { expect(subject.foo).to_not be_nil } # 2 - # - # it "does a cool thing" do - # # 1 - # end - # end - # end - # ``` - # - # See also: `#pre_condition`. - macro post_condition - # Post-condition check. - def %condition : Nil - {{yield}} - end - - ::Spectator::SpecBuilder.add_post_condition do - example = ::Spectator::Internals::Harness.current.example - instance = example.instance.as({{@type.id}}) - instance.%condition - end - end - - # Creates an example, or a test case. - # The *what* argument describes "what" is being tested or asserted. - # The block contains the code to run the test. - # One or more expectations should be in the block. - # - # ``` - # it "can do math" do - # expect(1 + 2).to eq(3) - # end - # ``` - # - # See `ExampleDSL` and `MatcherDSL` for additional macros and methods - # that can be used in example code blocks. - # - # A short-hand, one-liner syntax can also be used. - # Typically, this is combined with `#subject`. - # For instance: - # ``` - # subject { 1 + 2 } - # it is_expected.to eq(3) - # ``` - macro it(what, _source_file = __FILE__, _source_line = __LINE__, &block) - # Create the wrapper class for the test code. - {% if block.is_a?(Nop) %} - {% if what.is_a?(Call) %} - def %run - {{what}} - end - {% else %} - {% raise "Unrecognized syntax: `it #{what}`" %} - {% end %} - {% else %} - def %run - {{block.body}} - end - {% end %} - - %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - ::Spectator::SpecBuilder.add_example({{what.stringify}}, %source, {{@type.name}}) { |test| test.as({{@type.name}}).%run } - end - - # Creates an example, or a test case. - # The block contains the code to run the test. - # One or more expectations should be in the block. - # - # ``` - # it { expect(1 + 2).to eq(3) } - # ``` - # - # See `ExampleDSL` and `MatcherDSL` for additional macros and methods - # that can be used in example code blocks. - # - # A short-hand, one-liner syntax can also be used. - # Typically, this is combined with `#subject`. - # For instance: - # ``` - # subject { 1 + 2 } - # it is_expected.to eq(3) - # ``` - macro it(&block) - it({{block.body.stringify}}) {{block}} - end - - # An alternative way to write an example. - # This is identical to `#it`. - macro specify(what, &block) - it({{what}}) {{block}} - end - - # An alternative way to write an example. - # This is identical to `#it`, - # except that it doesn't take a "what" argument. - macro specify(&block) - it({{block.body.stringify}}) {{block}} - end - - # Creates an example, or a test case, that does not run. - # This can be used to prototype functionality that isn't ready. - # The *what* argument describes "what" is being tested or asserted. - # The block contains the code to run the test. - # One or more expectations should be in the block. - # - # ``` - # pending "something that isn't implemented yet" do - # # ... - # end - # ``` - # - # See `ExampleDSL` and `MatcherDSL` for additional macros and methods - # that can be used in example code blocks. - # - # NOTE: Crystal appears to "lazily" compile code. - # Any code that isn't referenced seems to be ignored. - # Sometimes syntax, type, and other compile-time errors - # can occur in unreferenced code and won't be caught by the compiler. - # By creating a `#pending` test, the code will be referenced. - # Thus, forcing the compiler to at least process the code, even if it isn't run. - macro pending(what, _source_file = __FILE__, _source_line = __LINE__, &block) - # TODO - end - - # Creates an example, or a test case, that does not run. - # This can be used to prototype functionality that isn't ready. - # The *what* argument describes "what" is being tested or asserted. - # The block contains the code to run the test. - # One or more expectations should be in the block. - # - # ``` - # pending do - # # Something that isn't implemented yet. - # end - # ``` - macro pending(&block) - peding({{block.body.stringify}}) {{block}} - end - - # Same as `#pending`. - # Included for compatibility with RSpec. - macro skip(what, &block) - pending({{what}}) {{block}} - end - - # Same as `#pending`. - # Included for compatibility with RSpec. - macro skip(&block) - pending({{block.body.stringify}}) {{block}} - end - - # Same as `#pending`. - # Included for compatibility with RSpec. - macro xit(what, &block) - pending({{what}}) {{block}} - end - - # Same as `#pending`. - # Included for compatibility with RSpec. - macro xit(&block) - pending({{block.body.stringify}}) {{block}} - end - end -end From 7002564ebd53397d1438001cf1030fa62f481042 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 15 Sep 2019 09:45:48 -0600 Subject: [PATCH 027/205] Add let and subject with variants New behavior mimics RSpec - bang variant defines immediately instead of not storing. --- src/spectator/dsl/values.cr | 54 +++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/spectator/dsl/values.cr diff --git a/src/spectator/dsl/values.cr b/src/spectator/dsl/values.cr new file mode 100644 index 0000000..283c6a3 --- /dev/null +++ b/src/spectator/dsl/values.cr @@ -0,0 +1,54 @@ +module Spectator + module DSL + macro let(name, &block) + def %value + {{block.body}} + end + + @%wrapper : ::Spectator::Internals::ValueWrapper? + + def {{name.id}} + if (wrapper = @%wrapper) + wrapper.as(::Spectator::Internals::TypedValueWrapper(typeof(%value))).value + else + %value.tap do |value| + @%wrapper = ::Spectator::Internals::TypedValueWrapper.new(value) + end + end + end + end + + macro let!(name, &block) + # TODO: Doesn't work with late-defined values (let). + @%value = {{yield}} + + def {{name.id}} + @%value + end + end + + macro subject(&block) + let(:subject) {{block}} + end + + macro subject(name, &block) + let({{name.id}}) {{block}} + + def subject + {{name.id}} + end + end + + macro subject!(&block) + let!(:subject) {{block}} + end + + macro subject!(name, &block) + let!({{name.id}}) {{block}} + + def subject + {{name.id}} + end + end + end +end From 52ef5e5ced3cfbfe51335de58f12059929e0439f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 15 Sep 2019 10:40:53 -0600 Subject: [PATCH 028/205] Some DSL methods for hooks --- src/spectator/dsl/hooks.cr | 35 +++++++++++++++++++ src/spectator/spec_builder.cr | 4 +-- .../spec_builder/example_group_builder.cr | 25 +++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 src/spectator/dsl/hooks.cr diff --git a/src/spectator/dsl/hooks.cr b/src/spectator/dsl/hooks.cr new file mode 100644 index 0000000..bc110d4 --- /dev/null +++ b/src/spectator/dsl/hooks.cr @@ -0,0 +1,35 @@ +module Spectator + module DSL + macro before_each(&block) + def %hook : Nil + {{block.body}} + end + + ::Spectator::SpecBuilder.add_before_each_hook { |test| test.as({{@type.id}}).%hook } + end + + macro after_each(&block) + def %hook : Nil + {{block.body}} + end + + ::Spectator::SpecBuilder.add_after_each_hook { |test| test.as({{@type.id}}).%hook } + 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.splat}}) : Nil + {{block.body}} + end + + # TODO: Handle wrapping proc and test together. + end + end +end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 88f1673..9978535 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -56,7 +56,7 @@ module Spectator end # Adds a block of code to run before each example in the current group. - def add_before_each_hook(&block : ->) : Nil + def add_before_each_hook(&block : TestMethod) : Nil @@stack.current.add_before_each_hook(block) end @@ -66,7 +66,7 @@ module Spectator end # Adds a block of code to run after each example in the current group. - def add_after_each_hook(&block : ->) : Nil + def add_after_each_hook(&block : TestMethod) : Nil @@stack.current.add_after_each_hook(block) end diff --git a/src/spectator/spec_builder/example_group_builder.cr b/src/spectator/spec_builder/example_group_builder.cr index 69d91a8..6f317cd 100644 --- a/src/spectator/spec_builder/example_group_builder.cr +++ b/src/spectator/spec_builder/example_group_builder.cr @@ -6,8 +6,33 @@ module Spectator::SpecBuilder private getter children = [] of Child + @before_each_hooks = Deque(TestMethod).new + @after_each_hooks = Deque(TestMethod).new + @before_all_hooks = Deque(->).new + @after_all_hooks = Deque(->).new + def add_child(child : Child) @children << child end + + def add_before_each_hook(hook : TestMethod) + @before_each_hooks << hook + end + + def add_after_each_hook(hook : TestMethod) + @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 + + private def build_hooks + ExampleHooks.empty + end end end From 9129aa428675d5f9f65d66cd8c0e91a0584424d8 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 17 Sep 2019 20:35:19 -0600 Subject: [PATCH 029/205] Use Deque instead of Array --- src/spectator/spec_builder/example_group_builder.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/spec_builder/example_group_builder.cr b/src/spectator/spec_builder/example_group_builder.cr index 6f317cd..38fdbf2 100644 --- a/src/spectator/spec_builder/example_group_builder.cr +++ b/src/spectator/spec_builder/example_group_builder.cr @@ -4,7 +4,7 @@ module Spectator::SpecBuilder abstract class ExampleGroupBuilder alias Child = NestedExampleGroupBuilder | ExampleBuilder - private getter children = [] of Child + private getter children = Deque(Child).new @before_each_hooks = Deque(TestMethod).new @after_each_hooks = Deque(TestMethod).new From da8736f89182fafecda9933465606a1773b4a0a3 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 17 Sep 2019 20:37:06 -0600 Subject: [PATCH 030/205] Don't create test as part of run method --- src/spectator/runnable_example.cr | 2 +- src/spectator/spec_builder.cr | 3 +-- src/spectator/spec_builder/example_builder.cr | 11 ++++++-- src/spectator/test_wrapper.cr | 26 ++++++++----------- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/spectator/runnable_example.cr b/src/spectator/runnable_example.cr index 9dca2d1..d844e4d 100644 --- a/src/spectator/runnable_example.cr +++ b/src/spectator/runnable_example.cr @@ -25,7 +25,7 @@ module Spectator # Capture how long it takes to run the test code. result.elapsed = Time.measure do begin - test_wrapper.run {} # Actually run the example code. + test_wrapper.run # Actually run the example code. rescue ex # Catch all errors and handle them later. result.error = ex end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 9978535..38110eb 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -45,8 +45,7 @@ module Spectator def add_example(description : String, source : Source, example_type : ::SpectatorTest.class, &runner : ::SpectatorTest ->) : Nil builder = ->{ example_type.new.as(::SpectatorTest) } - wrapper = TestWrapper.new(description, source, builder, runner) - factory = ExampleBuilder.new(wrapper) + factory = ExampleBuilder.new(description, source, builder, runner) @@stack.current.add_child(factory) end diff --git a/src/spectator/spec_builder/example_builder.cr b/src/spectator/spec_builder/example_builder.cr index 5961467..a3f0621 100644 --- a/src/spectator/spec_builder/example_builder.cr +++ b/src/spectator/spec_builder/example_builder.cr @@ -1,10 +1,17 @@ +require "../../spectator_test" +require "../test_wrapper" + module Spectator::SpecBuilder class ExampleBuilder - def initialize(@wrapper : TestWrapper) + alias FactoryMethod = -> ::SpectatorTest + + def initialize(@description : String, @source : Source, @builder : FactoryMethod, @runner : TestMethod) end def build(group) - RunnableExample.new(group, @wrapper).as(ExampleComponent) + test = @builder.call + wrapper = TestWrapper.new(@description, @source, test, @runner) + RunnableExample.new(group, wrapper).as(ExampleComponent) end end end diff --git a/src/spectator/test_wrapper.cr b/src/spectator/test_wrapper.cr index 77c097b..b087623 100644 --- a/src/spectator/test_wrapper.cr +++ b/src/spectator/test_wrapper.cr @@ -7,26 +7,22 @@ module Spectator # Stores information about a end-user test. # Used to instantiate tests and run them. struct TestWrapper - # Location of the test in source code. - getter source : Source - # Description the user provided for the test. - getter description : String + getter description + + # Location of the test in source code. + getter source # Creates a wrapper for the test. - # The *builder* creates an instance of the test. - # The *runner* takes the test created by *builder* and runs it. - def initialize(@description, @source, @builder : -> ::SpectatorTest, @runner : TestMethod) + def initialize(@description : String, @source : Source, @test : ::SpectatorTest, @runner : TestMethod) end - # Instantiates and runs the test. - # This method yields twice - before and after the test. - # The test instance is yielded. - def run : Nil - test = @builder.call - yield test - @runner.call(test) - yield test + def run + call(@runner) + end + + def call(method : TestMethod) : Nil + method.call(@test) end end end From 6e1605f246561391f22d5a58fbe5f5cc441cf3cc Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 17 Sep 2019 20:57:59 -0600 Subject: [PATCH 031/205] Add TestContext --- src/spectator/example_group.cr | 5 +++++ src/spectator/nested_example_group.cr | 3 ++- .../spec_builder/example_group_builder.cr | 5 +++++ .../nested_example_group_builder.cr | 2 +- .../root_example_group_builder.cr | 2 +- src/spectator/test_context.cr | 22 +++++++++++++++++++ 6 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 src/spectator/test_context.cr diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index b703cd7..17ebd82 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -33,6 +33,11 @@ module Spectator @example_count = children.sum(&.example_count) end + getter context + + def initialize(@context : TestContext) + end + # Yields each direct descendant. def each children.each do |child| diff --git a/src/spectator/nested_example_group.cr b/src/spectator/nested_example_group.cr index 00f16e9..0bb55b7 100644 --- a/src/spectator/nested_example_group.cr +++ b/src/spectator/nested_example_group.cr @@ -18,7 +18,8 @@ module Spectator # The parent's children must contain this group, # otherwise there may be unexpected behavior. # The *hooks* are stored to be triggered later. - def initialize(@what, @parent) + def initialize(@what, @parent, context) + super(context) end # Indicates wheter the group references a type. diff --git a/src/spectator/spec_builder/example_group_builder.cr b/src/spectator/spec_builder/example_group_builder.cr index 38fdbf2..1e7113a 100644 --- a/src/spectator/spec_builder/example_group_builder.cr +++ b/src/spectator/spec_builder/example_group_builder.cr @@ -1,3 +1,4 @@ +require "../test_context" require "./example_builder" module Spectator::SpecBuilder @@ -31,6 +32,10 @@ module Spectator::SpecBuilder @after_all_hooks << hook end + private def context + TestContext.new(build_hooks) + end + private def build_hooks ExampleHooks.empty end diff --git a/src/spectator/spec_builder/nested_example_group_builder.cr b/src/spectator/spec_builder/nested_example_group_builder.cr index 1693c52..2d8afa8 100644 --- a/src/spectator/spec_builder/nested_example_group_builder.cr +++ b/src/spectator/spec_builder/nested_example_group_builder.cr @@ -6,7 +6,7 @@ module Spectator::SpecBuilder end def build(group) - NestedExampleGroup.new(@what, group).tap do |group| + NestedExampleGroup.new(@what, group, context).tap do |group| group.children = children.map do |child| child.build(group).as(ExampleComponent) end diff --git a/src/spectator/spec_builder/root_example_group_builder.cr b/src/spectator/spec_builder/root_example_group_builder.cr index 9b1d3df..ca59f02 100644 --- a/src/spectator/spec_builder/root_example_group_builder.cr +++ b/src/spectator/spec_builder/root_example_group_builder.cr @@ -3,7 +3,7 @@ require "./example_group_builder" module Spectator::SpecBuilder class RootExampleGroupBuilder < ExampleGroupBuilder def build - RootExampleGroup.new.tap do |group| + RootExampleGroup.new(context).tap do |group| group.children = children.map do |child| child.build(group).as(ExampleComponent) end diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr new file mode 100644 index 0000000..f23456a --- /dev/null +++ b/src/spectator/test_context.cr @@ -0,0 +1,22 @@ +module Spectator + struct TestContext + def initialize(@hooks : ExampleHooks) + @before_all_hooks_run = false + @after_all_hooks_run = false + end + + def run_before_hooks(wrapper : TestWrapper) + @hooks.run_before_all + @hooks.run_before_each(wrapper) + ensure + @before_all_hooks_run = true + end + + def run_after_hooks(wrapper : TestWrapper) + @hooks.run_after_each(wrapper) + @hooks.run_after_all + ensure + @after_all_hooks_run = true + end + end +end From 957b8a54dbab5784bd201748db85102f670e2f80 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 17 Sep 2019 20:58:26 -0600 Subject: [PATCH 032/205] Implement before and after hooks --- src/spectator/example_hooks.cr | 20 +++++++++++-------- src/spectator/runnable_example.cr | 3 +++ .../spec_builder/example_group_builder.cr | 8 +++++++- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/spectator/example_hooks.cr b/src/spectator/example_hooks.cr index db6eef9..28d8a24 100644 --- a/src/spectator/example_hooks.cr +++ b/src/spectator/example_hooks.cr @@ -7,9 +7,9 @@ module Spectator def self.empty new( [] of ->, + [] of TestMethod, [] of ->, - [] of ->, - [] of ->, + [] of TestMethod, [] of Proc(Nil) -> ) end @@ -17,9 +17,9 @@ module Spectator # Creates a new set of hooks. def initialize( @before_all : Array(->), - @before_each : Array(->), + @before_each : Array(TestMethod), @after_all : Array(->), - @after_each : Array(->), + @after_each : Array(TestMethod), @around_each : Array(Proc(Nil) ->) ) end @@ -32,8 +32,10 @@ module Spectator # Runs all "before-each" hooks. # These hooks should be run every time before each example in a group. - def run_before_each - @before_each.each &.call + def run_before_each(wrapper : TestWrapper) + @before_each.each do |hook| + wrapper.call(hook) + end end # Runs all "after-all" hooks. @@ -44,8 +46,10 @@ module Spectator # Runs all "after-all" hooks. # These hooks should be run every time after each example in a group. - def run_after_each - @after_each.each &.call + def run_after_each(wrapper : TestWrapper) + @after_each.each do |hook| + wrapper.call(hook) + end end # Creates a proc that runs the "around-each" hooks diff --git a/src/spectator/runnable_example.cr b/src/spectator/runnable_example.cr index d844e4d..2188d8a 100644 --- a/src/spectator/runnable_example.cr +++ b/src/spectator/runnable_example.cr @@ -15,8 +15,11 @@ module Spectator # 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(test_wrapper) run_example(result) + context.run_after_hooks(test_wrapper) end end diff --git a/src/spectator/spec_builder/example_group_builder.cr b/src/spectator/spec_builder/example_group_builder.cr index 1e7113a..45a817e 100644 --- a/src/spectator/spec_builder/example_group_builder.cr +++ b/src/spectator/spec_builder/example_group_builder.cr @@ -37,7 +37,13 @@ module Spectator::SpecBuilder end private def build_hooks - ExampleHooks.empty + ExampleHooks.new( + @before_all_hooks.to_a, + @before_each_hooks.to_a, + @after_all_hooks.to_a, + @after_each_hooks.to_a, + [] of Proc(Nil) -> + ) end end end From db3f89731c346a5ef2019f03e3a70a464d47cefa Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 18 Sep 2019 09:51:45 -0600 Subject: [PATCH 033/205] Call parent hooks --- .../spec_builder/example_group_builder.cr | 4 --- .../nested_example_group_builder.cr | 2 ++ .../root_example_group_builder.cr | 1 + src/spectator/test_context.cr | 32 ++++++++++++++++--- 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/spectator/spec_builder/example_group_builder.cr b/src/spectator/spec_builder/example_group_builder.cr index 45a817e..d02c33c 100644 --- a/src/spectator/spec_builder/example_group_builder.cr +++ b/src/spectator/spec_builder/example_group_builder.cr @@ -32,10 +32,6 @@ module Spectator::SpecBuilder @after_all_hooks << hook end - private def context - TestContext.new(build_hooks) - end - private def build_hooks ExampleHooks.new( @before_all_hooks.to_a, diff --git a/src/spectator/spec_builder/nested_example_group_builder.cr b/src/spectator/spec_builder/nested_example_group_builder.cr index 2d8afa8..413e032 100644 --- a/src/spectator/spec_builder/nested_example_group_builder.cr +++ b/src/spectator/spec_builder/nested_example_group_builder.cr @@ -1,3 +1,4 @@ +require "../test_context" require "./example_group_builder" module Spectator::SpecBuilder @@ -6,6 +7,7 @@ module Spectator::SpecBuilder end def build(group) + context = TestContext.new(group.context, build_hooks) NestedExampleGroup.new(@what, group, context).tap do |group| group.children = children.map do |child| child.build(group).as(ExampleComponent) diff --git a/src/spectator/spec_builder/root_example_group_builder.cr b/src/spectator/spec_builder/root_example_group_builder.cr index ca59f02..0ad48ab 100644 --- a/src/spectator/spec_builder/root_example_group_builder.cr +++ b/src/spectator/spec_builder/root_example_group_builder.cr @@ -3,6 +3,7 @@ require "./example_group_builder" module Spectator::SpecBuilder class RootExampleGroupBuilder < ExampleGroupBuilder def build + context = TestContext.new(nil, build_hooks) RootExampleGroup.new(context).tap do |group| group.children = children.map do |child| child.build(group).as(ExampleComponent) diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr index f23456a..fea0ab4 100644 --- a/src/spectator/test_context.cr +++ b/src/spectator/test_context.cr @@ -1,22 +1,46 @@ module Spectator - struct TestContext - def initialize(@hooks : ExampleHooks) + class TestContext + def initialize(@parent : TestContext?, @hooks : ExampleHooks) @before_all_hooks_run = false @after_all_hooks_run = false end def run_before_hooks(wrapper : TestWrapper) + run_before_all_hooks + run_before_each_hooks(wrapper) + end + + protected def run_before_all_hooks + return if @before_all_hooks_run + + @parent.try &.run_before_all_hooks @hooks.run_before_all - @hooks.run_before_each(wrapper) ensure @before_all_hooks_run = true end + protected def run_before_each_hooks(wrapper : TestWrapper) + @parent.try &.run_before_each_hooks(wrapper) + @hooks.run_before_each(wrapper) + end + def run_after_hooks(wrapper : TestWrapper) - @hooks.run_after_each(wrapper) + run_after_each_hooks(wrapper) + run_after_all_hooks + end + + protected def run_after_all_hooks + return if @after_all_hooks_run + @hooks.run_after_all + @parent.try &.run_after_all_hooks ensure @after_all_hooks_run = true end + + protected def run_after_each_hooks(wrapper : TestWrapper) + @hooks.run_after_each(wrapper) + @parent.try &.run_after_each_hooks(wrapper) + end end end From b17d19689ef8c42c95b31c116c1a89b7f3f907cf Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 18 Sep 2019 09:52:49 -0600 Subject: [PATCH 034/205] Formatting --- src/spectator/runnable_example.cr | 2 +- src/spectator/spec_builder.cr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/runnable_example.cr b/src/spectator/runnable_example.cr index 2188d8a..2ee66f0 100644 --- a/src/spectator/runnable_example.cr +++ b/src/spectator/runnable_example.cr @@ -29,7 +29,7 @@ module Spectator result.elapsed = Time.measure do begin test_wrapper.run # Actually run the example code. - rescue ex # Catch all errors and handle them later. + rescue ex # Catch all errors and handle them later. result.error = ex end end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 38110eb..25404a0 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -43,7 +43,7 @@ module Spectator # 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 + example_type : ::SpectatorTest.class, &runner : ::SpectatorTest ->) : Nil builder = ->{ example_type.new.as(::SpectatorTest) } factory = ExampleBuilder.new(description, source, builder, runner) @@stack.current.add_child(factory) From 24eb5e419acf178167a8fe07128a8638f5156eed Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 18 Sep 2019 21:51:35 -0600 Subject: [PATCH 035/205] Get around_each hooks working again --- src/spectator/dsl/hooks.cr | 2 +- src/spectator/example_hooks.cr | 13 ++++++------- src/spectator/runnable_example.cr | 6 ++++-- src/spectator/spec_builder.cr | 6 +++--- src/spectator/spec_builder/example_group_builder.cr | 7 ++++++- src/spectator/test_context.cr | 9 +++++++++ src/spectator/test_wrapper.cr | 4 ++++ 7 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/spectator/dsl/hooks.cr b/src/spectator/dsl/hooks.cr index bc110d4..85dd6be 100644 --- a/src/spectator/dsl/hooks.cr +++ b/src/spectator/dsl/hooks.cr @@ -29,7 +29,7 @@ module Spectator {{block.body}} end - # TODO: Handle wrapping proc and test together. + ::Spectator::SpecBuilder.add_around_each_hook { |test, proc| test.as({{@type.id}}).%hook(proc) } end end end diff --git a/src/spectator/example_hooks.cr b/src/spectator/example_hooks.cr index 28d8a24..dd335da 100644 --- a/src/spectator/example_hooks.cr +++ b/src/spectator/example_hooks.cr @@ -10,7 +10,7 @@ module Spectator [] of TestMethod, [] of ->, [] of TestMethod, - [] of Proc(Nil) -> + [] of ::SpectatorTest, Proc(Nil) -> ) end @@ -20,7 +20,7 @@ module Spectator @before_each : Array(TestMethod), @after_all : Array(->), @after_each : Array(TestMethod), - @around_each : Array(Proc(Nil) ->) + @around_each : Array(::SpectatorTest, Proc(Nil) ->) ) end @@ -56,19 +56,18 @@ module Spectator # 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(&block : ->) : -> + 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_proc(hook, wrapper) + wrapper = wrap_foo(test, hook, wrapper) end wrapper end - # Utility method for wrapping one proc with another. - private def wrap_proc(inner : Proc(Nil) ->, wrapper : ->) - ->{ inner.call(wrapper) } + private def wrap_foo(test, hook, wrapper) + ->{ hook.call(test, wrapper) } end end end diff --git a/src/spectator/runnable_example.cr b/src/spectator/runnable_example.cr index 2ee66f0..630c2ac 100644 --- a/src/spectator/runnable_example.cr +++ b/src/spectator/runnable_example.cr @@ -25,11 +25,13 @@ module Spectator # Runs the test code and captures the result. private def run_example(result) + wrapper = test_wrapper.around_hook(group.context) + # Capture how long it takes to run the test code. result.elapsed = Time.measure do begin - test_wrapper.run # Actually run the example code. - rescue ex # Catch all errors and handle them later. + wrapper.call + rescue ex # Catch all errors and handle them later. result.error = ex end end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 25404a0..880105b 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -70,9 +70,9 @@ module Spectator 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 proc as an argument. - # It is expected that the block will call the proc. - def add_around_each_hook(&block : Proc(Nil) ->) : Nil + # 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 diff --git a/src/spectator/spec_builder/example_group_builder.cr b/src/spectator/spec_builder/example_group_builder.cr index d02c33c..f7d6255 100644 --- a/src/spectator/spec_builder/example_group_builder.cr +++ b/src/spectator/spec_builder/example_group_builder.cr @@ -11,6 +11,7 @@ module Spectator::SpecBuilder @after_each_hooks = Deque(TestMethod).new @before_all_hooks = Deque(->).new @after_all_hooks = Deque(->).new + @around_each_hooks = Deque(::SpectatorTest, Proc(Nil) ->).new def add_child(child : Child) @children << child @@ -32,13 +33,17 @@ module Spectator::SpecBuilder @after_all_hooks << hook end + def add_around_each_hook(hook : ::SpectatorTest, Proc(Nil) ->) + @around_each_hooks << hook + 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, - [] of Proc(Nil) -> + @around_each_hooks.to_a ) end end diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr index fea0ab4..d02f906 100644 --- a/src/spectator/test_context.cr +++ b/src/spectator/test_context.cr @@ -42,5 +42,14 @@ module Spectator @hooks.run_after_each(wrapper) @parent.try &.run_after_each_hooks(wrapper) 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 end end diff --git a/src/spectator/test_wrapper.cr b/src/spectator/test_wrapper.cr index b087623..99c1512 100644 --- a/src/spectator/test_wrapper.cr +++ b/src/spectator/test_wrapper.cr @@ -24,5 +24,9 @@ module Spectator def call(method : TestMethod) : Nil method.call(@test) end + + def around_hook(context : TestContext) + context.wrap_around_each_hooks(@test) { run } + end end end From 5f3c9d2f6305cf0d9252d7972be795e90b38c736 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 22 Sep 2019 11:02:53 -0600 Subject: [PATCH 036/205] Distinguish group variables --- src/spectator/spec_builder/nested_example_group_builder.cr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/spectator/spec_builder/nested_example_group_builder.cr b/src/spectator/spec_builder/nested_example_group_builder.cr index 413e032..002629b 100644 --- a/src/spectator/spec_builder/nested_example_group_builder.cr +++ b/src/spectator/spec_builder/nested_example_group_builder.cr @@ -6,9 +6,9 @@ module Spectator::SpecBuilder def initialize(@what : String | Symbol) end - def build(group) - context = TestContext.new(group.context, build_hooks) - NestedExampleGroup.new(@what, group, context).tap do |group| + def build(parent_group) + context = TestContext.new(parent_group.context, build_hooks) + NestedExampleGroup.new(@what, parent_group, context).tap do |group| group.children = children.map do |child| child.build(group).as(ExampleComponent) end From 24ccb7759552923a2d1aade2a6a5112a71e1f578 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 22 Sep 2019 11:27:18 -0600 Subject: [PATCH 037/205] Expose running example to before-each and after-each hooks --- src/spectator/dsl/hooks.cr | 22 +++++++++++++++---- src/spectator/example.cr | 2 +- src/spectator/example_hooks.cr | 18 ++++++++------- src/spectator/runnable_example.cr | 4 ++-- src/spectator/spec_builder.cr | 4 ++-- .../spec_builder/example_group_builder.cr | 8 +++---- src/spectator/test_context.cr | 20 ++++++++--------- src/spectator/test_wrapper.cr | 4 ++++ 8 files changed, 51 insertions(+), 31 deletions(-) diff --git a/src/spectator/dsl/hooks.cr b/src/spectator/dsl/hooks.cr index 85dd6be..00bdd14 100644 --- a/src/spectator/dsl/hooks.cr +++ b/src/spectator/dsl/hooks.cr @@ -1,19 +1,33 @@ module Spectator module DSL macro before_each(&block) - def %hook : Nil + def %hook({{block.args.splat}}) : Nil {{block.body}} end - ::Spectator::SpecBuilder.add_before_each_hook { |test| test.as({{@type.id}}).%hook } + ::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 : Nil + def %hook({{block.args.splat}}) : Nil {{block.body}} end - ::Spectator::SpecBuilder.add_after_each_hook { |test| test.as({{@type.id}}).%hook } + ::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) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 56af0c7..e9c149e 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -12,7 +12,7 @@ module Spectator getter group : ExampleGroup # Retrieves the internal wrapped instance. - private getter test_wrapper : TestWrapper + protected getter test_wrapper : TestWrapper # Source where the example originated from. def source diff --git a/src/spectator/example_hooks.cr b/src/spectator/example_hooks.cr index dd335da..d345bf5 100644 --- a/src/spectator/example_hooks.cr +++ b/src/spectator/example_hooks.cr @@ -1,4 +1,6 @@ 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 @@ -7,9 +9,9 @@ module Spectator def self.empty new( [] of ->, - [] of TestMethod, + [] of TestMetaMethod, [] of ->, - [] of TestMethod, + [] of TestMetaMethod, [] of ::SpectatorTest, Proc(Nil) -> ) end @@ -17,9 +19,9 @@ module Spectator # Creates a new set of hooks. def initialize( @before_all : Array(->), - @before_each : Array(TestMethod), + @before_each : Array(TestMetaMethod), @after_all : Array(->), - @after_each : Array(TestMethod), + @after_each : Array(TestMetaMethod), @around_each : Array(::SpectatorTest, Proc(Nil) ->) ) end @@ -32,9 +34,9 @@ module Spectator # Runs all "before-each" hooks. # These hooks should be run every time before each example in a group. - def run_before_each(wrapper : TestWrapper) + def run_before_each(wrapper : TestWrapper, example : Example) @before_each.each do |hook| - wrapper.call(hook) + wrapper.call(hook, example) end end @@ -46,9 +48,9 @@ module Spectator # Runs all "after-all" hooks. # These hooks should be run every time after each example in a group. - def run_after_each(wrapper : TestWrapper) + def run_after_each(wrapper : TestWrapper, example : Example) @after_each.each do |hook| - wrapper.call(hook) + wrapper.call(hook, example) end end diff --git a/src/spectator/runnable_example.cr b/src/spectator/runnable_example.cr index 630c2ac..70496a1 100644 --- a/src/spectator/runnable_example.cr +++ b/src/spectator/runnable_example.cr @@ -17,9 +17,9 @@ module Spectator private def capture_result context = group.context ResultCapture.new.tap do |result| - context.run_before_hooks(test_wrapper) + context.run_before_hooks(self) run_example(result) - context.run_after_hooks(test_wrapper) + context.run_after_hooks(self) end end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 880105b..676d02c 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -55,7 +55,7 @@ module Spectator end # Adds a block of code to run before each example in the current group. - def add_before_each_hook(&block : TestMethod) : Nil + def add_before_each_hook(&block : TestMetaMethod) : Nil @@stack.current.add_before_each_hook(block) end @@ -65,7 +65,7 @@ module Spectator end # Adds a block of code to run after each example in the current group. - def add_after_each_hook(&block : TestMethod) : Nil + def add_after_each_hook(&block : TestMetaMethod) : Nil @@stack.current.add_after_each_hook(block) end diff --git a/src/spectator/spec_builder/example_group_builder.cr b/src/spectator/spec_builder/example_group_builder.cr index f7d6255..344ea04 100644 --- a/src/spectator/spec_builder/example_group_builder.cr +++ b/src/spectator/spec_builder/example_group_builder.cr @@ -7,8 +7,8 @@ module Spectator::SpecBuilder private getter children = Deque(Child).new - @before_each_hooks = Deque(TestMethod).new - @after_each_hooks = Deque(TestMethod).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 @@ -17,11 +17,11 @@ module Spectator::SpecBuilder @children << child end - def add_before_each_hook(hook : TestMethod) + def add_before_each_hook(hook : TestMetaMethod) @before_each_hooks << hook end - def add_after_each_hook(hook : TestMethod) + def add_after_each_hook(hook : TestMetaMethod) @after_each_hooks << hook end diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr index d02f906..5681db5 100644 --- a/src/spectator/test_context.cr +++ b/src/spectator/test_context.cr @@ -5,9 +5,9 @@ module Spectator @after_all_hooks_run = false end - def run_before_hooks(wrapper : TestWrapper) + def run_before_hooks(example : Example) run_before_all_hooks - run_before_each_hooks(wrapper) + run_before_each_hooks(example) end protected def run_before_all_hooks @@ -19,13 +19,13 @@ module Spectator @before_all_hooks_run = true end - protected def run_before_each_hooks(wrapper : TestWrapper) - @parent.try &.run_before_each_hooks(wrapper) - @hooks.run_before_each(wrapper) + 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(wrapper : TestWrapper) - run_after_each_hooks(wrapper) + def run_after_hooks(example : Example) + run_after_each_hooks(example) run_after_all_hooks end @@ -38,9 +38,9 @@ module Spectator @after_all_hooks_run = true end - protected def run_after_each_hooks(wrapper : TestWrapper) - @hooks.run_after_each(wrapper) - @parent.try &.run_after_each_hooks(wrapper) + 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 : ->) diff --git a/src/spectator/test_wrapper.cr b/src/spectator/test_wrapper.cr index 99c1512..756f287 100644 --- a/src/spectator/test_wrapper.cr +++ b/src/spectator/test_wrapper.cr @@ -25,6 +25,10 @@ module Spectator 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 From a355b59f3ecacce1af4b6235c7ba62b12630d4d2 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 25 Sep 2019 09:12:03 -0600 Subject: [PATCH 038/205] Type annotations for return types --- 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 374cdfb..96d65f7 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -19,15 +19,15 @@ module Spectator protected getter test_wrapper : TestWrapper # Source where the example originated from. - def source + def source : Source @test_wrapper.source end - def what + def what : String | Symbol @test_wrapper.description end - def symbolic? + def symbolic? : Bool description = @test_wrapper.description description.starts_with?('#') || description.starts_with?('.') end From 73cef75fcb516d1c3c03ea69e5bab5df32e070f5 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 09:56:54 -0600 Subject: [PATCH 039/205] Remove dummy example --- src/spectator/dummy_example.cr | 37 ---------------------------------- src/spectator/includes.cr | 1 - 2 files changed, 38 deletions(-) delete mode 100644 src/spectator/dummy_example.cr diff --git a/src/spectator/dummy_example.cr b/src/spectator/dummy_example.cr deleted file mode 100644 index 62e11c9..0000000 --- a/src/spectator/dummy_example.cr +++ /dev/null @@ -1,37 +0,0 @@ -require "./runnable_example" - -module Spectator - # Example that does nothing. - # This is to workaround a Crystal compiler bug. - # See: [Issue 4225](https://github.com/crystal-lang/crystal/issues/4225) - # If there are no concrete implementations of an abstract class, - # the compiler gives an error. - # The error indicates an abstract method is undefined. - # This class shouldn't be used, it's just to trick the compiler. - private class DummyExample < RunnableExample - # Dummy description. - def what : Symbol | String - "DUMMY" - end - - # Dummy symbolic flag. - def symbolic? : Bool - false - end - - # Dummy source. - def source : Source - Source.new(__FILE__, __LINE__) - end - - # Dummy instance. - def instance - nil - end - - # Dummy run that does nothing. - def run_instance - raise "You shouldn't be running this." - end - end -end diff --git a/src/spectator/includes.cr b/src/spectator/includes.cr index db239f1..8974cf7 100644 --- a/src/spectator/includes.cr +++ b/src/spectator/includes.cr @@ -23,7 +23,6 @@ require "./example_component" require "./example" require "./runnable_example" require "./pending_example" -require "./dummy_example" require "./example_conditions" require "./example_hooks" From 422c805e219b9de6d003576a9d2158d61926615f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 10:26:37 -0600 Subject: [PATCH 040/205] Rename SampleValues to TestValues Move out of internal namespace. --- .../{internals/sample_values.cr => test_values.cr} | 9 ++++----- src/spectator/{internals => }/typed_value_wrapper.cr | 2 +- src/spectator/{internals => }/value_wrapper.cr | 4 +--- 3 files changed, 6 insertions(+), 9 deletions(-) rename src/spectator/{internals/sample_values.cr => test_values.cr} (89%) rename src/spectator/{internals => }/typed_value_wrapper.cr (94%) rename src/spectator/{internals => }/value_wrapper.cr (69%) diff --git a/src/spectator/internals/sample_values.cr b/src/spectator/test_values.cr similarity index 89% rename from src/spectator/internals/sample_values.cr rename to src/spectator/test_values.cr index 8cab714..74fc923 100644 --- a/src/spectator/internals/sample_values.cr +++ b/src/spectator/test_values.cr @@ -1,10 +1,10 @@ require "./value_wrapper" -module Spectator::Internals +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 SampleValues + struct TestValues # Creates an empty set of sample values. def self.empty new({} of Symbol => Entry) @@ -17,9 +17,9 @@ module Spectator::Internals # 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 : T) : SampleValues forall T + def add(id : Symbol, name : String, value : T) : TestValues forall T wrapper = TypedValueWrapper(T).new(value) - SampleValues.new(@values.merge({ + TestValues.new(@values.merge({ id => Entry.new(name, wrapper), })) end @@ -58,7 +58,6 @@ module Spectator::Internals end # This must be after `Entry` is defined. - # Could be a Cyrstal compiler bug? include Enumerable(Entry) end end diff --git a/src/spectator/internals/typed_value_wrapper.cr b/src/spectator/typed_value_wrapper.cr similarity index 94% rename from src/spectator/internals/typed_value_wrapper.cr rename to src/spectator/typed_value_wrapper.cr index 39e06cf..8131d82 100644 --- a/src/spectator/internals/typed_value_wrapper.cr +++ b/src/spectator/typed_value_wrapper.cr @@ -1,6 +1,6 @@ require "./value_wrapper" -module Spectator::Internals +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` diff --git a/src/spectator/internals/value_wrapper.cr b/src/spectator/value_wrapper.cr similarity index 69% rename from src/spectator/internals/value_wrapper.cr rename to src/spectator/value_wrapper.cr index 4c0183e..170951d 100644 --- a/src/spectator/internals/value_wrapper.cr +++ b/src/spectator/value_wrapper.cr @@ -1,9 +1,7 @@ -module Spectator::Internals +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 - # Retrieves the underlying value. - abstract def value end end From 5d9e7002d60c1333f36de4947e029f66b58d4d73 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 12:31:23 -0600 Subject: [PATCH 041/205] Some initial work on sample groups --- src/spectator/dsl/groups.cr | 21 +++++++++++++++ .../sample_example_group_builder.cr | 26 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 src/spectator/spec_builder/sample_example_group_builder.cr diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 60571bf..dbf6d60 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -25,5 +25,26 @@ module Spectator macro describe(what, &block) context({{what}}) {{block}} end + + macro sample(what, &block) + {% block_arg = block.args.empty? ? :value.id : block.args.first.id %} + class Sample%sample < {{@type.id}} + def %collection + {{what}} + end + end + + class Context%sample < {{@type.id}} + ::Spectator::SpecBuilder.start_sample_group({{what.stringify}}) + + def {{block_arg}} + 1 + end + + {{block.body}} + + ::Spectator::SpecBuilder.end_group + end + 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 new file mode 100644 index 0000000..17a100e --- /dev/null +++ b/src/spectator/spec_builder/sample_example_group_builder.cr @@ -0,0 +1,26 @@ +require "./nested_example_group_builder" + +module Spectator::SpecBuilder + class SampleExampleGroupBuilder < NestedExampleGroupBuilder + def initialize(@what : String) + end + + def build(parent_group) + context = TestContext.new(parent_group.context, build_hooks) + NestedExampleGroup.new(@what, parent_group, context).tap do |group| + group.children = [:TODO].map do |element| + build_sub_group(group, element).as(ExampleComponent) + end + end + end + + private def build_sub_group(parent_group, element) + context = TestContext.new(parent_group.context, ExampleHooks.empty) + NestedExampleGroup.new("TODO", parent_group, context).tap do |group| + group.children = children.map do |child| + child.build(group).as(ExampleComponent) + end + end + end + end +end From 3c9846ae9b0c506573a5dc50955b4a827b6020cf Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 12:59:42 -0600 Subject: [PATCH 042/205] Remove reference to internals --- src/spectator/includes.cr | 1 - src/spectator/internals.cr | 7 ------- src/spectator/runner.cr | 2 ++ 3 files changed, 2 insertions(+), 8 deletions(-) delete mode 100644 src/spectator/internals.cr diff --git a/src/spectator/includes.cr b/src/spectator/includes.cr index 8974cf7..c27fc49 100644 --- a/src/spectator/includes.cr +++ b/src/spectator/includes.cr @@ -11,7 +11,6 @@ require "openssl" # First the sub-modules. -require "./internals" require "./dsl" require "./expectations" require "./matchers" diff --git a/src/spectator/internals.cr b/src/spectator/internals.cr deleted file mode 100644 index c29ac5d..0000000 --- a/src/spectator/internals.cr +++ /dev/null @@ -1,7 +0,0 @@ -require "./internals/*" - -module Spectator - # Utilities and black magic (hacks) employed by the testing framework. - module Internals - end -end diff --git a/src/spectator/runner.cr b/src/spectator/runner.cr index afd517c..1229ebf 100644 --- a/src/spectator/runner.cr +++ b/src/spectator/runner.cr @@ -1,3 +1,5 @@ +require "./internals/harness" + module Spectator # Main driver for executing tests and feeding results to formatters. class Runner From cd0ba81417dd8275bb150bc4d0c27afc46d02be1 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 13:00:23 -0600 Subject: [PATCH 043/205] Pass test values to test instance --- src/spectator/spec_builder.cr | 2 +- src/spectator/spec_builder/example_builder.cr | 5 +++-- .../spec_builder/nested_example_group_builder.cr | 2 +- src/spectator/spec_builder/root_example_group_builder.cr | 3 ++- .../spec_builder/sample_example_group_builder.cr | 8 +++++--- src/spectator/test_context.cr | 7 ++++++- src/spectator/test_values.cr | 1 + src/spectator_test.cr | 3 +++ 8 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 676d02c..a5efc17 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -44,7 +44,7 @@ module Spectator # The example will be instantiated later. def add_example(description : String, source : Source, example_type : ::SpectatorTest.class, &runner : ::SpectatorTest ->) : Nil - builder = ->{ example_type.new.as(::SpectatorTest) } + builder = ->(values : TestValues) { example_type.new(values).as(::SpectatorTest) } factory = ExampleBuilder.new(description, source, builder, runner) @@stack.current.add_child(factory) end diff --git a/src/spectator/spec_builder/example_builder.cr b/src/spectator/spec_builder/example_builder.cr index a3f0621..d249214 100644 --- a/src/spectator/spec_builder/example_builder.cr +++ b/src/spectator/spec_builder/example_builder.cr @@ -1,15 +1,16 @@ require "../../spectator_test" +require "../test_values" require "../test_wrapper" module Spectator::SpecBuilder class ExampleBuilder - alias FactoryMethod = -> ::SpectatorTest + alias FactoryMethod = TestValues -> ::SpectatorTest def initialize(@description : String, @source : Source, @builder : FactoryMethod, @runner : TestMethod) end def build(group) - test = @builder.call + test = @builder.call(group.context.values) wrapper = TestWrapper.new(@description, @source, test, @runner) RunnableExample.new(group, wrapper).as(ExampleComponent) end diff --git a/src/spectator/spec_builder/nested_example_group_builder.cr b/src/spectator/spec_builder/nested_example_group_builder.cr index 002629b..0c2bf1e 100644 --- a/src/spectator/spec_builder/nested_example_group_builder.cr +++ b/src/spectator/spec_builder/nested_example_group_builder.cr @@ -7,7 +7,7 @@ module Spectator::SpecBuilder end def build(parent_group) - context = TestContext.new(parent_group.context, build_hooks) + context = TestContext.new(parent_group.context, build_hooks, parent_group.context.values) NestedExampleGroup.new(@what, parent_group, context).tap do |group| group.children = children.map do |child| child.build(group).as(ExampleComponent) diff --git a/src/spectator/spec_builder/root_example_group_builder.cr b/src/spectator/spec_builder/root_example_group_builder.cr index 0ad48ab..09cb104 100644 --- a/src/spectator/spec_builder/root_example_group_builder.cr +++ b/src/spectator/spec_builder/root_example_group_builder.cr @@ -1,9 +1,10 @@ +require "../test_values" require "./example_group_builder" module Spectator::SpecBuilder class RootExampleGroupBuilder < ExampleGroupBuilder def build - context = TestContext.new(nil, build_hooks) + context = TestContext.new(nil, build_hooks, TestValues.empty) RootExampleGroup.new(context).tap do |group| group.children = children.map do |child| child.build(group).as(ExampleComponent) diff --git a/src/spectator/spec_builder/sample_example_group_builder.cr b/src/spectator/spec_builder/sample_example_group_builder.cr index 17a100e..a7b3463 100644 --- a/src/spectator/spec_builder/sample_example_group_builder.cr +++ b/src/spectator/spec_builder/sample_example_group_builder.cr @@ -2,11 +2,12 @@ require "./nested_example_group_builder" module Spectator::SpecBuilder class SampleExampleGroupBuilder < NestedExampleGroupBuilder - def initialize(@what : String) + def initialize(@what : String | Symbol) + @id = :TODO end def build(parent_group) - context = TestContext.new(parent_group.context, build_hooks) + context = TestContext.new(parent_group.context, build_hooks, parent_group.context.values) NestedExampleGroup.new(@what, parent_group, context).tap do |group| group.children = [:TODO].map do |element| build_sub_group(group, element).as(ExampleComponent) @@ -15,7 +16,8 @@ module Spectator::SpecBuilder end private def build_sub_group(parent_group, element) - context = TestContext.new(parent_group.context, ExampleHooks.empty) + values = parent_group.context.values.add(@id, @what.to_s, element) + context = TestContext.new(parent_group.context, ExampleHooks.empty, values) NestedExampleGroup.new("TODO", parent_group, context).tap do |group| group.children = children.map do |child| child.build(group).as(ExampleComponent) diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr index 5681db5..0bc421d 100644 --- a/src/spectator/test_context.cr +++ b/src/spectator/test_context.cr @@ -1,6 +1,11 @@ +require "./example_hooks" +require "./test_values" + module Spectator class TestContext - def initialize(@parent : TestContext?, @hooks : ExampleHooks) + getter values + + def initialize(@parent : TestContext?, @hooks : ExampleHooks, @values : TestValues) @before_all_hooks_run = false @after_all_hooks_run = false end diff --git a/src/spectator/test_values.cr b/src/spectator/test_values.cr index 74fc923..d07da71 100644 --- a/src/spectator/test_values.cr +++ b/src/spectator/test_values.cr @@ -1,3 +1,4 @@ +require "./typed_value_wrapper" require "./value_wrapper" module Spectator diff --git a/src/spectator_test.cr b/src/spectator_test.cr index 8f14454..364d79e 100644 --- a/src/spectator_test.cr +++ b/src/spectator_test.cr @@ -5,4 +5,7 @@ require "./spectator/dsl" # so that the namespace isn't leaked into tests unexpectedly. class SpectatorTest include ::Spectator::DSL + + def initialize(@spectator_test_values : ::Spectator::TestValues) + end end From e5bd582f5fdce727fde4b9f306738610bd129f13 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 14:05:55 -0600 Subject: [PATCH 044/205] Handle collection for sample group --- src/spectator/dsl/groups.cr | 5 ++++- src/spectator/spec_builder.cr | 4 ++-- .../spec_builder/sample_example_group_builder.cr | 14 ++++++++------ 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index dbf6d60..309ba68 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -35,7 +35,10 @@ module Spectator end class Context%sample < {{@type.id}} - ::Spectator::SpecBuilder.start_sample_group({{what.stringify}}) + ::Spectator::SpecBuilder.start_sample_group({{what.stringify}}, :%sample) do |values| + sample = Sample%sample.new(values) + sample.%collection.to_a + end def {{block_arg}} 1 diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index a5efc17..7b0c93c 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -26,8 +26,8 @@ module Spectator # 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) : Nil - group = SampleExampleGroupBuilder.new(*args) + def start_sample_group(*args, &block : TestValues -> Array(T)) : Nil forall T + group = SampleExampleGroupBuilder(T).new(*args, block) @@stack.push(group) end diff --git a/src/spectator/spec_builder/sample_example_group_builder.cr b/src/spectator/spec_builder/sample_example_group_builder.cr index a7b3463..aab9a03 100644 --- a/src/spectator/spec_builder/sample_example_group_builder.cr +++ b/src/spectator/spec_builder/sample_example_group_builder.cr @@ -1,15 +1,17 @@ require "./nested_example_group_builder" module Spectator::SpecBuilder - class SampleExampleGroupBuilder < NestedExampleGroupBuilder - def initialize(@what : String | Symbol) - @id = :TODO + class SampleExampleGroupBuilder(T) < NestedExampleGroupBuilder + def initialize(what : String | Symbol, @id : Symbol, @collection_builder : TestValues -> Array(T)) + super(what) end def build(parent_group) - context = TestContext.new(parent_group.context, build_hooks, parent_group.context.values) + values = parent_group.context.values + collection = @collection_builder.call(values) + context = TestContext.new(parent_group.context, build_hooks, values) NestedExampleGroup.new(@what, parent_group, context).tap do |group| - group.children = [:TODO].map do |element| + group.children = collection.map do |element| build_sub_group(group, element).as(ExampleComponent) end end @@ -18,7 +20,7 @@ module Spectator::SpecBuilder private def build_sub_group(parent_group, element) values = parent_group.context.values.add(@id, @what.to_s, element) context = TestContext.new(parent_group.context, ExampleHooks.empty, values) - NestedExampleGroup.new("TODO", parent_group, context).tap do |group| + NestedExampleGroup.new(element.inspect, parent_group, context).tap do |group| group.children = children.map do |child| child.build(group).as(ExampleComponent) end From f6e95719ad026e6f02de917e27aa044758aa3a95 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 14:08:17 -0600 Subject: [PATCH 045/205] Remove unnecessary quotes in example name --- 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 738fb2d..0637b5f 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -20,7 +20,7 @@ module Spectator %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) ::Spectator::SpecBuilder.add_example( - {{what.stringify}}, + {{what.is_a?(StringLiteral) ? what : what.stringify}}, %source, {{@type.name}} ) { |test| test.as({{@type.name}}).%run } From c3ea34e762576be018041b89b20885a2698c586b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 14:57:31 -0600 Subject: [PATCH 046/205] Retrieve test value for sample group --- src/spectator/dsl/groups.cr | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 309ba68..abe93b4 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -26,22 +26,25 @@ module Spectator context({{what}}) {{block}} end - macro sample(what, &block) + macro sample(collection, &block) {% block_arg = block.args.empty? ? :value.id : block.args.first.id %} - class Sample%sample < {{@type.id}} - def %collection - {{what}} - end + + def %collection + {{collection}} + end + + def %to_a + %collection.to_a end class Context%sample < {{@type.id}} - ::Spectator::SpecBuilder.start_sample_group({{what.stringify}}, :%sample) do |values| - sample = Sample%sample.new(values) - sample.%collection.to_a + ::Spectator::SpecBuilder.start_sample_group({{collection.stringify}}, :%sample) do |values| + sample = {{@type.id}}.new(values) + sample.%to_a end def {{block_arg}} - 1 + @spectator_test_values.get_value(:%sample, typeof(%to_a.first)) end {{block.body}} From 1f300a4a184817f72b6ecff13fd1539cc5bfa71b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 15:05:28 -0600 Subject: [PATCH 047/205] Use label in sub-group name --- src/spectator/dsl/groups.cr | 2 +- src/spectator/spec_builder/sample_example_group_builder.cr | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index abe93b4..a40b26f 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -38,7 +38,7 @@ module Spectator end class Context%sample < {{@type.id}} - ::Spectator::SpecBuilder.start_sample_group({{collection.stringify}}, :%sample) do |values| + ::Spectator::SpecBuilder.start_sample_group({{collection.stringify}}, :%sample, {{block_arg.stringify}}) do |values| sample = {{@type.id}}.new(values) sample.%to_a end diff --git a/src/spectator/spec_builder/sample_example_group_builder.cr b/src/spectator/spec_builder/sample_example_group_builder.cr index aab9a03..ae2909e 100644 --- a/src/spectator/spec_builder/sample_example_group_builder.cr +++ b/src/spectator/spec_builder/sample_example_group_builder.cr @@ -2,7 +2,7 @@ require "./nested_example_group_builder" module Spectator::SpecBuilder class SampleExampleGroupBuilder(T) < NestedExampleGroupBuilder - def initialize(what : String | Symbol, @id : Symbol, @collection_builder : TestValues -> Array(T)) + def initialize(what : String | Symbol, @id : Symbol, @label : String, @collection_builder : TestValues -> Array(T)) super(what) end @@ -20,7 +20,7 @@ module Spectator::SpecBuilder private def build_sub_group(parent_group, element) values = parent_group.context.values.add(@id, @what.to_s, element) context = TestContext.new(parent_group.context, ExampleHooks.empty, values) - NestedExampleGroup.new(element.inspect, parent_group, context).tap do |group| + NestedExampleGroup.new("#{@label} = #{element.inspect}", parent_group, context).tap do |group| group.children = children.map do |child| child.build(group).as(ExampleComponent) end From 0067191423ec073cc85895f6281295c9b1a373a2 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 15:11:36 -0600 Subject: [PATCH 048/205] Remove reference to Internals namespace --- src/spectator/dsl/values.cr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/spectator/dsl/values.cr b/src/spectator/dsl/values.cr index 283c6a3..6b8db5c 100644 --- a/src/spectator/dsl/values.cr +++ b/src/spectator/dsl/values.cr @@ -5,14 +5,14 @@ module Spectator {{block.body}} end - @%wrapper : ::Spectator::Internals::ValueWrapper? + @%wrapper : ::Spectator::ValueWrapper? def {{name.id}} if (wrapper = @%wrapper) - wrapper.as(::Spectator::Internals::TypedValueWrapper(typeof(%value))).value + wrapper.as(::Spectator::TypedValueWrapper(typeof(%value))).value else %value.tap do |value| - @%wrapper = ::Spectator::Internals::TypedValueWrapper.new(value) + @%wrapper = ::Spectator::TypedValueWrapper.new(value) end end end From 7e6080aa5f6bc71c1489fc1ba00fa5366b5d0c4c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 15:11:54 -0600 Subject: [PATCH 049/205] Move harness out of internals --- src/spectator/expectations/expectation_partial.cr | 2 +- src/spectator/{internals => }/harness.cr | 4 ++-- src/spectator/runnable_example.cr | 2 +- src/spectator/runner.cr | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) rename src/spectator/{internals => }/harness.cr (96%) diff --git a/src/spectator/expectations/expectation_partial.cr b/src/spectator/expectations/expectation_partial.cr index 1cc8831..ed2e33c 100644 --- a/src/spectator/expectations/expectation_partial.cr +++ b/src/spectator/expectations/expectation_partial.cr @@ -40,7 +40,7 @@ module Spectator::Expectations # Reports an expectation to the current harness. private def report(match_data : Matchers::MatchData) expectation = Expectation.new(match_data, @source) - Internals::Harness.current.report_expectation(expectation) + Harness.current.report_expectation(expectation) end end end diff --git a/src/spectator/internals/harness.cr b/src/spectator/harness.cr similarity index 96% rename from src/spectator/internals/harness.cr rename to src/spectator/harness.cr index 8f70045..8fe0070 100644 --- a/src/spectator/internals/harness.cr +++ b/src/spectator/harness.cr @@ -1,4 +1,4 @@ -module Spectator::Internals +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. @@ -9,7 +9,7 @@ module Spectator::Internals # ``` # Then from the example code, the harness can be accessed via `#current` like so: # ``` - # harness = ::Spectator::Internals::Harness.current + # harness = ::Spectator::Harness.current # # Do something with the harness. # ``` # Of course, the end-user shouldn't see this or work directly with the harness. diff --git a/src/spectator/runnable_example.cr b/src/spectator/runnable_example.cr index e56f7d2..c608619 100644 --- a/src/spectator/runnable_example.cr +++ b/src/spectator/runnable_example.cr @@ -8,7 +8,7 @@ module Spectator # and translates to a usable result. def run_impl : Result result = capture_result - expectations = Internals::Harness.current.expectations + expectations = Harness.current.expectations translate_result(result, expectations) end diff --git a/src/spectator/runner.cr b/src/spectator/runner.cr index 1229ebf..bf2e53c 100644 --- a/src/spectator/runner.cr +++ b/src/spectator/runner.cr @@ -1,4 +1,4 @@ -require "./internals/harness" +require "./harness" module Spectator # Main driver for executing tests and feeding results to formatters. @@ -59,7 +59,7 @@ module Spectator result = if @config.dry_run? && example.is_a?(RunnableExample) dry_run_result(example) else - Internals::Harness.run(example) + Harness.run(example) end @config.each_formatter(&.end_example(result)) result From 311b4b0f2fcf8fcba7d5e9abfea4c70773b14add Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 15:30:29 -0600 Subject: [PATCH 050/205] Use name instead of block_arg --- src/spectator/dsl/groups.cr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index a40b26f..b9bf307 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -27,7 +27,7 @@ module Spectator end macro sample(collection, &block) - {% block_arg = block.args.empty? ? :value.id : block.args.first.id %} + {% name = block.args.empty? ? :value.id : block.args.first.id %} def %collection {{collection}} @@ -38,12 +38,12 @@ module Spectator end class Context%sample < {{@type.id}} - ::Spectator::SpecBuilder.start_sample_group({{collection.stringify}}, :%sample, {{block_arg.stringify}}) do |values| + ::Spectator::SpecBuilder.start_sample_group({{collection.stringify}}, :%sample, {{name.stringify}}) do |values| sample = {{@type.id}}.new(values) sample.%to_a end - def {{block_arg}} + def {{name}} @spectator_test_values.get_value(:%sample, typeof(%to_a.first)) end From 3d37a94aaf0cb1c6f815809995ea6a247775006f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 15:41:58 -0600 Subject: [PATCH 051/205] Add optional count to sample group --- src/spectator/dsl/groups.cr | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index b9bf307..8caa779 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -26,7 +26,7 @@ module Spectator context({{what}}) {{block}} end - macro sample(collection, &block) + macro sample(collection, count = nil, &block) {% name = block.args.empty? ? :value.id : block.args.first.id %} def %collection @@ -34,7 +34,11 @@ module Spectator end def %to_a - %collection.to_a + {% if count %} + %collection.first({{count}}) + {% else %} + %collection.to_a + {% end %} end class Context%sample < {{@type.id}} From 6ae817d4bc61255bb627cd68f22dc99674e6fff9 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 15:49:06 -0600 Subject: [PATCH 052/205] Add random sample --- src/spectator/dsl/groups.cr | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 8caa779..5c9e2d1 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -56,5 +56,41 @@ module Spectator ::Spectator::SpecBuilder.end_group end end + + macro random_sample(collection, count = nil, &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}} + ::Spectator::SpecBuilder.start_sample_group({{collection.stringify}}, :%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 end end From a26159277606c6f2aadf7c15c50860dc4981ed4e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 15:56:48 -0600 Subject: [PATCH 053/205] Add given group --- src/spectator/dsl/groups.cr | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 5c9e2d1..314d894 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -92,5 +92,40 @@ module Spectator ::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 end From dacca0bf1e2ead679392b85f24bedb19fa605d3e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 16:02:24 -0600 Subject: [PATCH 054/205] Add implicit subject --- src/spectator/dsl/groups.cr | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 314d894..d0aa425 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -16,6 +16,16 @@ module Spectator {% end %} ) + {% if what.is_a?(Path) || what.is_a?(Generic) %} + macro described_class + {{what}} + end + + def subject(*args) + described_class.new(*args) + end + {% end %} + {{block.body}} ::Spectator::SpecBuilder.end_group From e3e4cac9c13ef0b407720ba626692e299ccf1a03 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 16:23:13 -0600 Subject: [PATCH 055/205] Add source to example groups --- src/spectator/dsl/groups.cr | 37 +++++++++++-------- src/spectator/example_component.cr | 2 + src/spectator/nested_example_group.cr | 4 +- src/spectator/root_example_group.cr | 4 ++ .../nested_example_group_builder.cr | 4 +- .../sample_example_group_builder.cr | 8 ++-- 6 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index d0aa425..7b3b851 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -2,19 +2,22 @@ require "../spec_builder" module Spectator module DSL - macro context(what, &block) + macro context(what, _source_file = __FILE__, _source_line = __LINE__, &block) class Context%context < {{@type.id}} - ::Spectator::SpecBuilder.start_group( - {% if what.is_a?(StringLiteral) %} - {% if what.starts_with?("#") || what.starts_with?(".") %} - {{what.id.symbolize}} - {% else %} - {{what}} - {% end %} - {% else %} - {{what.symbolize}} - {% end %} - ) + {% + description = if what.is_a?(StringLiteral) + if what.starts_with?("#") || what.starts_with?(".") + what.id.symbolize + else + what + end + else + what.symbolize + end + %} + + %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) + ::Spectator::SpecBuilder.start_group({{description}}, %source) {% if what.is_a?(Path) || what.is_a?(Generic) %} macro described_class @@ -36,7 +39,7 @@ module Spectator context({{what}}) {{block}} end - macro sample(collection, count = nil, &block) + macro sample(collection, count = nil, _source_file = __FILE__, _source_line = __LINE__, &block) {% name = block.args.empty? ? :value.id : block.args.first.id %} def %collection @@ -52,7 +55,8 @@ module Spectator end class Context%sample < {{@type.id}} - ::Spectator::SpecBuilder.start_sample_group({{collection.stringify}}, :%sample, {{name.stringify}}) do |values| + %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 @@ -67,7 +71,7 @@ module Spectator end end - macro random_sample(collection, count = nil, &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 @@ -83,7 +87,8 @@ module Spectator end class Context%sample < {{@type.id}} - ::Spectator::SpecBuilder.start_sample_group({{collection.stringify}}, :%sample, {{name.stringify}}) do |values| + %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 %} diff --git a/src/spectator/example_component.cr b/src/spectator/example_component.cr index d2b651e..4311f54 100644 --- a/src/spectator/example_component.cr +++ b/src/spectator/example_component.cr @@ -6,6 +6,8 @@ module Spectator # TODO: Rename to description. abstract def what : Symbol | String + abstract def source : Source + # Indicates whether the example (or group) has been completely run. abstract def finished? : Bool diff --git a/src/spectator/nested_example_group.cr b/src/spectator/nested_example_group.cr index c529696..fb1902c 100644 --- a/src/spectator/nested_example_group.cr +++ b/src/spectator/nested_example_group.cr @@ -8,6 +8,8 @@ module Spectator # This is a symbol when referencing a type. getter what : Symbol | String + getter source : Source + # Group that this is nested in. getter parent : ExampleGroup @@ -18,7 +20,7 @@ module Spectator # The parent's children must contain this group, # otherwise there may be unexpected behavior. # The *hooks* are stored to be triggered later. - def initialize(@what, @parent, context) + def initialize(@what, @source, @parent, context) super(context) end diff --git a/src/spectator/root_example_group.cr b/src/spectator/root_example_group.cr index 65836c3..ee787c0 100644 --- a/src/spectator/root_example_group.cr +++ b/src/spectator/root_example_group.cr @@ -9,6 +9,10 @@ module Spectator "ROOT" end + def source : Source + Source.new(__FILE__, __LINE__) + end + # Indicates that the group is symbolic. def symbolic? : Bool true diff --git a/src/spectator/spec_builder/nested_example_group_builder.cr b/src/spectator/spec_builder/nested_example_group_builder.cr index 0c2bf1e..7525858 100644 --- a/src/spectator/spec_builder/nested_example_group_builder.cr +++ b/src/spectator/spec_builder/nested_example_group_builder.cr @@ -3,12 +3,12 @@ require "./example_group_builder" module Spectator::SpecBuilder class NestedExampleGroupBuilder < ExampleGroupBuilder - def initialize(@what : String | Symbol) + def initialize(@what : String | Symbol, @source : Source) end def build(parent_group) context = TestContext.new(parent_group.context, build_hooks, parent_group.context.values) - NestedExampleGroup.new(@what, parent_group, context).tap do |group| + NestedExampleGroup.new(@what, @source, parent_group, context).tap do |group| group.children = children.map do |child| child.build(group).as(ExampleComponent) end diff --git a/src/spectator/spec_builder/sample_example_group_builder.cr b/src/spectator/spec_builder/sample_example_group_builder.cr index ae2909e..a2c6ce3 100644 --- a/src/spectator/spec_builder/sample_example_group_builder.cr +++ b/src/spectator/spec_builder/sample_example_group_builder.cr @@ -2,15 +2,15 @@ require "./nested_example_group_builder" module Spectator::SpecBuilder class SampleExampleGroupBuilder(T) < NestedExampleGroupBuilder - def initialize(what : String | Symbol, @id : Symbol, @label : String, @collection_builder : TestValues -> Array(T)) - super(what) + def initialize(what : String | Symbol, source : Source, @id : Symbol, @label : String, @collection_builder : TestValues -> Array(T)) + super(what, source) end def build(parent_group) values = parent_group.context.values collection = @collection_builder.call(values) context = TestContext.new(parent_group.context, build_hooks, values) - NestedExampleGroup.new(@what, parent_group, context).tap do |group| + NestedExampleGroup.new(@what, @source, parent_group, context).tap do |group| group.children = collection.map do |element| build_sub_group(group, element).as(ExampleComponent) end @@ -20,7 +20,7 @@ module Spectator::SpecBuilder private def build_sub_group(parent_group, element) values = parent_group.context.values.add(@id, @what.to_s, element) context = TestContext.new(parent_group.context, ExampleHooks.empty, values) - NestedExampleGroup.new("#{@label} = #{element.inspect}", parent_group, context).tap do |group| + NestedExampleGroup.new("#{@label} = #{element.inspect}", @source, parent_group, context).tap do |group| group.children = children.map do |child| child.build(group).as(ExampleComponent) end From edabaa94471e2226174fa80febe0edb051657f8c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 16:25:43 -0600 Subject: [PATCH 056/205] Dumb auto-format --- src/spectator/dsl/groups.cr | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 7b3b851..8153b0e 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -114,21 +114,19 @@ module Spectator 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 - %} + {% # 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. From 25778d7b41126c828fc915d667021070b071f18b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 16:49:44 -0600 Subject: [PATCH 057/205] Change "what" to "description" --- src/spectator.cr | 10 +++++----- src/spectator/dsl/examples.cr | 10 +++++----- src/spectator/dsl/groups.cr | 20 +++++++++---------- src/spectator/example.cr | 4 ++-- src/spectator/example_component.cr | 7 +++++-- .../formatting/document_formatter.cr | 4 ++-- src/spectator/nested_example_group.cr | 12 +++++------ src/spectator/root_example_group.cr | 4 ++-- .../nested_example_group_builder.cr | 4 ++-- .../sample_example_group_builder.cr | 8 ++++---- 10 files changed, 43 insertions(+), 40 deletions(-) diff --git a/src/spectator.cr b/src/spectator.cr index bcfeda6..a752c60 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -24,7 +24,7 @@ module Spectator # 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(what, &block) + 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. @@ -39,15 +39,15 @@ module Spectator # Root-level class that contains all examples and example groups. class SpectatorTest - # Pass off the "what" argument and block to `DSL::StructureDSL.describe`. + # Pass off the description argument and block to `DSL::StructureDSL.describe`. # That method will handle creating a new group for this spec. - describe({{what}}) {{block}} + describe({{description}}) {{block}} end end # ditto - macro context(what, &block) - describe({{what}}) {{block}} + macro context(description, &block) + describe({{description}}) {{block}} end # Flag indicating whether Spectator should automatically run tests. diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 0637b5f..3b0d81e 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -3,14 +3,14 @@ require "../spec_builder" module Spectator module DSL - macro it(what, _source_file = __FILE__, _source_line = __LINE__, &block) + macro it(description, _source_file = __FILE__, _source_line = __LINE__, &block) {% if block.is_a?(Nop) %} - {% if what.is_a?(Call) %} + {% if description.is_a?(Call) %} def %run - {{what}} + {{description}} end {% else %} - {% raise "Unrecognized syntax: `it #{what}` at #{_source_file}:#{_source_line}" %} + {% raise "Unrecognized syntax: `it #{description}` at #{_source_file}:#{_source_line}" %} {% end %} {% else %} def %run @@ -20,7 +20,7 @@ module Spectator %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) ::Spectator::SpecBuilder.add_example( - {{what.is_a?(StringLiteral) ? what : what.stringify}}, + {{description.is_a?(StringLiteral) ? description : description.stringify}}, %source, {{@type.name}} ) { |test| test.as({{@type.name}}).%run } diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 8153b0e..1e1386c 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -2,26 +2,26 @@ require "../spec_builder" module Spectator module DSL - macro context(what, _source_file = __FILE__, _source_line = __LINE__, &block) + macro context(description, _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 + description = if description.is_a?(StringLiteral) + if description.starts_with?("#") || description.starts_with?(".") + description.id.symbolize else - what + description end else - what.symbolize + description.symbolize end %} %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) ::Spectator::SpecBuilder.start_group({{description}}, %source) - {% if what.is_a?(Path) || what.is_a?(Generic) %} + {% if description.is_a?(Path) || description.is_a?(Generic) %} macro described_class - {{what}} + {{description}} end def subject(*args) @@ -35,8 +35,8 @@ module Spectator end end - macro describe(what, &block) - context({{what}}) {{block}} + macro describe(description, &block) + context({{description}}) {{block}} end macro sample(collection, count = nil, _source_file = __FILE__, _source_line = __LINE__, &block) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 96d65f7..e437bc4 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -23,7 +23,7 @@ module Spectator @test_wrapper.source end - def what : String | Symbol + def description : String | Symbol @test_wrapper.description end @@ -66,7 +66,7 @@ module Spectator def to_s(io) @group.to_s(io) io << ' ' unless symbolic? && @group.symbolic? - io << what + io << description end # Creates the JSON representation of the example, diff --git a/src/spectator/example_component.cr b/src/spectator/example_component.cr index 4311f54..9260f95 100644 --- a/src/spectator/example_component.cr +++ b/src/spectator/example_component.cr @@ -3,8 +3,11 @@ module Spectator # This is used as the base node type for the composite design pattern. abstract class ExampleComponent # Text that describes the context or test. - # TODO: Rename to description. - abstract def what : Symbol | String + abstract def description : Symbol | String + + def full_description + to_s + end abstract def source : Source diff --git a/src/spectator/formatting/document_formatter.cr b/src/spectator/formatting/document_formatter.cr index 45c0a3b..c100e55 100644 --- a/src/spectator/formatting/document_formatter.cr +++ b/src/spectator/formatting/document_formatter.cr @@ -28,7 +28,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.what } + @io.puts result.call(Color) { result.example.description } end # Produces a list of groups making up the hierarchy for an example. @@ -56,7 +56,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.what + @io.puts group.description index += 1 end end diff --git a/src/spectator/nested_example_group.cr b/src/spectator/nested_example_group.cr index fb1902c..c21345e 100644 --- a/src/spectator/nested_example_group.cr +++ b/src/spectator/nested_example_group.cr @@ -6,7 +6,7 @@ module Spectator class NestedExampleGroup < ExampleGroup # Description from the user of the group's contents. # This is a symbol when referencing a type. - getter what : Symbol | String + getter description : Symbol | String getter source : Source @@ -14,23 +14,23 @@ module Spectator getter parent : ExampleGroup # Creates a new example group. - # The *what* argument is a description from the user. + # 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(@what, @source, @parent, context) + def initialize(@description, @source, @parent, context) super(context) end # Indicates wheter the group references a type. def symbolic? : Bool - @what.is_a?(Symbol) + @description.is_a?(Symbol) end # Creates a string representation of the group. - # The string consists of `#what` appended to the parent. + # The string consists of `#description` appended to the parent. # This results in a string like: # ```text # Foo#bar does something @@ -48,7 +48,7 @@ module Spectator def to_s(io) parent.to_s(io) io << ' ' unless (symbolic? || parent.is_a?(RootExampleGroup)) && parent.symbolic? - io << what + io << description end end end diff --git a/src/spectator/root_example_group.cr b/src/spectator/root_example_group.cr index ee787c0..16380e0 100644 --- a/src/spectator/root_example_group.cr +++ b/src/spectator/root_example_group.cr @@ -5,8 +5,8 @@ module Spectator # The root has no parent. class RootExampleGroup < ExampleGroup # Dummy value - this should never be used. - def what : Symbol | String - "ROOT" + def description : Symbol | String + :root end def source : Source diff --git a/src/spectator/spec_builder/nested_example_group_builder.cr b/src/spectator/spec_builder/nested_example_group_builder.cr index 7525858..efe62e5 100644 --- a/src/spectator/spec_builder/nested_example_group_builder.cr +++ b/src/spectator/spec_builder/nested_example_group_builder.cr @@ -3,12 +3,12 @@ require "./example_group_builder" module Spectator::SpecBuilder class NestedExampleGroupBuilder < ExampleGroupBuilder - def initialize(@what : String | Symbol, @source : Source) + def initialize(@description : String | Symbol, @source : Source) end def build(parent_group) context = TestContext.new(parent_group.context, build_hooks, parent_group.context.values) - NestedExampleGroup.new(@what, @source, parent_group, context).tap do |group| + NestedExampleGroup.new(@description, @source, parent_group, context).tap do |group| group.children = children.map do |child| child.build(group).as(ExampleComponent) end diff --git a/src/spectator/spec_builder/sample_example_group_builder.cr b/src/spectator/spec_builder/sample_example_group_builder.cr index a2c6ce3..c8013e3 100644 --- a/src/spectator/spec_builder/sample_example_group_builder.cr +++ b/src/spectator/spec_builder/sample_example_group_builder.cr @@ -2,15 +2,15 @@ require "./nested_example_group_builder" module Spectator::SpecBuilder class SampleExampleGroupBuilder(T) < NestedExampleGroupBuilder - def initialize(what : String | Symbol, source : Source, @id : Symbol, @label : String, @collection_builder : TestValues -> Array(T)) - super(what, source) + 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, values) - NestedExampleGroup.new(@what, @source, parent_group, context).tap do |group| + NestedExampleGroup.new(@description, @source, parent_group, context).tap do |group| group.children = collection.map do |element| build_sub_group(group, element).as(ExampleComponent) end @@ -18,7 +18,7 @@ module Spectator::SpecBuilder end private def build_sub_group(parent_group, element) - values = parent_group.context.values.add(@id, @what.to_s, element) + values = parent_group.context.values.add(@id, @description.to_s, element) context = TestContext.new(parent_group.context, ExampleHooks.empty, values) NestedExampleGroup.new("#{@label} = #{element.inspect}", @source, parent_group, context).tap do |group| group.children = children.map do |child| From 07aeaecca4d123e5533c838be98fe13141d48718 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 20:46:28 -0600 Subject: [PATCH 058/205] Add support for pending tests --- src/spectator/dsl/examples.cr | 35 +++++++++++++++++++ src/spectator/spec_builder.cr | 12 ++++++- src/spectator/spec_builder/example_builder.cr | 9 ++--- .../spec_builder/pending_example_builder.cr | 10 ++++++ .../spec_builder/runnable_example_builder.cr | 10 ++++++ 5 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 src/spectator/spec_builder/pending_example_builder.cr create mode 100644 src/spectator/spec_builder/runnable_example_builder.cr diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 3b0d81e..041b1a8 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -25,5 +25,40 @@ module Spectator {{@type.name}} ) { |test| test.as({{@type.name}}).%run } end + + macro specify(description, &block) + it({{description}}) {{block}} + end + + macro pending(description, _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 : description.stringify}}, + %source, + {{@type.name}} + ) { |test| test.as({{@type.name}}).%run } + end + + macro skip(description, &block) + pending({{description}}) {{block}} + end + + macro xit(description, &block) + pending({{description}}) {{block}} + end end end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 7b0c93c..ed897da 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -45,7 +45,17 @@ module Spectator def add_example(description : String, source : Source, example_type : ::SpectatorTest.class, &runner : ::SpectatorTest ->) : Nil builder = ->(values : TestValues) { example_type.new(values).as(::SpectatorTest) } - factory = ExampleBuilder.new(description, source, builder, runner) + 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 diff --git a/src/spectator/spec_builder/example_builder.cr b/src/spectator/spec_builder/example_builder.cr index d249214..44f23fe 100644 --- a/src/spectator/spec_builder/example_builder.cr +++ b/src/spectator/spec_builder/example_builder.cr @@ -3,16 +3,17 @@ require "../test_values" require "../test_wrapper" module Spectator::SpecBuilder - class ExampleBuilder + abstract class ExampleBuilder alias FactoryMethod = TestValues -> ::SpectatorTest def initialize(@description : String, @source : Source, @builder : FactoryMethod, @runner : TestMethod) end - def build(group) + abstract def build(group) : ExampleComponent + + private def build_test_wrapper(group) test = @builder.call(group.context.values) - wrapper = TestWrapper.new(@description, @source, test, @runner) - RunnableExample.new(group, wrapper).as(ExampleComponent) + TestWrapper.new(@description, @source, test, @runner) end end end diff --git a/src/spectator/spec_builder/pending_example_builder.cr b/src/spectator/spec_builder/pending_example_builder.cr new file mode 100644 index 0000000..731b9ab --- /dev/null +++ b/src/spectator/spec_builder/pending_example_builder.cr @@ -0,0 +1,10 @@ +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/runnable_example_builder.cr b/src/spectator/spec_builder/runnable_example_builder.cr new file mode 100644 index 0000000..8c22a15 --- /dev/null +++ b/src/spectator/spec_builder/runnable_example_builder.cr @@ -0,0 +1,10 @@ +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 From 2c400950a41f06bc1d4b331f4f75740ce235b8a4 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 20:52:26 -0600 Subject: [PATCH 059/205] Remove specs since they're mostly invalidated by the refactor --- spec/composite_example_filter_spec.cr | 33 - spec/dsl/example_factory_spec.cr | 27 - spec/dsl/nested_example_group_builder_spec.cr | 234 --- spec/dsl/root_example_group_builder_spec.cr | 202 --- spec/dsl/sample_example_group_builder_spec.cr | 350 ---- spec/errored_result_spec.cr | 87 - spec/example_conditions_spec.cr | 36 - spec/example_hooks_spec.cr | 79 - spec/example_iterator_spec.cr | 205 --- spec/expectation_failed_spec.cr | 19 - .../expectations/example_expectations_spec.cr | 207 --- .../expectations/expectation_reporter_spec.cr | 80 - spec/expectations/expectation_spec.cr | 51 - spec/failed_result_spec.cr | 87 - spec/fomatting/color_spec.cr | 93 - spec/helpers/errored_example.cr | 37 - spec/helpers/example_hooks_helper.cr | 59 - spec/helpers/expectations_helper.cr | 71 - spec/helpers/failing_example.cr | 44 - spec/helpers/matchers_helper.cr | 20 - spec/helpers/passing_example.cr | 42 - spec/helpers/result_call_spy.cr | 16 - spec/helpers/spy_example.cr | 69 - spec/helpers/spy_formatter.cr | 47 - spec/helpers/spy_sut.cr | 28 - spec/internals/harness_spec.cr | 109 -- spec/internals/sample_values_spec.cr | 117 -- spec/internals/typed_value_wrapper_spec.cr | 18 - spec/line_example_filter_spec.cr | 21 - spec/name_example_filter_spec.cr | 21 - spec/nested_example_group_spec.cr | 1130 ------------ spec/null_example_filter_spec.cr | 11 - spec/pending_example_spec.cr | 122 -- spec/pending_result_spec.cr | 53 - spec/report_spec.cr | 237 --- spec/root_example_group_spec.cr | 810 --------- spec/runnable_example_spec.cr | 1568 ----------------- spec/runner_spec.cr | 270 --- spec/source_example_filter_spec.cr | 22 - spec/source_spec.cr | 76 - spec/spec_helper.cr | 4 +- spec/successful_result_spec.cr | 77 - spec/test_suite_spec.cr | 31 - 43 files changed, 1 insertion(+), 6919 deletions(-) delete mode 100644 spec/composite_example_filter_spec.cr delete mode 100644 spec/dsl/example_factory_spec.cr delete mode 100644 spec/dsl/nested_example_group_builder_spec.cr delete mode 100644 spec/dsl/root_example_group_builder_spec.cr delete mode 100644 spec/dsl/sample_example_group_builder_spec.cr delete mode 100644 spec/errored_result_spec.cr delete mode 100644 spec/example_conditions_spec.cr delete mode 100644 spec/example_hooks_spec.cr delete mode 100644 spec/example_iterator_spec.cr delete mode 100644 spec/expectation_failed_spec.cr delete mode 100644 spec/expectations/example_expectations_spec.cr delete mode 100644 spec/expectations/expectation_reporter_spec.cr delete mode 100644 spec/expectations/expectation_spec.cr delete mode 100644 spec/failed_result_spec.cr delete mode 100644 spec/fomatting/color_spec.cr delete mode 100644 spec/helpers/errored_example.cr delete mode 100644 spec/helpers/example_hooks_helper.cr delete mode 100644 spec/helpers/expectations_helper.cr delete mode 100644 spec/helpers/failing_example.cr delete mode 100644 spec/helpers/matchers_helper.cr delete mode 100644 spec/helpers/passing_example.cr delete mode 100644 spec/helpers/result_call_spy.cr delete mode 100644 spec/helpers/spy_example.cr delete mode 100644 spec/helpers/spy_formatter.cr delete mode 100644 spec/helpers/spy_sut.cr delete mode 100644 spec/internals/harness_spec.cr delete mode 100644 spec/internals/sample_values_spec.cr delete mode 100644 spec/internals/typed_value_wrapper_spec.cr delete mode 100644 spec/line_example_filter_spec.cr delete mode 100644 spec/name_example_filter_spec.cr delete mode 100644 spec/nested_example_group_spec.cr delete mode 100644 spec/null_example_filter_spec.cr delete mode 100644 spec/pending_example_spec.cr delete mode 100644 spec/pending_result_spec.cr delete mode 100644 spec/report_spec.cr delete mode 100644 spec/root_example_group_spec.cr delete mode 100644 spec/runnable_example_spec.cr delete mode 100644 spec/runner_spec.cr delete mode 100644 spec/source_example_filter_spec.cr delete mode 100644 spec/source_spec.cr delete mode 100644 spec/successful_result_spec.cr delete mode 100644 spec/test_suite_spec.cr diff --git a/spec/composite_example_filter_spec.cr b/spec/composite_example_filter_spec.cr deleted file mode 100644 index 0319712..0000000 --- a/spec/composite_example_filter_spec.cr +++ /dev/null @@ -1,33 +0,0 @@ -require "./spec_helper" - -describe Spectator::CompositeExampleFilter do - describe "#includes?" do - context "with a matching filter" do - it "is true" do - example = PassingExample.create - filters = [Spectator::NullExampleFilter.new.as(Spectator::ExampleFilter)] - filter = Spectator::CompositeExampleFilter.new(filters) - filter.includes?(example).should be_true - end - end - - context "with a non-matching filter" do - it "is false" do - example = PassingExample.create - source = Spectator::Source.new(__FILE__, __LINE__) - filters = [Spectator::SourceExampleFilter.new(source).as(Spectator::ExampleFilter)] - filter = Spectator::CompositeExampleFilter.new(filters) - filter.includes?(example).should be_false - end - end - - context "with no filters" do - it "is false" do - example = PassingExample.create - filters = [] of Spectator::ExampleFilter - filter = Spectator::CompositeExampleFilter.new(filters) - filter.includes?(example).should be_false - end - end - end -end diff --git a/spec/dsl/example_factory_spec.cr b/spec/dsl/example_factory_spec.cr deleted file mode 100644 index b3041d2..0000000 --- a/spec/dsl/example_factory_spec.cr +++ /dev/null @@ -1,27 +0,0 @@ -require "../spec_helper" - -describe Spectator::DSL::ExampleFactory do - describe "#build" do - it "creates an example of the correct type" do - factory = Spectator::DSL::ExampleFactory.new(SpyExample) - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - example = factory.build(group, Spectator::Internals::SampleValues.empty) - example.should be_a(SpyExample) - end - - it "passes along the group" do - factory = Spectator::DSL::ExampleFactory.new(SpyExample) - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - example = factory.build(group, Spectator::Internals::SampleValues.empty) - example.group.should eq(group) - end - - it "passes along the sample values" do - factory = Spectator::DSL::ExampleFactory.new(SpyExample) - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - values = Spectator::Internals::SampleValues.empty.add(:foo, "foo", 12345) - example = factory.build(group, values) - example.as(SpyExample).sample_values.should eq(values) - end - end -end diff --git a/spec/dsl/nested_example_group_builder_spec.cr b/spec/dsl/nested_example_group_builder_spec.cr deleted file mode 100644 index cf41fb6..0000000 --- a/spec/dsl/nested_example_group_builder_spec.cr +++ /dev/null @@ -1,234 +0,0 @@ -require "../spec_helper" - -describe Spectator::DSL::NestedExampleGroupBuilder do - describe "#add_child" do - it "creates the correct number of children" do - builder = Spectator::DSL::NestedExampleGroupBuilder.new("foo") - 3.times do - factory = Spectator::DSL::ExampleFactory.new(PassingExample) - group_builder = Spectator::DSL::NestedExampleGroupBuilder.new("bar") - builder.add_child(factory) - builder.add_child(group_builder) - end - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.children.size.should eq(6) - end - - context "with an ExampleFactory" do - it "creates the example" do - builder = Spectator::DSL::NestedExampleGroupBuilder.new("foo") - factory = Spectator::DSL::ExampleFactory.new(PassingExample) - builder.add_child(factory) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.children.first.should be_a(PassingExample) - end - end - - context "with an ExampleGroupBuilder" do - it "creates the group" do - builder = Spectator::DSL::NestedExampleGroupBuilder.new("foo") - group_builder = Spectator::DSL::NestedExampleGroupBuilder.new("bar") - builder.add_child(group_builder) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.children.first.should be_a(Spectator::NestedExampleGroup) - end - end - end - - describe "#add_before_all_hook" do - it "adds a hook" do - hook_called = false - builder = Spectator::DSL::NestedExampleGroupBuilder.new("foo") - builder.add_before_all_hook(->{ - hook_called = true - }) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.run_before_hooks - hook_called.should eq(true) - end - - it "supports multiple hooks" do - call_count = 0 - builder = Spectator::DSL::NestedExampleGroupBuilder.new("foo") - 5.times do |i| - builder.add_before_all_hook(->{ - call_count += i + 1 - }) - end - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.run_before_hooks - call_count.should eq(15) - end - end - - describe "#add_before_each_hook" do - it "adds a hook" do - hook_called = false - builder = Spectator::DSL::NestedExampleGroupBuilder.new("foo") - builder.add_before_each_hook(->{ - hook_called = true - }) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.run_before_hooks - hook_called.should eq(true) - end - - it "supports multiple hooks" do - call_count = 0 - builder = Spectator::DSL::NestedExampleGroupBuilder.new("foo") - 5.times do |i| - builder.add_before_each_hook(->{ - call_count += i + 1 - }) - end - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.run_before_hooks - call_count.should eq(15) - end - end - - describe "#add_after_all_hook" do - it "adds a hook" do - hook_called = false - builder = Spectator::DSL::NestedExampleGroupBuilder.new("foo") - builder.add_after_all_hook(->{ - hook_called = true - }) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.run_after_hooks - hook_called.should eq(true) - end - - it "supports multiple hooks" do - call_count = 0 - builder = Spectator::DSL::NestedExampleGroupBuilder.new("foo") - 5.times do |i| - builder.add_after_all_hook(->{ - call_count += i + 1 - }) - end - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.run_after_hooks - call_count.should eq(15) - end - end - - describe "#add_after_each_hook" do - it "adds a hook" do - hook_called = false - builder = Spectator::DSL::NestedExampleGroupBuilder.new("foo") - builder.add_after_each_hook(->{ - hook_called = true - }) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.run_after_hooks - hook_called.should eq(true) - end - - it "supports multiple hooks" do - call_count = 0 - builder = Spectator::DSL::NestedExampleGroupBuilder.new("foo") - 5.times do |i| - builder.add_after_each_hook(->{ - call_count += i + 1 - }) - end - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.run_after_hooks - call_count.should eq(15) - end - end - - describe "#add_around_each_hook" do - it "adds a hook" do - hook_called = false - builder = Spectator::DSL::NestedExampleGroupBuilder.new("foo") - builder.add_around_each_hook(->(_proc : ->) { - hook_called = true - }) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - proc = group.wrap_around_each_hooks { } - proc.call - hook_called.should eq(true) - end - - it "supports multiple hooks" do - call_count = 0 - builder = Spectator::DSL::NestedExampleGroupBuilder.new("foo") - 5.times do |i| - builder.add_around_each_hook(->(proc : ->) { - call_count += i + 1 - proc.call - }) - end - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - proc = group.wrap_around_each_hooks { } - proc.call - call_count.should eq(15) - end - end - - describe "#build" do - it "passes along the what value" do - what = "TEST" - builder = Spectator::DSL::NestedExampleGroupBuilder.new(what) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.what.should eq(what) - end - - it "passes along the parent" do - factory = Spectator::DSL::ExampleFactory.new(SpyExample) - builder = Spectator::DSL::NestedExampleGroupBuilder.new("foo") - builder.add_child(factory) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.parent.should be(root) - end - - it "passes along the sample values" do - factory = Spectator::DSL::ExampleFactory.new(SpyExample) - builder = Spectator::DSL::NestedExampleGroupBuilder.new("foo") - builder.add_child(factory) - values = Spectator::Internals::SampleValues.empty.add(:foo, "foo", 12345) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, values) - group.children.first.as(SpyExample).sample_values.should eq(values) - end - - it "specifies the parent of the children correctly" do - builder = Spectator::DSL::NestedExampleGroupBuilder.new("foo") - 3.times do - factory = Spectator::DSL::ExampleFactory.new(PassingExample) - group_builder = Spectator::DSL::NestedExampleGroupBuilder.new("bar") - builder.add_child(factory) - builder.add_child(group_builder) - end - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.children.all? do |child| - case (child) - when Spectator::Example - child.group == group - when Spectator::NestedExampleGroup - child.parent == group - else - false - end - end.should be_true - end - end -end diff --git a/spec/dsl/root_example_group_builder_spec.cr b/spec/dsl/root_example_group_builder_spec.cr deleted file mode 100644 index b4aeb9d..0000000 --- a/spec/dsl/root_example_group_builder_spec.cr +++ /dev/null @@ -1,202 +0,0 @@ -require "../spec_helper" - -describe Spectator::DSL::RootExampleGroupBuilder do - describe "#add_child" do - it "creates the correct number of children" do - builder = Spectator::DSL::RootExampleGroupBuilder.new - 3.times do - factory = Spectator::DSL::ExampleFactory.new(PassingExample) - group_builder = Spectator::DSL::NestedExampleGroupBuilder.new("foo") - builder.add_child(factory) - builder.add_child(group_builder) - end - group = builder.build(Spectator::Internals::SampleValues.empty) - group.children.size.should eq(6) - end - - context "with an ExampleFactory" do - it "creates the example" do - builder = Spectator::DSL::RootExampleGroupBuilder.new - factory = Spectator::DSL::ExampleFactory.new(PassingExample) - builder.add_child(factory) - group = builder.build(Spectator::Internals::SampleValues.empty) - group.children.first.should be_a(PassingExample) - end - end - - context "with an ExampleGroupBuilder" do - it "creates the group" do - builder = Spectator::DSL::RootExampleGroupBuilder.new - group_builder = Spectator::DSL::NestedExampleGroupBuilder.new("foo") - builder.add_child(group_builder) - group = builder.build(Spectator::Internals::SampleValues.empty) - group.children.first.should be_a(Spectator::NestedExampleGroup) - end - end - end - - describe "#add_before_all_hook" do - it "adds a hook" do - hook_called = false - builder = Spectator::DSL::RootExampleGroupBuilder.new - builder.add_before_all_hook(->{ - hook_called = true - }) - group = builder.build(Spectator::Internals::SampleValues.empty) - group.run_before_hooks - hook_called.should eq(true) - end - - it "supports multiple hooks" do - call_count = 0 - builder = Spectator::DSL::RootExampleGroupBuilder.new - 5.times do |i| - builder.add_before_all_hook(->{ - call_count += i + 1 - }) - end - group = builder.build(Spectator::Internals::SampleValues.empty) - group.run_before_hooks - call_count.should eq(15) - end - end - - describe "#add_before_each_hook" do - it "adds a hook" do - hook_called = false - builder = Spectator::DSL::RootExampleGroupBuilder.new - builder.add_before_each_hook(->{ - hook_called = true - }) - group = builder.build(Spectator::Internals::SampleValues.empty) - group.run_before_hooks - hook_called.should eq(true) - end - - it "supports multiple hooks" do - call_count = 0 - builder = Spectator::DSL::RootExampleGroupBuilder.new - 5.times do |i| - builder.add_before_each_hook(->{ - call_count += i + 1 - }) - end - group = builder.build(Spectator::Internals::SampleValues.empty) - group.run_before_hooks - call_count.should eq(15) - end - end - - describe "#add_after_all_hook" do - it "adds a hook" do - hook_called = false - builder = Spectator::DSL::RootExampleGroupBuilder.new - builder.add_after_all_hook(->{ - hook_called = true - }) - group = builder.build(Spectator::Internals::SampleValues.empty) - group.run_after_hooks - hook_called.should eq(true) - end - - it "supports multiple hooks" do - call_count = 0 - builder = Spectator::DSL::RootExampleGroupBuilder.new - 5.times do |i| - builder.add_after_all_hook(->{ - call_count += i + 1 - }) - end - group = builder.build(Spectator::Internals::SampleValues.empty) - group.run_after_hooks - call_count.should eq(15) - end - end - - describe "#add_after_each_hook" do - it "adds a hook" do - hook_called = false - builder = Spectator::DSL::RootExampleGroupBuilder.new - builder.add_after_each_hook(->{ - hook_called = true - }) - group = builder.build(Spectator::Internals::SampleValues.empty) - group.run_after_hooks - hook_called.should eq(true) - end - - it "supports multiple hooks" do - call_count = 0 - builder = Spectator::DSL::RootExampleGroupBuilder.new - 5.times do |i| - builder.add_after_each_hook(->{ - call_count += i + 1 - }) - end - group = builder.build(Spectator::Internals::SampleValues.empty) - group.run_after_hooks - call_count.should eq(15) - end - end - - describe "#add_around_each_hook" do - it "adds a hook" do - hook_called = false - builder = Spectator::DSL::RootExampleGroupBuilder.new - builder.add_around_each_hook(->(_proc : ->) { - hook_called = true - }) - group = builder.build(Spectator::Internals::SampleValues.empty) - proc = group.wrap_around_each_hooks { } - proc.call - hook_called.should eq(true) - end - - it "supports multiple hooks" do - call_count = 0 - builder = Spectator::DSL::RootExampleGroupBuilder.new - 5.times do |i| - builder.add_around_each_hook(->(proc : ->) { - call_count += i + 1 - proc.call - }) - end - group = builder.build(Spectator::Internals::SampleValues.empty) - proc = group.wrap_around_each_hooks { } - proc.call - call_count.should eq(15) - end - end - - describe "#build" do - it "passes along the sample values" do - factory = Spectator::DSL::ExampleFactory.new(SpyExample) - builder = Spectator::DSL::RootExampleGroupBuilder.new - builder.add_child(factory) - values = Spectator::Internals::SampleValues.empty.add(:foo, "foo", 12345) - group = builder.build(values) - group.children.first.as(SpyExample).sample_values.should eq(values) - end - - it "specifies the parent of the children correctly" do - builder = Spectator::DSL::RootExampleGroupBuilder.new - 3.times do - factory = Spectator::DSL::ExampleFactory.new(PassingExample) - group_builder = Spectator::DSL::NestedExampleGroupBuilder.new("foo") - builder.add_child(factory) - builder.add_child(group_builder) - end - group = builder.build(Spectator::Internals::SampleValues.empty) - group.children.all? do |child| - case (child) - when Spectator::Example - child.group == group - when Spectator::NestedExampleGroup - child.parent == group - else - false - end - end.should be_true - end - end -end diff --git a/spec/dsl/sample_example_group_builder_spec.cr b/spec/dsl/sample_example_group_builder_spec.cr deleted file mode 100644 index a5a9d88..0000000 --- a/spec/dsl/sample_example_group_builder_spec.cr +++ /dev/null @@ -1,350 +0,0 @@ -require "../spec_helper" - -SAMPLE_VALUES_COLLECTION = %i[foo bar baz] - -struct SampleValueCollection - def initialize(sample_values : ::Spectator::Internals::SampleValues) - end - - def create - SAMPLE_VALUES_COLLECTION - end -end - -describe Spectator::DSL::SampleExampleGroupBuilder do - describe "#add_child" do - it "creates the correct number of children" do - create_proc = ->(s : SampleValueCollection) { s.create } - builder = Spectator::DSL::SampleExampleGroupBuilder.new("foobar", SampleValueCollection, create_proc, "value", :foo) - count = 4 - count.times do - factory = Spectator::DSL::ExampleFactory.new(PassingExample) - group_builder = Spectator::DSL::NestedExampleGroupBuilder.new("bar") - builder.add_child(factory) - builder.add_child(group_builder) - end - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - all_children = group.map { |child| child.as(Spectator::ExampleGroup).to_a }.flatten - all_children.size.should eq(2 * count * SAMPLE_VALUES_COLLECTION.size) - end - - context "with an ExampleFactory" do - it "creates an example for each item in the collection" do - create_proc = ->(s : SampleValueCollection) { s.create } - builder = Spectator::DSL::SampleExampleGroupBuilder.new("foobar", SampleValueCollection, create_proc, "value", :foo) - factory = Spectator::DSL::ExampleFactory.new(PassingExample) - builder.add_child(factory) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - all_children = group.map { |child| child.as(Spectator::ExampleGroup).to_a }.flatten - all_children.all? { |child| child.is_a?(PassingExample) }.should be_true - end - end - - context "with an ExampleGroupBuilder" do - it "creates a group for each item in the collection" do - create_proc = ->(s : SampleValueCollection) { s.create } - builder = Spectator::DSL::SampleExampleGroupBuilder.new("foobar", SampleValueCollection, create_proc, "value", :foo) - group_builder = Spectator::DSL::NestedExampleGroupBuilder.new("bar") - builder.add_child(group_builder) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - all_children = group.map { |child| child.as(Spectator::ExampleGroup).to_a }.flatten - all_children.all? { |child| child.is_a?(Spectator::NestedExampleGroup) }.should be_true - end - end - end - - describe "#add_before_all_hook" do - it "adds a hook" do - hook_called = false - create_proc = ->(s : SampleValueCollection) { s.create } - builder = Spectator::DSL::SampleExampleGroupBuilder.new("foobar", SampleValueCollection, create_proc, "value", :foo) - builder.add_before_all_hook(->{ - hook_called = true - }) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.run_before_hooks - hook_called.should eq(true) - end - - it "attachs the hook to just the top-level group" do - call_count = 0 - create_proc = ->(s : SampleValueCollection) { s.create } - builder = Spectator::DSL::SampleExampleGroupBuilder.new("foobar", SampleValueCollection, create_proc, "value", :foo) - builder.add_before_all_hook(->{ - call_count += 1 - }) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.children.map(&.as(Spectator::ExampleGroup)).each(&.run_before_hooks) - call_count.should eq(1) - end - - it "supports multiple hooks" do - call_count = 0 - create_proc = ->(s : SampleValueCollection) { s.create } - builder = Spectator::DSL::SampleExampleGroupBuilder.new("foobar", SampleValueCollection, create_proc, "value", :foo) - 5.times do |i| - builder.add_before_all_hook(->{ - call_count += i + 1 - }) - end - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.run_before_hooks - call_count.should eq(15) - end - end - - describe "#add_before_each_hook" do - it "adds a hook" do - hook_called = false - create_proc = ->(s : SampleValueCollection) { s.create } - builder = Spectator::DSL::SampleExampleGroupBuilder.new("foobar", SampleValueCollection, create_proc, "value", :foo) - builder.add_before_each_hook(->{ - hook_called = true - }) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.run_before_hooks - hook_called.should eq(true) - end - - it "attachs the hook to just the top-level group" do - call_count = 0 - create_proc = ->(s : SampleValueCollection) { s.create } - builder = Spectator::DSL::SampleExampleGroupBuilder.new("foobar", SampleValueCollection, create_proc, "value", :foo) - builder.add_before_each_hook(->{ - call_count += 1 - }) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.children.map(&.as(Spectator::ExampleGroup)).each(&.run_before_hooks) - call_count.should eq(SAMPLE_VALUES_COLLECTION.size) - end - - it "supports multiple hooks" do - call_count = 0 - create_proc = ->(s : SampleValueCollection) { s.create } - builder = Spectator::DSL::SampleExampleGroupBuilder.new("foobar", SampleValueCollection, create_proc, "value", :foo) - 5.times do |i| - builder.add_before_each_hook(->{ - call_count += i + 1 - }) - end - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.run_before_hooks - call_count.should eq(15) - end - end - - describe "#add_after_all_hook" do - it "adds a hook" do - hook_called = false - create_proc = ->(s : SampleValueCollection) { s.create } - builder = Spectator::DSL::SampleExampleGroupBuilder.new("foobar", SampleValueCollection, create_proc, "value", :foo) - builder.add_after_all_hook(->{ - hook_called = true - }) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.run_after_hooks - hook_called.should eq(true) - end - - it "attachs the hook to just the top-level group" do - call_count = 0 - create_proc = ->(s : SampleValueCollection) { s.create } - builder = Spectator::DSL::SampleExampleGroupBuilder.new("foobar", SampleValueCollection, create_proc, "value", :foo) - builder.add_after_all_hook(->{ - call_count += 1 - }) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.children.map(&.as(Spectator::ExampleGroup)).each(&.run_after_hooks) - call_count.should eq(1) - end - - it "supports multiple hooks" do - call_count = 0 - create_proc = ->(s : SampleValueCollection) { s.create } - builder = Spectator::DSL::SampleExampleGroupBuilder.new("foobar", SampleValueCollection, create_proc, "value", :foo) - 5.times do |i| - builder.add_after_all_hook(->{ - call_count += i + 1 - }) - end - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.run_after_hooks - call_count.should eq(15) - end - end - - describe "#add_after_each_hook" do - it "adds a hook" do - hook_called = false - create_proc = ->(s : SampleValueCollection) { s.create } - builder = Spectator::DSL::SampleExampleGroupBuilder.new("foobar", SampleValueCollection, create_proc, "value", :foo) - builder.add_after_each_hook(->{ - hook_called = true - }) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.run_after_hooks - hook_called.should eq(true) - end - - it "attachs the hook to just the top-level group" do - call_count = 0 - create_proc = ->(s : SampleValueCollection) { s.create } - builder = Spectator::DSL::SampleExampleGroupBuilder.new("foobar", SampleValueCollection, create_proc, "value", :foo) - builder.add_after_each_hook(->{ - call_count += 1 - }) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.children.map(&.as(Spectator::ExampleGroup)).each(&.run_after_hooks) - call_count.should eq(SAMPLE_VALUES_COLLECTION.size) - end - - it "supports multiple hooks" do - call_count = 0 - create_proc = ->(s : SampleValueCollection) { s.create } - builder = Spectator::DSL::SampleExampleGroupBuilder.new("foobar", SampleValueCollection, create_proc, "value", :foo) - 5.times do |i| - builder.add_after_each_hook(->{ - call_count += i + 1 - }) - end - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.run_after_hooks - call_count.should eq(15) - end - end - - describe "#build" do - it "passes along the what value" do - what = "TEST" - create_proc = ->(s : SampleValueCollection) { s.create } - builder = Spectator::DSL::SampleExampleGroupBuilder.new(what, SampleValueCollection, create_proc, "value", :foo) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.what.should eq(what) - end - - it "passes along the parent" do - factory = Spectator::DSL::ExampleFactory.new(SpyExample) - create_proc = ->(s : SampleValueCollection) { s.create } - builder = Spectator::DSL::SampleExampleGroupBuilder.new("foobar", SampleValueCollection, create_proc, "value", :foo) - builder.add_child(factory) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.parent.should be(root) - end - - it "passes along the sample values" do - factory = Spectator::DSL::ExampleFactory.new(SpyExample) - create_proc = ->(s : SampleValueCollection) { s.create } - builder = Spectator::DSL::SampleExampleGroupBuilder.new("foobar", SampleValueCollection, create_proc, "value", :foo) - builder.add_child(factory) - symbol = :test - values = Spectator::Internals::SampleValues.empty.add(symbol, "foo", 12345) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, values) - all_children = group.map { |child| child.as(Spectator::ExampleGroup).to_a }.flatten - all_children.map(&.as(SpyExample)).all? { |child| child.sample_values.get_wrapper(symbol) }.should be_true - end - - it "passes along the value name" do - symbol = :foo - name = "value" - factory = Spectator::DSL::ExampleFactory.new(SpyExample) - create_proc = ->(s : SampleValueCollection) { s.create } - builder = Spectator::DSL::SampleExampleGroupBuilder.new("foobar", SampleValueCollection, create_proc, name, symbol) - builder.add_child(factory) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - all_children = group.map { |child| child.as(Spectator::ExampleGroup).to_a }.flatten - all_children.each do |child| - entries = child.as(SpyExample).sample_values.map(&.name) - entries.should contain(name) - end - end - - it "creates the correct number of sub-groups" do - create_proc = ->(s : SampleValueCollection) { s.create } - builder = Spectator::DSL::SampleExampleGroupBuilder.new("foobar", SampleValueCollection, create_proc, "value", :foo) - factory = Spectator::DSL::ExampleFactory.new(PassingExample) - builder.add_child(factory) - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.children.size.should eq(SAMPLE_VALUES_COLLECTION.size) - end - - it "passes the correct value to each sub-group" do - factory = Spectator::DSL::ExampleFactory.new(SpyExample) - symbol = :test - count = 3 - expected = Array.new(SAMPLE_VALUES_COLLECTION.size * count) { |i| SAMPLE_VALUES_COLLECTION[i // count] } - create_proc = ->(s : SampleValueCollection) { s.create } - builder = Spectator::DSL::SampleExampleGroupBuilder.new("foobar", SampleValueCollection, create_proc, "value", symbol) - count.times { builder.add_child(factory) } - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - all_children = group.map { |child| child.as(Spectator::ExampleGroup).to_a }.flatten - all_children.map { |child| child.as(SpyExample).sample_values.get_value(symbol, typeof(SAMPLE_VALUES_COLLECTION.first)) }.should eq(expected) - end - - it "specifies the parent of the children correctly" do - create_proc = ->(s : SampleValueCollection) { s.create } - builder = Spectator::DSL::SampleExampleGroupBuilder.new("foobar", SampleValueCollection, create_proc, "value", :foo) - 3.times do - factory = Spectator::DSL::ExampleFactory.new(PassingExample) - group_builder = Spectator::DSL::NestedExampleGroupBuilder.new("baz") - builder.add_child(factory) - builder.add_child(group_builder) - end - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.children.all? do |child| - case (child) - when Spectator::Example - child.group == group - when Spectator::NestedExampleGroup - child.parent == group - else - false - end - end.should be_true - end - - it "specifies the container for the parent of the sub-groups" do - create_proc = ->(s : SampleValueCollection) { s.create } - builder = Spectator::DSL::SampleExampleGroupBuilder.new("foobar", SampleValueCollection, create_proc, "value", :foo) - 3.times do - factory = Spectator::DSL::ExampleFactory.new(PassingExample) - group_builder = Spectator::DSL::NestedExampleGroupBuilder.new("baz") - builder.add_child(factory) - builder.add_child(group_builder) - end - root = Spectator::DSL::RootExampleGroupBuilder.new.build(Spectator::Internals::SampleValues.empty) - group = builder.build(root, Spectator::Internals::SampleValues.empty) - group.children.all? do |child| - case (child) - when Spectator::Example - child.group == group - when Spectator::NestedExampleGroup - child.parent == group - else - false - end - end.should be_true - end - end -end diff --git a/spec/errored_result_spec.cr b/spec/errored_result_spec.cr deleted file mode 100644 index 4b5f524..0000000 --- a/spec/errored_result_spec.cr +++ /dev/null @@ -1,87 +0,0 @@ -require "./spec_helper" - -def new_errored_result( - example : Spectator::Example? = nil, - elapsed : Time::Span? = nil, - expectations : Spectator::Expectations::ExampleExpectations? = nil, - error : Exception? = nil -) - Spectator::ErroredResult.new( - example || FailingExample.create, - elapsed || Time::Span.zero, - expectations || Spectator::Expectations::ExampleExpectations.new(generate_expectations(0, 1)[:expectations]), - error || Exception.new("foobar") - ) -end - -describe Spectator::ErroredResult do - describe "#call" do - context "without a block" do - it "invokes #error on an instance" do - spy = ResultCallSpy.new - new_errored_result.call(spy) - spy.error?.should be_true - end - - it "returns the value of #failure" do - result = new_errored_result - returned = result.call(ResultCallSpy.new) - returned.should eq(:error) - end - end - - context "with a block" do - it "invokes #error on an instance" do - spy = ResultCallSpy.new - new_errored_result.call(spy) { nil } - spy.error?.should be_true - end - - it "yields itself" do - result = new_errored_result - value = nil.as(Spectator::Result?) - result.call(ResultCallSpy.new) { |r| value = r } - value.should eq(result) - end - - it "returns the value of #failure" do - result = new_errored_result - value = 42 - returned = result.call(ResultCallSpy.new) { value } - returned.should eq(value) - end - end - end - - describe "#example" do - it "is the expected value" do - example = FailingExample.create - result = new_errored_result(example: example) - result.example.should eq(example) - end - end - - describe "#elapsed" do - it "is the expected value" do - elapsed = Time::Span.new(10, 10, 10) - result = new_errored_result(elapsed: elapsed) - result.elapsed.should eq(elapsed) - end - end - - describe "#expectations" do - it "is the expected value" do - expectations = Spectator::Expectations::ExampleExpectations.new(generate_expectations(5, 1)[:expectations]) - result = new_errored_result(expectations: expectations) - result.expectations.should eq(expectations) - end - end - - describe "#error" do - it "is the expected value" do - error = IO::Error.new("oops") - result = new_errored_result(error: error) - result.error.should eq(error) - end - end -end diff --git a/spec/example_conditions_spec.cr b/spec/example_conditions_spec.cr deleted file mode 100644 index 3398f02..0000000 --- a/spec/example_conditions_spec.cr +++ /dev/null @@ -1,36 +0,0 @@ -require "./spec_helper" - -describe Spectator::ExampleConditions do - {% for condition in %i[pre post] %} - describe "#run_{{condition.id}}_conditions" do - it "calls a proc" do - called = false - conditions = new_conditions({{condition.id}}: ->{ called = true; nil }) - conditions.run_{{condition.id}}_conditions - called.should be_true - end - - it "calls multiple procs" do - call_count = 0 - conditions = new_conditions({{condition.id}}: [ - ->{ call_count += 1; nil }, - ->{ call_count += 2; nil }, - ->{ call_count += 3; nil }, - ]) - conditions.run_{{condition.id}}_conditions - call_count.should eq(6) - end - - it "calls procs in the correct order" do - calls = [] of Symbol - conditions = new_conditions({{condition.id}}: [ - ->{ calls << :a; nil }, - ->{ calls << :b; nil }, - ->{ calls << :c; nil }, - ]) - conditions.run_{{condition.id}}_conditions - calls.should eq(\%i[a b c]) - end - end - {% end %} -end diff --git a/spec/example_hooks_spec.cr b/spec/example_hooks_spec.cr deleted file mode 100644 index 9fc8798..0000000 --- a/spec/example_hooks_spec.cr +++ /dev/null @@ -1,79 +0,0 @@ -require "./spec_helper" - -describe Spectator::ExampleHooks do - {% for hook in %i[before_all before_each after_all after_each] %} - describe "#run_{{hook.id}}" do - it "calls a proc" do - called = false - hooks = new_hooks({{hook.id}}: ->{ called = true; nil }) - hooks.run_{{hook.id}} - called.should be_true - end - - it "calls multiple procs" do - call_count = 0 - hooks = new_hooks({{hook.id}}: [ - ->{ call_count += 1; nil }, - ->{ call_count += 2; nil }, - ->{ call_count += 3; nil }, - ]) - hooks.run_{{hook.id}} - call_count.should eq(6) - end - - it "calls procs in the correct order" do - calls = [] of Symbol - hooks = new_hooks({{hook.id}}: [ - ->{ calls << :a; nil }, - ->{ calls << :b; nil }, - ->{ calls << :c; nil }, - ]) - hooks.run_{{hook.id}} - calls.should eq(\%i[a b c]) - end - end - {% end %} - - describe "#wrap_around_each" do - it "wraps the block" do - called = false - wrapper = new_hooks.wrap_around_each do - called = true - end - wrapper.call - called.should be_true - end - - it "wraps a proc" do - called = false - hooks = new_hooks(around_each: ->(proc : ->) { called = true; proc.call }) - wrapper = hooks.wrap_around_each { } - wrapper.call - called.should be_true - end - - it "wraps multiple procs" do - call_count = 0 - hooks = new_hooks(around_each: [ - ->(proc : ->) { call_count += 1; proc.call }, - ->(proc : ->) { call_count += 2; proc.call }, - ->(proc : ->) { call_count += 3; proc.call }, - ]) - wrapper = hooks.wrap_around_each { } - wrapper.call - call_count.should eq(6) - end - - it "wraps procs in the correct order" do - calls = [] of Symbol - hooks = new_hooks(around_each: [ - ->(proc : ->) { calls << :a; proc.call }, - ->(proc : ->) { calls << :b; proc.call }, - ->(proc : ->) { calls << :c; proc.call }, - ]) - wrapper = hooks.wrap_around_each { } - wrapper.call - calls.should eq(%i[a b c]) - end - end -end diff --git a/spec/example_iterator_spec.cr b/spec/example_iterator_spec.cr deleted file mode 100644 index e7829f4..0000000 --- a/spec/example_iterator_spec.cr +++ /dev/null @@ -1,205 +0,0 @@ -require "./spec_helper" - -describe Spectator::ExampleIterator do - describe "#next" do - context "with one example" do - it "returns the example" do - example = PassingExample.create - iterator = Spectator::ExampleIterator.new(example.group) - iterator.next.should eq(example) - end - - it "returns 'stop' after the example" do - example = PassingExample.create - iterator = Spectator::ExampleIterator.new(example.group) - iterator.next # Should return example. - iterator.next.should be_a(Iterator::Stop) - end - end - - context "when empty" do - it "returns 'stop'" do - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group.children = [] of Spectator::ExampleComponent - iterator = Spectator::ExampleIterator.new(group) - iterator.next.should be_a(Iterator::Stop) - end - end - - context "with one level of examples" do - it "iterates through all examples" do - examples = [] of Spectator::Example - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group.children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(group, Spectator::Internals::SampleValues.empty) - end - iterator = Spectator::ExampleIterator.new(group) - 5.times { examples << iterator.next.as(Spectator::Example) } - examples.should eq(group.children) - end - - it "returns 'stop' at the end" do - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group.children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(group, Spectator::Internals::SampleValues.empty) - end - iterator = Spectator::ExampleIterator.new(group) - 5.times { iterator.next } - iterator.next.should be_a(Iterator::Stop) - end - end - - context "with empty sub-groups" do - context "one level deep" do - it "returns 'stop'" do - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group.children = Array(Spectator::ExampleComponent).new(5) do |i| - Spectator::NestedExampleGroup.new(i.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group| - sub_group.children = [] of Spectator::ExampleComponent - end - end - iterator = Spectator::ExampleIterator.new(group) - iterator.next.should be_a(Iterator::Stop) - end - end - - context "multiple levels deep" do - it "returns 'stop'" do - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group.children = Array(Spectator::ExampleComponent).new(5) do |i| - Spectator::NestedExampleGroup.new(i.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group| - sub_group.children = Array(Spectator::ExampleComponent).new(5) do |j| - Spectator::NestedExampleGroup.new(j.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_sub_group| - sub_sub_group.children = [] of Spectator::ExampleComponent - end - end - end - end - iterator = Spectator::ExampleIterator.new(group) - iterator.next.should be_a(Iterator::Stop) - end - end - end - - context "with multiple levels of examples" do - it "iterates through all examples" do - actual_examples = [] of Spectator::Example - expected_examples = [] of Spectator::Example - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group.children = Array(Spectator::ExampleComponent).new(5) do |i| - if i % 2 == 0 - PassingExample.new(group, Spectator::Internals::SampleValues.empty).tap do |example| - expected_examples << example - end - else - Spectator::NestedExampleGroup.new(i.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group| - sub_group.children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(sub_group, Spectator::Internals::SampleValues.empty).tap do |example| - expected_examples << example - end - end - end - end - end - iterator = Spectator::ExampleIterator.new(group) - 13.times { actual_examples << iterator.next.as(Spectator::Example) } - actual_examples.should eq(expected_examples) - end - - it "returns 'stop' at the end" do - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group.children = Array(Spectator::ExampleComponent).new(5) do |i| - if i % 2 == 0 - PassingExample.new(group, Spectator::Internals::SampleValues.empty) - else - Spectator::NestedExampleGroup.new(i.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group| - sub_group.children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(sub_group, Spectator::Internals::SampleValues.empty) - end - end - end - end - iterator = Spectator::ExampleIterator.new(group) - 13.times { iterator.next } - iterator.next.should be_a(Iterator::Stop) - end - end - - context "with deep nesting" do - # Sorry for this atrocity, - # but it was fun to write. - it "iterates through all examples" do - actual_examples = [] of Spectator::Example - expected_examples = [] of Spectator::Example - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group.children = Array(Spectator::ExampleComponent).new(5) do |i| - Spectator::NestedExampleGroup.new(i.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group1| - sub_group1.children = Array(Spectator::ExampleComponent).new(5) do |j| - Spectator::NestedExampleGroup.new(j.to_s, sub_group1, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group2| - sub_group2.children = Array(Spectator::ExampleComponent).new(5) do |k| - Spectator::NestedExampleGroup.new(k.to_s, sub_group2, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group3| - sub_group3.children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(sub_group3, Spectator::Internals::SampleValues.empty).tap do |example| - expected_examples << example - end - end - end - end - end - end - end - end - iterator = Spectator::ExampleIterator.new(group) - (5 ** 4).times { actual_examples << iterator.next.as(Spectator::Example) } - actual_examples.should eq(expected_examples) - end - - it "returns 'stop' at the end" do - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group.children = Array(Spectator::ExampleComponent).new(5) do |i| - Spectator::NestedExampleGroup.new(i.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group1| - sub_group1.children = Array(Spectator::ExampleComponent).new(5) do |j| - Spectator::NestedExampleGroup.new(j.to_s, sub_group1, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group2| - sub_group2.children = Array(Spectator::ExampleComponent).new(5) do |k| - Spectator::NestedExampleGroup.new(k.to_s, sub_group2, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group3| - sub_group3.children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(sub_group3, Spectator::Internals::SampleValues.empty) - end - end - end - end - end - end - end - iterator = Spectator::ExampleIterator.new(group) - (5 ** 4).times { iterator.next } - iterator.next.should be_a(Iterator::Stop) - end - end - - it "returns 'stop' after the end has been reached" do - example = PassingExample.create - iterator = Spectator::ExampleIterator.new(example.group) - iterator.next # Should return example. - iterator.next # Should return "stop". - iterator.next.should be_a(Iterator::Stop) # Should still return "stop". - end - end - - describe "#rewind" do - it "restarts the iterator" do - example = PassingExample.create - iterator = Spectator::ExampleIterator.new(example.group) - iterator.next - iterator.rewind - iterator.next.should eq(example) - end - - it "can be called before #next" do - example = PassingExample.create - iterator = Spectator::ExampleIterator.new(example.group) - iterator.rewind - iterator.next.should eq(example) - end - end -end diff --git a/spec/expectation_failed_spec.cr b/spec/expectation_failed_spec.cr deleted file mode 100644 index 54c5d1d..0000000 --- a/spec/expectation_failed_spec.cr +++ /dev/null @@ -1,19 +0,0 @@ -require "./spec_helper" - -describe Spectator::ExpectationFailed do - describe "#expectation" do - it "contains the expected value" do - expectation = new_unsatisfied_expectation - error = Spectator::ExpectationFailed.new(expectation) - error.expectation.should eq(expectation) - end - end - - describe "#message" do - it "is the same as the expectation's #actual_message" do - expectation = new_unsatisfied_expectation - error = Spectator::ExpectationFailed.new(expectation) - error.message.should eq(expectation.failure_message) - end - end -end diff --git a/spec/expectations/example_expectations_spec.cr b/spec/expectations/example_expectations_spec.cr deleted file mode 100644 index 0b55ab9..0000000 --- a/spec/expectations/example_expectations_spec.cr +++ /dev/null @@ -1,207 +0,0 @@ -require "../spec_helper" - -describe Spectator::Expectations::ExampleExpectations do - describe "#each" do - it "yields all expectations" do - tuple = generate_expectations(5, 5) - expectations = [] of Spectator::Expectations::Expectation - tuple[:reporter].expectations.each { |expectation| expectations << expectation } - # Expectations might not be in the same order. - # Just check if if the arrays contain the same items. - expectations.size.should eq(tuple[:expectations].size) - (expectations - tuple[:expectations]).empty?.should be_true - end - end - - describe "#satisfied" do - it "returns only satisfied expectations" do - tuple = generate_expectations(5, 5) - expectations = tuple[:reporter].expectations - expectations.satisfied.all?(&.satisfied?).should be_true - end - - it "returns the correct expectations" do - tuple = generate_expectations(5, 5) - expectations = tuple[:reporter].expectations - satisfied = expectations.satisfied.to_a - satisfied.size.should eq(5) - (satisfied - tuple[:satisfied]).empty?.should be_true - end - - context "with all satisfied expectations" do - it "returns all expectations" do - tuple = generate_expectations(5, 0) - expectations = tuple[:reporter].expectations - expectations.satisfied.size.should eq(tuple[:satisfied].size) - end - end - - context "with all unsatisfied expectations" do - it "returns an empty collection" do - tuple = generate_expectations(0, 5) - expectations = tuple[:reporter].expectations - expectations.satisfied.size.should eq(0) - end - end - end - - describe "#each_satisfied" do - it "yields only satisfied expectations" do - tuple = generate_expectations(5, 5) - expectations = [] of Spectator::Expectations::Expectation - tuple[:reporter].expectations.each_satisfied { |expectation| expectations << expectation } - # Expectations might not be in the same order. - # Just check if if the arrays contain the same items. - expectations.size.should eq(tuple[:satisfied].size) - (expectations - tuple[:satisfied]).empty?.should be_true - end - - context "with all satisfied expectations" do - it "yields all expectations" do - tuple = generate_expectations(0, 5) - expectations = [] of Spectator::Expectations::Expectation - tuple[:reporter].expectations.each_satisfied { |expectation| expectations << expectation } - expectations.size.should eq(tuple[:satisfied].size) - end - end - - context "with all unsatisfied expectations" do - it "yields nothing" do - tuple = generate_expectations(0, 5) - expectations = [] of Spectator::Expectations::Expectation - tuple[:reporter].expectations.each_satisfied { |expectation| expectations << expectation } - expectations.empty?.should be_true - end - end - end - - describe "#unsatisfied" do - it "returns only unsatisfied expectations" do - tuple = generate_expectations(5, 5) - expectations = tuple[:reporter].expectations - expectations.unsatisfied.all?(&.satisfied?).should be_false - end - - it "returns the correct expectations" do - tuple = generate_expectations(5, 5) - expectations = tuple[:reporter].expectations - unsatisfied = expectations.unsatisfied.to_a - unsatisfied.size.should eq(5) - (unsatisfied - tuple[:unsatisfied]).empty?.should be_true - end - - context "with all satisfied expectations" do - it "returns an empty collection" do - tuple = generate_expectations(5, 0) - expectations = tuple[:reporter].expectations - expectations.unsatisfied.size.should eq(0) - end - end - - context "with all unsatisfied expectations" do - it "returns all expectations" do - tuple = generate_expectations(0, 5) - expectations = tuple[:reporter].expectations - expectations.unsatisfied.size.should eq(tuple[:unsatisfied].size) - end - end - end - - describe "#each_unsatisfied" do - it "yields only unsatisfied expectations" do - tuple = generate_expectations(5, 5) - expectations = [] of Spectator::Expectations::Expectation - tuple[:reporter].expectations.each_unsatisfied { |expectation| expectations << expectation } - # Expectations might not be in the same order. - # Just check if if the arrays contain the same items. - expectations.size.should eq(tuple[:unsatisfied].size) - (expectations - tuple[:unsatisfied]).empty?.should be_true - end - - context "with all satisfied expectations" do - it "yields nothing" do - tuple = generate_expectations(5, 0) - expectations = [] of Spectator::Expectations::Expectation - tuple[:reporter].expectations.each_unsatisfied { |expectation| expectations << expectation } - expectations.empty?.should be_true - end - end - - context "with all unsatisfied expectations" do - it "yields all expectations" do - tuple = generate_expectations(0, 5) - expectations = [] of Spectator::Expectations::Expectation - tuple[:reporter].expectations.each_unsatisfied { |expectation| expectations << expectation } - expectations.size.should eq(tuple[:unsatisfied].size) - end - end - end - - describe "#successful?" do - context "with all satisfied expectations" do - it "is true" do - tuple = generate_expectations(5, 0) - expectations = tuple[:reporter].expectations - expectations.successful?.should be_true - end - end - - context "with one unsatisfied expectation" do - it "is false" do - tuple = generate_expectations(5, 1) - expectations = tuple[:reporter].expectations - expectations.successful?.should be_false - end - end - - context "with one satisfied expectation" do - it "is false" do - tuple = generate_expectations(1, 5) - expectations = tuple[:reporter].expectations - expectations.successful?.should be_false - end - end - - context "with all unsatisfied expectations" do - it "is false" do - tuple = generate_expectations(0, 5) - expectations = tuple[:reporter].expectations - expectations.successful?.should be_false - end - end - end - - describe "#failed?" do - context "with all satisfied expectations" do - it "is false" do - tuple = generate_expectations(5, 0) - expectations = tuple[:reporter].expectations - expectations.failed?.should be_false - end - end - - context "with one unsatisfied expectation" do - it "is true" do - tuple = generate_expectations(5, 1) - expectations = tuple[:reporter].expectations - expectations.failed?.should be_true - end - end - - context "with one satisfied expectation" do - it "is true" do - tuple = generate_expectations(1, 5) - expectations = tuple[:reporter].expectations - expectations.failed?.should be_true - end - end - - context "with all unsatisfied expectations" do - it "is true" do - tuple = generate_expectations(0, 5) - expectations = tuple[:reporter].expectations - expectations.failed?.should be_true - end - end - end -end diff --git a/spec/expectations/expectation_reporter_spec.cr b/spec/expectations/expectation_reporter_spec.cr deleted file mode 100644 index 0f32281..0000000 --- a/spec/expectations/expectation_reporter_spec.cr +++ /dev/null @@ -1,80 +0,0 @@ -require "../spec_helper" - -describe Spectator::Expectations::ExpectationReporter do - describe "#report" do - context "with raise flag set" do - context "given a satisfied expectation" do - it "stores the result" do - expectation = new_satisfied_expectation - reporter = Spectator::Expectations::ExpectationReporter.new(true) - reporter.report(expectation) - reporter.expectations.should contain(expectation) - end - end - - context "given a unsatisfied expectation" do - it "raises and error" do - expectation = new_unsatisfied_expectation - reporter = Spectator::Expectations::ExpectationReporter.new(true) - expect_raises(Spectator::ExpectationFailed) { reporter.report(expectation) } - end - - it "stores the expectation" do - expectation = new_unsatisfied_expectation - reporter = Spectator::Expectations::ExpectationReporter.new(true) - begin - reporter.report(expectation) - rescue - # Ignore error, not testing that in this example. - end - reporter.expectations.should contain(expectation) - end - end - end - - context "with raise flag not set" do - context "given a satisfied expectation" do - it "stores the expectation" do - expectation = new_satisfied_expectation - reporter = Spectator::Expectations::ExpectationReporter.new(false) - reporter.report(expectation) - reporter.expectations.should contain(expectation) - end - end - - context "given a unsatisfied expectation" do - it "stores the expectation" do - expectation = new_unsatisfied_expectation - reporter = Spectator::Expectations::ExpectationReporter.new(false) - reporter.report(expectation) - reporter.expectations.should contain(expectation) - end - end - end - end - - describe "#expectations" do - context "with no expectations" do - it "is empty" do - reporter = Spectator::Expectations::ExpectationReporter.new - reporter.expectations.size.should eq(0) - end - end - - context "with multiple expectations" do - it "contains all expectations" do - expectation1 = new_satisfied_expectation - expectation2 = new_unsatisfied_expectation - reporter = Spectator::Expectations::ExpectationReporter.new(false) - begin - reporter.report(expectation1) - reporter.report(expectation2) - rescue - # Ignore errors for this test. - end - reporter.expectations.should contain(expectation1) - reporter.expectations.should contain(expectation2) - end - end - end -end diff --git a/spec/expectations/expectation_spec.cr b/spec/expectations/expectation_spec.cr deleted file mode 100644 index 06540fc..0000000 --- a/spec/expectations/expectation_spec.cr +++ /dev/null @@ -1,51 +0,0 @@ -require "../spec_helper" - -describe Spectator::Expectations::Expectation do - describe "#satisifed?" do - context "with a successful match" do - it "is true" do - value = 42 - matcher = new_matcher(value) - partial = new_partial(value) - match_data = matcher.match(partial.actual) - match_data.matched?.should be_true # Sanity check. - expectation = Spectator::Expectations::Expectation.new(match_data, partial.source) - expectation.satisfied?.should be_true - end - - context "when negated" do - it "is false" do - value = 42 - matcher = new_matcher(value) - partial = new_partial(value) - match_data = matcher.negated_match(partial.actual) - match_data.matched?.should be_false # Sanity check. - expectation = Spectator::Expectations::Expectation.new(match_data, partial.source) - expectation.satisfied?.should be_false - end - end - end - - context "with an unsuccessful match" do - it "is false" do - matcher = new_matcher(42) - partial = new_partial(777) - match_data = matcher.match(partial.actual) - match_data.matched?.should be_false # Sanity check. - expectation = Spectator::Expectations::Expectation.new(match_data, partial.source) - expectation.satisfied?.should be_false - end - - context "when negated" do - it "is true" do - matcher = new_matcher(42) - partial = new_partial(777) - match_data = matcher.negated_match(partial.actual) - match_data.matched?.should be_true # Sanity check. - expectation = Spectator::Expectations::Expectation.new(match_data, partial.source) - expectation.satisfied?.should be_true - end - end - end - end -end diff --git a/spec/failed_result_spec.cr b/spec/failed_result_spec.cr deleted file mode 100644 index 34b557d..0000000 --- a/spec/failed_result_spec.cr +++ /dev/null @@ -1,87 +0,0 @@ -require "./spec_helper" - -def new_failed_result( - example : Spectator::Example? = nil, - elapsed : Time::Span? = nil, - expectations : Spectator::Expectations::ExampleExpectations? = nil, - error : Exception? = nil -) - Spectator::FailedResult.new( - example || FailingExample.create, - elapsed || Time::Span.zero, - expectations || Spectator::Expectations::ExampleExpectations.new(generate_expectations(0, 1)[:expectations]), - error || Exception.new("foobar") - ) -end - -describe Spectator::FailedResult do - describe "#call" do - context "without a block" do - it "invokes #failure on an instance" do - spy = ResultCallSpy.new - new_failed_result.call(spy) - spy.failure?.should be_true - end - - it "returns the value of #failure" do - result = new_failed_result - returned = result.call(ResultCallSpy.new) - returned.should eq(:failure) - end - end - - context "with a block" do - it "invokes #failure on an instance" do - spy = ResultCallSpy.new - new_failed_result.call(spy) { nil } - spy.failure?.should be_true - end - - it "yields itself" do - result = new_failed_result - value = nil.as(Spectator::Result?) - result.call(ResultCallSpy.new) { |r| value = r } - value.should eq(result) - end - - it "returns the value of #failure" do - result = new_failed_result - value = 42 - returned = result.call(ResultCallSpy.new) { value } - returned.should eq(value) - end - end - end - - describe "#example" do - it "is the expected value" do - example = FailingExample.create - result = new_failed_result(example: example) - result.example.should eq(example) - end - end - - describe "#elapsed" do - it "is the expected value" do - elapsed = Time::Span.new(10, 10, 10) - result = new_failed_result(elapsed: elapsed) - result.elapsed.should eq(elapsed) - end - end - - describe "#expectations" do - it "is the expected value" do - expectations = Spectator::Expectations::ExampleExpectations.new(generate_expectations(5, 1)[:expectations]) - result = new_failed_result(expectations: expectations) - result.expectations.should eq(expectations) - end - end - - describe "#error" do - it "is the expected value" do - error = IO::Error.new("oops") - result = new_failed_result(error: error) - result.error.should eq(error) - end - end -end diff --git a/spec/fomatting/color_spec.cr b/spec/fomatting/color_spec.cr deleted file mode 100644 index 406b5db..0000000 --- a/spec/fomatting/color_spec.cr +++ /dev/null @@ -1,93 +0,0 @@ -require "../spec_helper" - -describe Spectator::Formatting::Color do - describe "#success" do - it "includes the input text" do - text = "foobar" - output = Spectator::Formatting::Color.success(text) - output.to_s.should contain(text) - end - - it "prefixes with green markers" do - output = Spectator::Formatting::Color.success("foobar") - output.to_s.should start_with("\e[32m") - end - - it "appends color reset markers" do - output = Spectator::Formatting::Color.success("foobar") - output.to_s.should end_with("\e[0m") - end - end - - describe "#failure" do - it "includes the input text" do - text = "foobar" - output = Spectator::Formatting::Color.failure(text) - output.to_s.should contain(text) - end - - it "prefixes with green markers" do - output = Spectator::Formatting::Color.failure("foobar") - output.to_s.should start_with("\e[31m") - end - - it "appends color reset markers" do - output = Spectator::Formatting::Color.failure("foobar") - output.to_s.should end_with("\e[0m") - end - end - - describe "#error" do - it "includes the input text" do - text = "foobar" - output = Spectator::Formatting::Color.error(text) - output.to_s.should contain(text) - end - - it "prefixes with green markers" do - output = Spectator::Formatting::Color.error("foobar") - output.to_s.should start_with("\e[35m") - end - - it "appends color reset markers" do - output = Spectator::Formatting::Color.error("foobar") - output.to_s.should end_with("\e[0m") - end - end - - describe "#pending" do - it "includes the input text" do - text = "foobar" - output = Spectator::Formatting::Color.pending(text) - output.to_s.should contain(text) - end - - it "prefixes with green markers" do - output = Spectator::Formatting::Color.pending("foobar") - output.to_s.should start_with("\e[33m") - end - - it "appends color reset markers" do - output = Spectator::Formatting::Color.pending("foobar") - output.to_s.should end_with("\e[0m") - end - end - - describe "#comment" do - it "includes the input text" do - text = "foobar" - output = Spectator::Formatting::Color.comment(text) - output.to_s.should contain(text) - end - - it "prefixes with green markers" do - output = Spectator::Formatting::Color.comment("foobar") - output.to_s.should start_with("\e[36m") - end - - it "appends color reset markers" do - output = Spectator::Formatting::Color.comment("foobar") - output.to_s.should end_with("\e[0m") - end - end -end diff --git a/spec/helpers/errored_example.cr b/spec/helpers/errored_example.cr deleted file mode 100644 index aa82617..0000000 --- a/spec/helpers/errored_example.cr +++ /dev/null @@ -1,37 +0,0 @@ -# Example that always raises an exception. -class ErroredExample < Spectator::RunnableExample - # Dummy description. - def what : Symbol | String - "ERROR" - end - - # Dummy source. - def source : ::Spectator::Source - ::Spectator::Source.new(__FILE__, __LINE__) - end - - # Dummy symbolic flag. - def symbolic? : Bool - false - end - - # Dummy instance. - def instance - nil - end - - # Run the example that always produces an error. - private def run_instance - raise "Oops" - end - - # Creates an errored example. - def self.create - hooks = Spectator::ExampleHooks.empty - group = Spectator::RootExampleGroup.new(hooks) - values = Spectator::Internals::SampleValues.empty - new(group, values).tap do |example| - group.children = [example.as(Spectator::ExampleComponent)] - end - end -end diff --git a/spec/helpers/example_hooks_helper.cr b/spec/helpers/example_hooks_helper.cr deleted file mode 100644 index afdf537..0000000 --- a/spec/helpers/example_hooks_helper.cr +++ /dev/null @@ -1,59 +0,0 @@ -# Creates a new `Spectator::ExampleHooks` instance. -# All arguments are optional, -# only specify the sets of hooks that are needed. -# The hooks that aren't specified will be left empty. -def new_hooks( - before_all = [] of ->, - before_each = [] of ->, - after_all = [] of ->, - after_each = [] of ->, - around_each = [] of Proc(Nil) -> -) - Spectator::ExampleHooks.new(before_all, before_each, - after_all, after_each, around_each) -end - -# Creates a new `Spectator::ExampleHooks` instance. -# All arguments are optional, -# only specify a hook for the types that are needed. -# The hooks that aren't specified will be left empty. -def new_hooks( - before_all : Proc(Nil)? = nil, - before_each : Proc(Nil)? = nil, - after_all : Proc(Nil)? = nil, - after_each : Proc(Nil)? = nil, - around_each : Proc(Proc(Nil), Nil)? = nil -) - new_hooks( - before_all ? [before_all] : [] of ->, - before_each ? [before_each] : [] of ->, - after_all ? [after_all] : [] of ->, - after_each ? [after_each] : [] of ->, - around_each ? [around_each] : [] of Proc(Nil) -> - ) -end - -# Creates a new `Spectator::ExampleConditions` instance. -# All arguments are optional, -# only specify the sets of conditions that are needed. -# The conditions that aren't specified will be left empty. -def new_conditions( - pre = [] of ->, - post = [] of -> -) - Spectator::ExampleConditions.new(pre, post) -end - -# Creates a new `Spectator::ExampleConditions` instance. -# All arguments are optional, -# only specify a condition for the types that are needed. -# The conditions that aren't specified will be left empty. -def new_conditions( - pre : Proc(Nil)? = nil, - post : Proc(Nil)? = nil -) - new_conditions( - pre ? [pre] : [] of ->, - post ? [post] : [] of -> - ) -end diff --git a/spec/helpers/expectations_helper.cr b/spec/helpers/expectations_helper.cr deleted file mode 100644 index 0096f84..0000000 --- a/spec/helpers/expectations_helper.cr +++ /dev/null @@ -1,71 +0,0 @@ -# Utility methods for creating expectations, partials, and matchers. - -def new_partial(actual : T, label : String) forall T - test_value = Spectator::TestValue.new(actual, label) - source = Spectator::Source.new(__FILE__, __LINE__) - Spectator::Expectations::ExpectationPartial.new(test_value, source) -end - -def new_partial(actual : T = 123) forall T - test_value = Spectator::TestValue.new(actual) - source = Spectator::Source.new(__FILE__, __LINE__) - Spectator::Expectations::ExpectationPartial.new(test_value, source) -end - -def new_block_partial(label = "BLOCK", &block) - test_block = Spectator::TestBlock.new(block, label) - source = Spectator::Source.new(__FILE__, __LINE__) - Spectator::Expectations::ExpectationPartial.new(test_block, source) -end - -def new_matcher(expected : T, label : String) forall T - test_value = Spectator::TestValue.new(expected, label) - Spectator::Matchers::EqualityMatcher.new(test_value) -end - -def new_matcher(expected : T = 123) forall T - test_value = Spectator::TestValue.new(expected) - Spectator::Matchers::EqualityMatcher.new(test_value) -end - -def new_expectation(expected : ExpectedType = 123, actual : ActualType = 123) forall ExpectedType, ActualType - partial = new_partial(actual, "foo") - matcher = new_matcher(expected, "bar") - match_data = matcher.match(partial.actual) - Spectator::Expectations::Expectation.new(match_data, partial.source) -end - -def new_satisfied_expectation(value : T = 123) forall T - new_expectation(value, value).tap do |expectation| - expectation.satisfied?.should be_true # Sanity check. - end -end - -def new_unsatisfied_expectation(expected : ExpectedType = 123, actual : ActualType = 456) forall ExpectedType, ActualType - new_expectation(expected, actual).tap do |expectation| - expectation.satisfied?.should be_false # Sanity check. - end -end - -def create_expectations(success_count = 1, failure_count = 0) - satisfied = Array.new(success_count) { new_satisfied_expectation } - unsatisfied = Array.new(failure_count) { new_unsatisfied_expectation } - (satisfied + unsatisfied).shuffle -end - -def generate_expectations(success_count = 1, failure_count = 0) - satisfied = Array.new(success_count) { new_satisfied_expectation } - unsatisfied = Array.new(failure_count) { new_unsatisfied_expectation } - expectations = (satisfied + unsatisfied).shuffle - reporter = Spectator::Expectations::ExpectationReporter.new(false) - expectations.each do |expectation| - reporter.report(expectation) - end - {satisfied: satisfied, unsatisfied: unsatisfied, expectations: expectations, reporter: reporter} -end - -def report_expectations(success_count = 1, failure_count = 0) - harness = Spectator::Internals::Harness.current - success_count.times { harness.report_expectation(new_satisfied_expectation) } - failure_count.times { harness.report_expectation(new_unsatisfied_expectation) } -end diff --git a/spec/helpers/failing_example.cr b/spec/helpers/failing_example.cr deleted file mode 100644 index a0e972d..0000000 --- a/spec/helpers/failing_example.cr +++ /dev/null @@ -1,44 +0,0 @@ -# Example that always fails. -class FailingExample < Spectator::RunnableExample - # Dummy description. - def what : Symbol | String - "FAIL" - end - - # Dummy source. - def source : ::Spectator::Source - ::Spectator::Source.new(__FILE__, __LINE__) - end - - # Dummy symbolic flag. - def symbolic? : Bool - false - end - - # Dummy instance. - def instance - nil - end - - # Run the example that always fails. - private def run_instance - report_expectations(0, 1) - end - - # Creates a failing example. - def self.create(hooks = Spectator::ExampleHooks.empty, conditions = Spectator::ExampleConditions.empty) - group = Spectator::RootExampleGroup.new(hooks, conditions) - values = Spectator::Internals::SampleValues.empty - new(group, values).tap do |example| - group.children = [example.as(Spectator::ExampleComponent)] - end - end - - # Creates a group of failing examples. - def self.create_group(count = 5, hooks = Spectator::ExampleHooks.empty, conditions = Spectator::ExampleConditions.empty) - values = Spectator::Internals::SampleValues.empty - Spectator::RootExampleGroup.new(hooks, conditions).tap do |group| - group.children = Array.new(count) { new(group, values).as(Spectator::ExampleComponent) } - end - end -end diff --git a/spec/helpers/matchers_helper.cr b/spec/helpers/matchers_helper.cr deleted file mode 100644 index 2f123df..0000000 --- a/spec/helpers/matchers_helper.cr +++ /dev/null @@ -1,20 +0,0 @@ -# Retrieves a value from match data given its key/label. -def match_data_value_with_key(match_data_values, key) - labeled_value = match_data_values.find { |v| v.label == key } - raise "#{key} is missing" unless labeled_value - labeled_value.value -end - -# Retrieves the string representation and base value -# from a `Spectator::Matchers::PrefixedMatchDataValue` (or similar) -# in the values returned by `Spectator::Matchers::MatchData#values`. -def match_data_value_sans_prefix(match_data_values, key) - prefix = match_data_value_with_key(match_data_values, key) - {to_s: prefix.to_s, value: prefix.value} -end - -# Check whether match data has a value with a specified key/label. -def match_data_has_key?(match_data_values, key) - found = match_data_values.find { |v| v.label == key } - !!found -end diff --git a/spec/helpers/passing_example.cr b/spec/helpers/passing_example.cr deleted file mode 100644 index 6d03aae..0000000 --- a/spec/helpers/passing_example.cr +++ /dev/null @@ -1,42 +0,0 @@ -# Example that always succeeds. -class PassingExample < Spectator::RunnableExample - # Creates the example. - def initialize(group, values, @symbolic = false) - super(group, values) - end - - # Dummy description. - def what : Symbol | String - "PASS" - end - - # Dummy source. - def source : ::Spectator::Source - ::Spectator::Source.new(__FILE__, __LINE__) - end - - # Dummy symbolic flag. - def symbolic? : Bool - @symbolic - end - - # Dummy instance. - def instance - nil - end - - # Run the example that always passes. - # If this doesn't something broke. - private def run_instance - report_expectations(1, 0) - end - - # Creates a passing example. - def self.create(hooks = Spectator::ExampleHooks.empty, conditions = Spectator::ExampleConditions.empty) - group = Spectator::RootExampleGroup.new(hooks, conditions) - values = Spectator::Internals::SampleValues.empty - new(group, values).tap do |example| - group.children = [example.as(Spectator::ExampleComponent)] - end - end -end diff --git a/spec/helpers/result_call_spy.cr b/spec/helpers/result_call_spy.cr deleted file mode 100644 index 30830fb..0000000 --- a/spec/helpers/result_call_spy.cr +++ /dev/null @@ -1,16 +0,0 @@ -# Spy class for testing `Spectator::Result#call`. -class ResultCallSpy - {% for name in %i[success failure error pending] %} - getter? {{name.id}} = false - - def {{name.id}} - @{{name.id}} = true - {{name}} - end - - def {{name.id}}(arg) - @{{name.id}} = true - arg - end - {% end %} -end diff --git a/spec/helpers/spy_example.cr b/spec/helpers/spy_example.cr deleted file mode 100644 index 32b6474..0000000 --- a/spec/helpers/spy_example.cr +++ /dev/null @@ -1,69 +0,0 @@ -# Example that invokes a closure when it is run. -# This is useful for capturing what's going on when an event is running. -class SpyExample < Spectator::RunnableExample - # Dummy description. - def what : Symbol | String - "SPY" - end - - # Dummy source. - def source : ::Spectator::Source - ::Spectator::Source.new(__FILE__, __LINE__) - end - - # Dummy symbolic flag. - def symbolic? : Bool - false - end - - # Dummy instance. - def instance - nil - end - - # Captures the sample values when the example is created. - def initialize(group, @sample_values) - super(group, @sample_values) - end - - # Sample values given to the example. - getter sample_values : Spectator::Internals::SampleValues - - # Harness that was used while running the example. - getter! harness : Spectator::Internals::Harness - - setter block : Proc(Nil)? = nil - - # Method called by the framework to run the example code. - private def run_instance - @harness = Spectator::Internals::Harness.current - if block = @block - block.call - end - end - - # Creates a spy example. - # The block passed to this method will be executed when the example runs. - def self.create(hooks = Spectator::ExampleHooks.empty, conditions = Spectator::ExampleConditions.empty, &block) - group = Spectator::RootExampleGroup.new(hooks, conditions) - values = Spectator::Internals::SampleValues.empty - new(group, values).tap do |example| - group.children = [example.as(Spectator::ExampleComponent)] - example.block = block - end - end - - # Creates a group of spy examples. - # Specify the number of examplese to create and a block to invoke when the example is run. - # The block is given the index of the example in the group. - def self.create_group(count, &block : Int32 -> Nil) - values = Spectator::Internals::SampleValues.empty - Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |group| - group.children = Array.new(count) do |index| - new(group, values).tap do |example| - example.block = block.partial(index) - end.as(Spectator::ExampleComponent) - end - end - end -end diff --git a/spec/helpers/spy_formatter.cr b/spec/helpers/spy_formatter.cr deleted file mode 100644 index 394200e..0000000 --- a/spec/helpers/spy_formatter.cr +++ /dev/null @@ -1,47 +0,0 @@ -# Formatter that doubles as a spy. -# This class tracks calls made to it. -class SpyFormatter < Spectator::Formatting::Formatter - {% for item in [ - {"start_suite", "Spectator::TestSuite"}, - {"start_example", "Spectator::Example"}, - {"end_example", "Spectator::Result"}, - ] %} - {% method_name = item[0].id %} - {% argument_type = item[1].id %} - - # Stores all invocations made to `#{{method_name}}`. - # Each element is an invocation and the value is the argument passed to the method. - getter {{method_name}}_calls = [] of {{argument_type}} - - # Number of times the `#{{method_name}}` method was called. - def {{method_name}}_call_count - @{{method_name}}_calls.size - end - - # Increments `#{{method_name}}_call_count` and stores the argument. - def {{method_name}}(arg : {{argument_type}}) - @all_calls << {{method_name.symbolize}} - @{{method_name}}_calls << arg - end - - {% end %} - - # Stores all invocatiosn made to `#end_suite`. - # Each element is an invocation and the value is the arguments passed to the method. - getter end_suite_calls = [] of NamedTuple(report: Spectator::Report, profile: Spectator::Profile?) - - # Number of times the `#end_suite` method was called. - def end_suite_call_count - @end_suite_calls.size - end - - # Increments `#end_suite_call_count` and stores the arguments. - def end_suite(report, profile) - @all_calls << :end_suite - @end_suite_calls << {report: report, profile: profile} - end - - # Stores the methods that were called and in which order. - # The symbols will be the method name (i.e. `:start_suite`). - getter all_calls = [] of Symbol -end diff --git a/spec/helpers/spy_sut.cr b/spec/helpers/spy_sut.cr deleted file mode 100644 index 2c3feff..0000000 --- a/spec/helpers/spy_sut.cr +++ /dev/null @@ -1,28 +0,0 @@ -# Example system to test that doubles as a spy. -# This class tracks calls made to it. -class SpySUT - {% for item in [ - {"==", "eq"}, - {"!=", "ne"}, - {"<", "lt"}, - {"<=", "le"}, - {">", "gt"}, - {">=", "ge"}, - {"===", "case_eq"}, - {"=~", "match"}, - {"includes?", "includes"}, - ] %} - {% operator = item[0].id %} - {% name = item[1].id %} - - # Number of times the `#{{operator}}` method was called. - getter {{name}}_call_count = 0 - - # Returns true and increments `#{{name}}_call_count`. - def {{operator}}(other : T) forall T - @{{name}}_call_count += 1 - true - end - - {% end %} -end diff --git a/spec/internals/harness_spec.cr b/spec/internals/harness_spec.cr deleted file mode 100644 index 9a360c0..0000000 --- a/spec/internals/harness_spec.cr +++ /dev/null @@ -1,109 +0,0 @@ -require "../spec_helper" - -describe Spectator::Internals::Harness do - describe "#run" do - it "runs an example" do - run_count = 0 - spy = SpyExample.create do - run_count += 1 - end - Spectator::Internals::Harness.run(spy) - run_count.should eq(1) - end - - context "with a passing exmaple" do - it "returns a passing result" do - example = PassingExample.create - result = Spectator::Internals::Harness.run(example) - result.should be_a(Spectator::SuccessfulResult) - end - end - - context "with a failing example" do - it "returns a failing result" do - example = FailingExample.create - result = Spectator::Internals::Harness.run(example) - result.should be_a(Spectator::FailedResult) - end - end - end - - describe "#current" do - it "references the current harness" do - harness = nil.as(Spectator::Internals::Harness?) - spy = SpyExample.create do - harness = Spectator::Internals::Harness.current - end - Spectator::Internals::Harness.run(spy) - harness.should be_a(Spectator::Internals::Harness) - end - end - - describe "#example" do - it "references the current example" do - example = nil.as(Spectator::Example?) - spy = SpyExample.create do - example = Spectator::Internals::Harness.current.example - end - Spectator::Internals::Harness.run(spy) - example.should be(spy) - end - end - - describe "#report_expectation" do - context "with a successful result" do - it "stores the result" do - expectation = new_satisfied_expectation - spy = SpyExample.create do - harness = Spectator::Internals::Harness.current - harness.report_expectation(expectation) - end - result = Spectator::Internals::Harness.run(spy) - result.should be_a(Spectator::SuccessfulResult) - result.as(Spectator::SuccessfulResult).expectations.should contain(expectation) - end - end - - context "with a failed result" do - it "raises an error" do - error = nil.as(Exception?) - spy = SpyExample.create do - harness = Spectator::Internals::Harness.current - begin - harness.report_expectation(new_unsatisfied_expectation) - rescue ex - error = ex - end - end - Spectator::Internals::Harness.run(spy) - error.should be_a(Spectator::ExampleFailed) - end - - it "stores the result" do - expectation = new_unsatisfied_expectation - spy = SpyExample.create do - harness = Spectator::Internals::Harness.current - harness.report_expectation(expectation) - end - result = Spectator::Internals::Harness.run(spy) - result.should be_a(Spectator::FailedResult) - result.as(Spectator::FailedResult).expectations.should contain(expectation) - end - end - end - - describe "#expectation_results" do - it "contains the reported results" do - expectations = [new_satisfied_expectation, new_unsatisfied_expectation] - spy = SpyExample.create do - harness = Spectator::Internals::Harness.current - expectations.each do |expectation| - harness.report_expectation(expectation) - end - end - result = Spectator::Internals::Harness.run(spy) - reported_results = result.as(Spectator::FailedResult).expectations.to_a - (expectations - reported_results).size.should eq(0) - end - end -end diff --git a/spec/internals/sample_values_spec.cr b/spec/internals/sample_values_spec.cr deleted file mode 100644 index 0e0991a..0000000 --- a/spec/internals/sample_values_spec.cr +++ /dev/null @@ -1,117 +0,0 @@ -require "../spec_helper" - -private def add_sample_value(values : Spectator::Internals::SampleValues, - symbol : Symbol, name : String, value : T) forall T - values.add(symbol, name, value) -end - -private def add_sample_value(symbol, name, value : T) forall T - add_sample_value(Spectator::Internals::SampleValues.empty, symbol, name, value) -end - -private def add_sample_value(symbol, value : T) forall T - add_sample_value(symbol, symbol.to_s, value) -end - -describe Spectator::Internals::SampleValues do - describe "#add" do - it "creates a new set" do - original = Spectator::Internals::SampleValues.empty - new_set = original.add(:new, "new", 123) - new_set.should_not eq(original) - end - - it "adds a new value" do - symbol = :new - value = 123 - values = add_sample_value(symbol, value) - values.get_value(symbol, typeof(value)).should eq(value) - end - end - - describe "#get_wrapper" do - it "returns a wrapper for a value" do - symbol = :new - value = 123 - values = add_sample_value(symbol, value) - wrapper = values.get_wrapper(symbol) - wrapper.should be_a(Spectator::Internals::ValueWrapper) - end - - it "returns the correct wrapper" do - symbol = :new - value = 123 - values = add_sample_value(symbol, value) - wrapper = values.get_wrapper(symbol) - wrapper.should be_a(Spectator::Internals::TypedValueWrapper(typeof(value))) - wrapper.as(Spectator::Internals::TypedValueWrapper(typeof(value))).value.should eq(value) - end - - context "with multiple values" do - it "returns the expected value" do - symbols = { - one: 123, - two: 456, - three: 789, - } - values = Spectator::Internals::SampleValues.empty - symbols.each do |symbol, number| - values = add_sample_value(values, symbol, symbol.to_s, number) - end - selected_symbol = :one - selected_number = symbols[selected_symbol] - wrapper = values.get_wrapper(selected_symbol) - wrapper.should be_a(Spectator::Internals::TypedValueWrapper(typeof(selected_number))) - wrapper.as(Spectator::Internals::TypedValueWrapper(typeof(selected_number))).value.should eq(selected_number) - end - end - end - - describe "#get_value" do - it "returns a value" do - symbol = :new - value = 123 - values = add_sample_value(symbol, value) - values.get_value(symbol, typeof(value)).should eq(value) - end - - context "with multiple values" do - it "returns the expected value" do - symbols = { - one: 123, - two: 456, - three: 789, - } - values = Spectator::Internals::SampleValues.empty - symbols.each do |symbol, number| - values = add_sample_value(values, symbol, symbol.to_s, number) - end - selected_symbol = :one - selected_number = symbols[selected_symbol] - value = values.get_value(selected_symbol, typeof(selected_number)) - value.should eq(selected_number) - end - end - end - - describe "#each" do - it "yields each entry" do - symbols = { - one: 123, - two: 456, - three: 789, - } - values = Spectator::Internals::SampleValues.empty - symbols.each do |symbol, number| - values = add_sample_value(values, symbol, symbol.to_s, number) - end - - size = 0 - values.each do |entry| - size += 1 - symbols.keys.map(&.to_s).should contain(entry.name) - end - size.should eq(symbols.size) - end - end -end diff --git a/spec/internals/typed_value_wrapper_spec.cr b/spec/internals/typed_value_wrapper_spec.cr deleted file mode 100644 index ec997b1..0000000 --- a/spec/internals/typed_value_wrapper_spec.cr +++ /dev/null @@ -1,18 +0,0 @@ -require "../spec_helper" - -describe Spectator::Internals::TypedValueWrapper do - describe "#value" do - it "returns the expected value" do - value = 12345 - wrapper = Spectator::Internals::TypedValueWrapper.new(value) - wrapper.value.should eq(value) - end - end - - it "can be cast for storage" do - value = 12345 - wrapper = Spectator::Internals::TypedValueWrapper.new(value).as(Spectator::Internals::ValueWrapper) - typed = wrapper.as(Spectator::Internals::TypedValueWrapper(typeof(value))) - typed.value.should eq(value) - end -end diff --git a/spec/line_example_filter_spec.cr b/spec/line_example_filter_spec.cr deleted file mode 100644 index ca2b1df..0000000 --- a/spec/line_example_filter_spec.cr +++ /dev/null @@ -1,21 +0,0 @@ -require "./spec_helper" - -describe Spectator::LineExampleFilter do - describe "#includes?" do - context "with a matching example" do - it "is true" do - example = PassingExample.create - filter = Spectator::LineExampleFilter.new(example.source.line) - filter.includes?(example).should be_true - end - end - - context "with a non-matching example" do - it "is false" do - example = PassingExample.create - filter = Spectator::LineExampleFilter.new(example.source.line + 5) - filter.includes?(example).should be_false - end - end - end -end diff --git a/spec/name_example_filter_spec.cr b/spec/name_example_filter_spec.cr deleted file mode 100644 index 0fdeefb..0000000 --- a/spec/name_example_filter_spec.cr +++ /dev/null @@ -1,21 +0,0 @@ -require "./spec_helper" - -describe Spectator::NameExampleFilter do - describe "#includes?" do - context "with a matching example" do - it "is true" do - example = PassingExample.create - filter = Spectator::NameExampleFilter.new(example.to_s) - filter.includes?(example).should be_true - end - end - - context "with a non-matching example" do - it "is false" do - example = PassingExample.create - filter = Spectator::NameExampleFilter.new("BOGUS") - filter.includes?(example).should be_false - end - end - end -end diff --git a/spec/nested_example_group_spec.cr b/spec/nested_example_group_spec.cr deleted file mode 100644 index 7d102b0..0000000 --- a/spec/nested_example_group_spec.cr +++ /dev/null @@ -1,1130 +0,0 @@ -require "./spec_helper" - -def new_nested_group(what : Symbol | String = "what", hooks = Spectator::ExampleHooks.empty, conditions = Spectator::ExampleConditions.empty, parent : Spectator::ExampleGroup? = nil) - parent ||= Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - Spectator::NestedExampleGroup.new(what, parent, hooks, conditions).tap do |group| - parent.children = [group.as(Spectator::ExampleComponent)] - group.children = [] of Spectator::ExampleComponent - end -end - -def nested_group_with_examples(example_count = 5) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - examples = [] of Spectator::Example - group.children = Array(Spectator::ExampleComponent).new(example_count) do - PassingExample.new(group, Spectator::Internals::SampleValues.empty).tap do |example| - examples << example - end - end - {group, examples} -end - -def nested_group_with_sub_groups(sub_group_count = 5, example_count = 5) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - examples = [] of Spectator::Example - group.children = Array(Spectator::ExampleComponent).new(sub_group_count) do |i| - Spectator::NestedExampleGroup.new(i.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group| - sub_group.children = Array(Spectator::ExampleComponent).new(example_count) do - PassingExample.new(group, Spectator::Internals::SampleValues.empty).tap do |example| - examples << example - end - end - end - end - {group, examples} -end - -def complex_nested_group - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - examples = [] of Spectator::Example - group.children = Array(Spectator::ExampleComponent).new(10) do |i| - if i % 2 == 0 - PassingExample.new(group, Spectator::Internals::SampleValues.empty).tap do |example| - examples << example - end - else - Spectator::NestedExampleGroup.new(i.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group1| - sub_group1.children = Array(Spectator::ExampleComponent).new(10) do |j| - if i % 2 == 0 - PassingExample.new(sub_group1, Spectator::Internals::SampleValues.empty).tap do |example| - examples << example - end - else - Spectator::NestedExampleGroup.new(j.to_s, sub_group1, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group2| - sub_group2.children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(sub_group2, Spectator::Internals::SampleValues.empty).tap do |example| - examples << example - end - end - end - end - end - end - end - end - {group, examples} -end - -describe Spectator::NestedExampleGroup do - describe "#what" do - it "is the expected value" do - what = "foobar" - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new(what, root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group.what.should eq(what) - end - end - - describe "#parent" do - it "is the expected value" do - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group.parent.should eq(root) - end - end - - describe "#run_before_hooks" do - it "runs a single before_all hook" do - called = false - hooks = new_hooks(before_all: ->{ called = true; nil }) - group = new_nested_group(hooks: hooks) - group.run_before_hooks - called.should be_true - end - - it "runs a single before_each hook" do - called = false - hooks = new_hooks(before_each: ->{ called = true; nil }) - group = new_nested_group(hooks: hooks) - group.run_before_hooks - called.should be_true - end - - it "runs multiple before_all hooks" do - call_count = 0 - hooks = new_hooks(before_all: [ - ->{ call_count += 1; nil }, - ->{ call_count += 2; nil }, - ->{ call_count += 3; nil }, - ]) - group = new_nested_group(hooks: hooks) - group.run_before_hooks - call_count.should eq(6) - end - - it "runs multiple before_each hooks" do - call_count = 0 - hooks = new_hooks(before_each: [ - ->{ call_count += 1; nil }, - ->{ call_count += 2; nil }, - ->{ call_count += 3; nil }, - ]) - group = new_nested_group(hooks: hooks) - group.run_before_hooks - call_count.should eq(6) - end - - it "runs hooks in the correct order" do - calls = [] of Symbol - hooks = new_hooks(before_all: [ - ->{ calls << :a; nil }, - ->{ calls << :b; nil }, - ->{ calls << :c; nil }, - ], - before_each: [ - ->{ calls << :d; nil }, - ->{ calls << :e; nil }, - ->{ calls << :f; nil }, - ]) - group = new_nested_group(hooks: hooks) - group.run_before_hooks - calls.should eq(%i[a b c d e f]) - end - - it "runs the parent before_all hooks" do - called = false - hooks = new_hooks(before_all: ->{ called = true; nil }) - root = Spectator::RootExampleGroup.new(hooks, Spectator::ExampleConditions.empty) - group = new_nested_group(parent: root) - group.run_before_hooks - called.should be_true - end - - it "runs the parent before_each hooks" do - called = false - hooks = new_hooks(before_each: ->{ called = true; nil }) - root = Spectator::RootExampleGroup.new(hooks, Spectator::ExampleConditions.empty) - group = new_nested_group(parent: root) - group.run_before_hooks - called.should be_true - end - - it "runs the parent before_all hooks first" do - calls = [] of Symbol - root_hooks = new_hooks(before_all: ->{ calls << :a; nil }) - group_hooks = new_hooks(before_all: ->{ calls << :b; nil }) - root = Spectator::RootExampleGroup.new(root_hooks, Spectator::ExampleConditions.empty) - group = new_nested_group(hooks: group_hooks, parent: root) - group.run_before_hooks - calls.should eq(%i[a b]) - end - - it "runs the parent before_each hooks first" do - calls = [] of Symbol - root_hooks = new_hooks(before_each: ->{ calls << :a; nil }) - group_hooks = new_hooks(before_each: ->{ calls << :b; nil }) - root = Spectator::RootExampleGroup.new(root_hooks, Spectator::ExampleConditions.empty) - group = new_nested_group(hooks: group_hooks, parent: root) - group.run_before_hooks - calls.should eq(%i[a b]) - end - - it "runs the before_all hooks once" do - call_count = 0 - hooks = new_hooks(before_all: ->{ call_count += 1; nil }) - group = new_nested_group(hooks: hooks) - 2.times { group.run_before_hooks } - call_count.should eq(1) - end - - it "runs the before_each hooks multiple times" do - call_count = 0 - hooks = new_hooks(before_each: ->{ call_count += 1; nil }) - group = new_nested_group(hooks: hooks) - 2.times { group.run_before_hooks } - call_count.should eq(2) - end - end - - describe "#run_after_hooks" do - # No children are used for most of these examples. - # That's because `[].all?` is always true. - # Which means that all examples are considered finished, since there are none. - it "runs a single after_all hook" do - called = false - hooks = new_hooks(after_all: ->{ called = true; nil }) - group = new_nested_group(hooks: hooks) - group.run_after_hooks - called.should be_true - end - - it "runs multiple after_all hooks" do - call_count = 0 - hooks = new_hooks(after_all: [ - ->{ call_count += 1; nil }, - ->{ call_count += 2; nil }, - ->{ call_count += 3; nil }, - ]) - group = new_nested_group(hooks: hooks) - group.run_after_hooks - call_count.should eq(6) - end - - it "runs hooks in the correct order" do - calls = [] of Symbol - hooks = new_hooks(after_each: [ - ->{ calls << :a; nil }, - ->{ calls << :b; nil }, - ->{ calls << :c; nil }, - ], - after_all: [ - ->{ calls << :d; nil }, - ->{ calls << :e; nil }, - ->{ calls << :f; nil }, - ]) - group = new_nested_group(hooks: hooks) - group.run_after_hooks - calls.should eq(%i[a b c d e f]) - end - - it "runs the parent after_all hooks" do - called = false - hooks = new_hooks(after_all: ->{ called = true; nil }) - root = Spectator::RootExampleGroup.new(hooks, Spectator::ExampleConditions.empty) - group = new_nested_group(parent: root) - group.run_after_hooks - called.should be_true - end - - it "runs the parent after_each hooks" do - called = false - hooks = new_hooks(after_each: ->{ called = true; nil }) - root = Spectator::RootExampleGroup.new(hooks, Spectator::ExampleConditions.empty) - group = new_nested_group(parent: root) - group.run_after_hooks - called.should be_true - end - - it "runs the parent after_all hooks last" do - calls = [] of Symbol - root_hooks = new_hooks(after_all: ->{ calls << :a; nil }) - group_hooks = new_hooks(after_all: ->{ calls << :b; nil }) - root = Spectator::RootExampleGroup.new(root_hooks, Spectator::ExampleConditions.empty) - group = new_nested_group(hooks: group_hooks, parent: root) - group.run_after_hooks - calls.should eq(%i[b a]) - end - - it "runs the parent after_each hooks last" do - calls = [] of Symbol - root_hooks = new_hooks(after_each: ->{ calls << :a; nil }) - group_hooks = new_hooks(after_each: ->{ calls << :b; nil }) - root = Spectator::RootExampleGroup.new(root_hooks, Spectator::ExampleConditions.empty) - group = new_nested_group(hooks: group_hooks, parent: root) - group.run_after_hooks - calls.should eq(%i[b a]) - end - - it "runs the after_all hooks once" do - call_count = 0 - hooks = new_hooks(after_all: ->{ call_count += 1; nil }) - group = new_nested_group(hooks: hooks) - 2.times { group.run_after_hooks } - call_count.should eq(1) - end - - it "runs the after_each hooks multiple times" do - call_count = 0 - hooks = new_hooks(after_each: ->{ call_count += 1; nil }) - group = new_nested_group(hooks: hooks) - 2.times { group.run_after_hooks } - call_count.should eq(2) - end - - context "with no examples finished" do - it "doesn't run the after_all hooks" do - called = false - hooks = new_hooks(after_all: ->{ called = true; nil }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, hooks, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - group.children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(group, Spectator::Internals::SampleValues.empty) - end - group.run_after_hooks - called.should be_false - end - - it "runs the after_each hooks" do - called = false - hooks = new_hooks(after_each: ->{ called = true; nil }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, hooks, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - group.children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(group, Spectator::Internals::SampleValues.empty) - end - group.run_after_hooks - called.should be_true - end - - it "doesn't run the parent after_all hooks" do - called = false - hooks = new_hooks(after_all: ->{ called = true; nil }) - root = Spectator::RootExampleGroup.new(hooks, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - group.children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(group, Spectator::Internals::SampleValues.empty) - end - group.run_after_hooks - called.should be_false - end - - it "runs the parent after_each hooks" do - called = false - hooks = new_hooks(after_each: ->{ called = true; nil }) - root = Spectator::RootExampleGroup.new(hooks, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - group.children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(group, Spectator::Internals::SampleValues.empty) - end - group.run_after_hooks - called.should be_true - end - end - - context "with some examples finished" do - it "doesn't run the after_all hooks" do - called = false - examples = [] of Spectator::Example - hooks = new_hooks(after_all: ->{ called = true; nil }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, hooks, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - group.children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(group, Spectator::Internals::SampleValues.empty).tap do |example| - examples << example - end - end - examples.each_with_index do |example, index| - Spectator::Internals::Harness.run(example) if index % 2 == 0 - end - group.run_after_hooks - called.should be_false - end - - it "runs the after_each hooks" do - called = false - examples = [] of Spectator::Example - hooks = new_hooks(after_each: ->{ called = true; nil }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, hooks, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - group.children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(group, Spectator::Internals::SampleValues.empty).tap do |example| - examples << example - end - end - examples.each_with_index do |example, index| - Spectator::Internals::Harness.run(example) if index % 2 == 0 - end - group.run_after_hooks - called.should be_true - end - - it "doesn't run the parent after_all hooks" do - called = false - examples = [] of Spectator::Example - hooks = new_hooks(after_all: ->{ called = true; nil }) - root = Spectator::RootExampleGroup.new(hooks, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - group.children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(group, Spectator::Internals::SampleValues.empty).tap do |example| - examples << example - end - end - examples.each_with_index do |example, index| - Spectator::Internals::Harness.run(example) if index % 2 == 0 - end - group.run_after_hooks - called.should be_false - end - - it "runs the parent after_each hooks" do - called = false - examples = [] of Spectator::Example - hooks = new_hooks(after_each: ->{ called = true; nil }) - root = Spectator::RootExampleGroup.new(hooks, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - group.children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(group, Spectator::Internals::SampleValues.empty).tap do |example| - examples << example - end - end - examples.each_with_index do |example, index| - Spectator::Internals::Harness.run(example) if index % 2 == 0 - end - group.run_after_hooks - called.should be_true - end - end - end - - describe "#wrap_around_each_hooks" do - it "wraps the block" do - called = false - wrapper = new_nested_group.wrap_around_each_hooks do - called = true - end - wrapper.call - called.should be_true - end - - it "wraps a proc" do - called = false - hooks = new_hooks(around_each: ->(proc : ->) { called = true; proc.call }) - wrapper = new_nested_group(hooks: hooks).wrap_around_each_hooks { } - wrapper.call - called.should be_true - end - - it "wraps multiple procs" do - call_count = 0 - hooks = new_hooks(around_each: [ - ->(proc : ->) { call_count += 1; proc.call }, - ->(proc : ->) { call_count += 2; proc.call }, - ->(proc : ->) { call_count += 3; proc.call }, - ]) - wrapper = new_nested_group(hooks: hooks).wrap_around_each_hooks { } - wrapper.call - call_count.should eq(6) - end - - it "wraps procs in the correct order" do - calls = [] of Symbol - hooks = new_hooks(around_each: [ - ->(proc : ->) { calls << :a; proc.call }, - ->(proc : ->) { calls << :b; proc.call }, - ->(proc : ->) { calls << :c; proc.call }, - ]) - wrapper = new_nested_group(hooks: hooks).wrap_around_each_hooks { } - wrapper.call - calls.should eq(%i[a b c]) - end - - it "wraps the parent hooks" do - called = false - hooks = new_hooks(around_each: ->(proc : ->) { called = true; proc.call }) - root = Spectator::RootExampleGroup.new(hooks, Spectator::ExampleConditions.empty) - wrapper = new_nested_group(parent: root).wrap_around_each_hooks { } - wrapper.call - called.should be_true - end - - it "wraps the parent hooks so they are outermost" do - calls = [] of Symbol - root_hooks = new_hooks(around_each: ->(proc : ->) { calls << :a; proc.call }) - group_hooks = new_hooks(around_each: ->(proc : ->) { calls << :b; proc.call }) - root = Spectator::RootExampleGroup.new(root_hooks, Spectator::ExampleConditions.empty) - group = new_nested_group(hooks: group_hooks, parent: root) - wrapper = group.wrap_around_each_hooks { } - wrapper.call - calls.should eq(%i[a b]) - end - end - - describe "#run_pre_conditions" do - it "runs a single pre-condition" do - called = false - conditions = new_conditions(pre: ->{ called = true; nil }) - group = new_nested_group(conditions: conditions) - group.run_pre_conditions - called.should be_true - end - - it "runs multiple pre-conditions" do - call_count = 0 - conditions = new_conditions(pre: [ - ->{ call_count += 1; nil }, - ->{ call_count += 2; nil }, - ->{ call_count += 3; nil }, - ]) - group = new_nested_group(conditions: conditions) - group.run_pre_conditions - call_count.should eq(6) - end - - it "runs conditions in the correct order" do - calls = [] of Symbol - conditions = new_conditions(pre: [ - ->{ calls << :a; nil }, - ->{ calls << :b; nil }, - ->{ calls << :c; nil }, - ]) - group = new_nested_group(conditions: conditions) - group.run_pre_conditions - calls.should eq(%i[a b c]) - end - - it "runs the parent conditions" do - called = false - conditions = new_conditions(pre: ->{ called = true; nil }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, conditions) - group = new_nested_group(parent: root) - group.run_pre_conditions - called.should be_true - end - - it "runs the parent conditions first" do - calls = [] of Symbol - root_conditions = new_conditions(pre: ->{ calls << :a; nil }) - group_conditions = new_conditions(pre: ->{ calls << :b; nil }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, root_conditions) - group = new_nested_group(conditions: group_conditions, parent: root) - group.run_pre_conditions - calls.should eq(%i[a b]) - end - - it "runs the conditions multiple times" do - call_count = 0 - conditions = new_conditions(pre: ->{ call_count += 1; nil }) - group = new_nested_group(conditions: conditions) - 2.times { group.run_pre_conditions } - call_count.should eq(2) - end - end - - describe "#run_post_conditions" do - it "runs a single condition" do - called = false - conditions = new_conditions(post: ->{ called = true; nil }) - group = new_nested_group(conditions: conditions) - group.run_post_conditions - called.should be_true - end - - it "runs multiple conditions" do - call_count = 0 - conditions = new_conditions(post: [ - ->{ call_count += 1; nil }, - ->{ call_count += 2; nil }, - ->{ call_count += 3; nil }, - ]) - group = new_nested_group(conditions: conditions) - group.run_post_conditions - call_count.should eq(6) - end - - it "runs conditions in the correct order" do - calls = [] of Symbol - conditions = new_conditions(post: [ - ->{ calls << :a; nil }, - ->{ calls << :b; nil }, - ->{ calls << :c; nil }, - ]) - group = new_nested_group(conditions: conditions) - group.run_post_conditions - calls.should eq(%i[a b c]) - end - - it "runs the parent conditions" do - called = false - conditions = new_conditions(post: ->{ called = true; nil }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, conditions) - group = new_nested_group(parent: root) - group.run_post_conditions - called.should be_true - end - - it "runs the parent conditions last" do - calls = [] of Symbol - root_conditions = new_conditions(post: ->{ calls << :a; nil }) - group_conditions = new_conditions(post: ->{ calls << :b; nil }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, root_conditions) - group = new_nested_group(conditions: group_conditions, parent: root) - group.run_post_conditions - calls.should eq(%i[b a]) - end - - it "runs the conditions multiple times" do - call_count = 0 - conditions = new_conditions(post: ->{ call_count += 1; nil }) - group = new_nested_group(conditions: conditions) - 2.times { group.run_post_conditions } - call_count.should eq(2) - end - end - - describe "#to_s" do - it "contains #what" do - group = new_nested_group - group.to_s.should contain(group.what.to_s) - end - - it "contains the parent's #to_s" do - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - parent = Spectator::NestedExampleGroup.new("PARENT", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("GROUP", parent, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [parent.as(Spectator::ExampleComponent)] - parent.children = [group.as(Spectator::ExampleComponent)] - group.children = [] of Spectator::ExampleComponent - group.to_s.should contain(parent.to_s) - end - - context "when #symbolic? is true" do - context "and the parent group is symbolic" do - it "omits the space" do - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - parent = Spectator::NestedExampleGroup.new(:Parent, root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new(:"#foo", parent, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [parent.as(Spectator::ExampleComponent)] - parent.children = [group.as(Spectator::ExampleComponent)] - group.children = [] of Spectator::ExampleComponent - parent.symbolic?.should be_true - group.symbolic?.should be_true - group.to_s.should_not contain(' ') - end - end - - context "and the parent group isn't symbolic" do - it "inserts a space" do - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - parent = Spectator::NestedExampleGroup.new("PARENT", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new(:"#foo", parent, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [parent.as(Spectator::ExampleComponent)] - parent.children = [group.as(Spectator::ExampleComponent)] - group.children = [] of Spectator::ExampleComponent - parent.symbolic?.should be_false - group.symbolic?.should be_true - group.to_s.should contain(' ') - end - end - end - - context "when #symbolic? is false" do - context "and the parent group is symbolic" do - it "inserts a space" do - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - parent = Spectator::NestedExampleGroup.new(:Parent, root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("GROUP", parent, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [parent.as(Spectator::ExampleComponent)] - parent.children = [group.as(Spectator::ExampleComponent)] - group.children = [] of Spectator::ExampleComponent - parent.symbolic?.should be_true - group.symbolic?.should be_false - group.to_s.should contain(' ') - end - end - - context "and the parent group isn't symbolic" do - it "inserts a space" do - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - parent = Spectator::NestedExampleGroup.new("PARENT", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("GROUP", parent, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [parent.as(Spectator::ExampleComponent)] - parent.children = [group.as(Spectator::ExampleComponent)] - group.children = [] of Spectator::ExampleComponent - parent.symbolic?.should be_false - group.symbolic?.should be_false - group.to_s.should contain(' ') - end - end - end - - context "when the parent group is root" do - it "omits the space" do - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("GROUP", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - group.children = [] of Spectator::ExampleComponent - group.to_s.should_not contain(' ') - end - end - end - - describe "#children" do - it "raises an error when not set" do - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - expect_raises(Exception) { group.children } - end - - it "returns the expected set" do - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(group, Spectator::Internals::SampleValues.empty) - end - group.children = children - group.children.should eq(children) - end - end - - describe "#children=" do - it "raises an error trying to reset" do - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(group, Spectator::Internals::SampleValues.empty) - end - group.children = children - expect_raises(Exception) { group.children = children } - end - end - - describe "#each" do - it "yields each child" do - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - group.children = Array(Spectator::ExampleComponent).new(5) do |i| - if i % 2 == 0 - PassingExample.new(group, Spectator::Internals::SampleValues.empty) - else - Spectator::NestedExampleGroup.new(i.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group| - sub_group.children = [] of Spectator::ExampleComponent - end - end - end - group.to_a.should eq(group.children) - end - - it "doesn't yield children of children" do - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - group.children = Array(Spectator::ExampleComponent).new(5) do |i| - if i % 2 == 0 - PassingExample.new(group, Spectator::Internals::SampleValues.empty) - else - Spectator::NestedExampleGroup.new(i.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group| - sub_group.children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(sub_group, Spectator::Internals::SampleValues.empty) - end - end - end - end - group.to_a.should eq(group.children) - end - end - - describe "#each : Iterator" do - it "iterates over each child" do - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - group.children = Array(Spectator::ExampleComponent).new(5) do |i| - if i % 2 == 0 - PassingExample.new(group, Spectator::Internals::SampleValues.empty) - else - Spectator::NestedExampleGroup.new(i.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group| - sub_group.children = [] of Spectator::ExampleComponent - end - end - end - group.each.to_a.should eq(group.children) - end - - it "doesn't iterate over children of children" do - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - group.children = Array(Spectator::ExampleComponent).new(5) do |i| - if i % 2 == 0 - PassingExample.new(group, Spectator::Internals::SampleValues.empty) - else - Spectator::NestedExampleGroup.new(i.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group| - sub_group.children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(sub_group, Spectator::Internals::SampleValues.empty) - end - end - end - end - group.each.to_a.should eq(group.children) - end - end - - describe "#example_count" do - context "with no examples" do - it "is zero" do - new_nested_group.example_count.should eq(0) - end - end - - context "with empty sub-groups" do - it "is zero" do - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - group.children = Array(Spectator::ExampleComponent).new(5) do |i| - Spectator::NestedExampleGroup.new(i.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - end - group.example_count.should eq(0) - end - end - - context "with direct descendant examples" do - it "equals the number of examples" do - example_count = 10 - group, _ = nested_group_with_examples(example_count) - group.example_count.should eq(example_count) - end - end - - context "with examples sub-groups" do - it "equals the total number of examples" do - sub_group_count = 3 - example_count = 10 - group, _ = nested_group_with_sub_groups(sub_group_count, example_count) - group.example_count.should eq(sub_group_count * example_count) - end - end - - context "with examples at all levels" do - it "equals the total number of examples" do - group, examples = complex_nested_group - group.example_count.should eq(examples.size) - end - end - end - - describe "#[]" do - context "when empty" do - it "raises an error" do - group = new_nested_group - expect_raises(IndexError) { group[0] } - end - end - - context "with direct descendant examples" do - context "given 0" do - it "returns the first example" do - group, examples = nested_group_with_examples - group[0].should eq(examples.first) - end - end - - context "given -1" do - it "returns the last example" do - group, examples = nested_group_with_examples - group[-1].should eq(examples.last) - end - end - - context "given an in-bounds positive index" do - it "returns the expected example" do - group, examples = nested_group_with_examples(10) - group[3].should eq(examples[3]) - end - end - - context "given an in-bounds negative index" do - it "returns the expected example" do - group, examples = nested_group_with_examples(10) - group[-3].should eq(examples[-3]) - end - end - - context "an out-of-bounds positive index" do - it "raises an index error" do - group, _ = nested_group_with_examples(10) - expect_raises(IndexError) { group[15] } - end - - it "handles off-by-one" do - group, _ = nested_group_with_examples(10) - expect_raises(IndexError) { group[10] } - end - end - - context "an out-of-bounds negative index" do - it "raises an index error" do - group, _ = nested_group_with_examples(10) - expect_raises(IndexError) { group[-15] } - end - - it "handles off-by-one" do - group, _ = nested_group_with_examples(10) - expect_raises(IndexError) { group[-11] } - end - end - end - - context "with examples only in sub-groups" do - context "given 0" do - it "returns the first example" do - group, examples = nested_group_with_sub_groups - group[0].should eq(examples.first) - end - end - - context "given -1" do - it "returns the last example" do - group, examples = nested_group_with_sub_groups - group[-1].should eq(examples.last) - end - end - - context "given an in-bounds positive index" do - it "returns the expected example" do - group, examples = nested_group_with_sub_groups(10, 2) - group[6].should eq(examples[6]) - end - end - - context "given an in-bounds negative index" do - it "returns the expected example" do - group, examples = nested_group_with_sub_groups(10, 2) - group[-6].should eq(examples[-6]) - end - end - - context "an out-of-bounds positive index" do - it "raises an index error" do - group, _ = nested_group_with_sub_groups(10, 2) - expect_raises(IndexError) { group[25] } - end - - it "handles off-by-one" do - group, _ = nested_group_with_sub_groups(10, 2) - expect_raises(IndexError) { group[20] } - end - end - - context "an out-of-bounds negative index" do - it "raises an index error" do - group, _ = nested_group_with_sub_groups(10, 2) - expect_raises(IndexError) { group[-25] } - end - - it "handles off-by-one" do - group, _ = nested_group_with_sub_groups(10, 2) - expect_raises(IndexError) { group[-21] } - end - end - end - - context "with examples at all levels" do - context "given 0" do - it "returns the first example" do - group, examples = complex_nested_group - group[0].should eq(examples.first) - end - end - - context "given -1" do - it "returns the last example" do - group, examples = complex_nested_group - group[-1].should eq(examples.last) - end - end - - context "given an in-bounds positive index" do - it "returns the expected example" do - group, examples = complex_nested_group - group[42].should eq(examples[42]) - end - end - - context "given an in-bounds negative index" do - it "returns the expected example" do - group, examples = complex_nested_group - group[-42].should eq(examples[-42]) - end - end - - context "an out-of-bounds positive index" do - it "raises an index error" do - group, examples = complex_nested_group - expect_raises(IndexError) { group[examples.size + 5] } - end - - it "handles off-by-one" do - group, examples = complex_nested_group - expect_raises(IndexError) { group[examples.size] } - end - end - - context "an out-of-bounds negative index" do - it "raises an index error" do - group, examples = complex_nested_group - expect_raises(IndexError) { group[-examples.size - 5] } - end - - it "handles off-by-one" do - group, examples = complex_nested_group - expect_raises(IndexError) { group[-examples.size - 1] } - end - end - end - - context "with only sub-groups and no examples" do - it "raises an index error" do - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - group.children = Array(Spectator::ExampleComponent).new(5) do |i| - Spectator::NestedExampleGroup.new(i.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group| - sub_group.children = [] of Spectator::ExampleComponent - end - end - expect_raises(IndexError) { group[0] } - end - end - end - - describe "#finished?" do - context "with no children" do - it "is true" do - new_nested_group.finished?.should be_true - end - end - - context "with all unfinished children" do - it "is false" do - group, _ = nested_group_with_examples - group.finished?.should be_false - end - end - - context "with some finished children" do - it "is false" do - group, examples = nested_group_with_examples - examples.each_with_index do |example, index| - Spectator::Internals::Harness.run(example) if index % 2 == 0 - end - group.finished?.should be_false - end - end - - context "with all finished children" do - it "is true" do - group, examples = nested_group_with_examples - examples.each do |example| - Spectator::Internals::Harness.run(example) - end - group.finished?.should be_true - end - end - - context "with a sub-group" do - context "with no children" do - it "is true" do - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - group.children = Array(Spectator::ExampleComponent).new(5) do |i| - Spectator::NestedExampleGroup.new(i.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group| - sub_group.children = [] of Spectator::ExampleComponent - end - end - group.finished?.should be_true - end - end - - context "with all unfinished children" do - it "is false" do - group, _ = nested_group_with_sub_groups - group.finished?.should be_false - end - end - - context "with some finished children" do - it "is false" do - group, examples = nested_group_with_sub_groups - examples.each_with_index do |example, index| - Spectator::Internals::Harness.run(example) if index % 2 == 0 - end - group.finished?.should be_false - end - end - - context "with all finished children" do - it "is true" do - group, examples = nested_group_with_sub_groups - examples.each do |example| - Spectator::Internals::Harness.run(example) - end - group.finished?.should be_true - end - end - end - end - - describe "#symbolic?" do - context "when 'what' is a Symbol" do - it "is true" do - new_nested_group(:What).symbolic?.should be_true - end - end - - context "when 'what' is a String" do - it "is false" do - new_nested_group("what").symbolic?.should be_false - end - end - end -end diff --git a/spec/null_example_filter_spec.cr b/spec/null_example_filter_spec.cr deleted file mode 100644 index 42722a6..0000000 --- a/spec/null_example_filter_spec.cr +++ /dev/null @@ -1,11 +0,0 @@ -require "./spec_helper" - -describe Spectator::NullExampleFilter do - describe "#includes?" do - it "returns true" do - example = PassingExample.create - filter = Spectator::NullExampleFilter.new - filter.includes?(example).should be_true - end - end -end diff --git a/spec/pending_example_spec.cr b/spec/pending_example_spec.cr deleted file mode 100644 index 8ed23df..0000000 --- a/spec/pending_example_spec.cr +++ /dev/null @@ -1,122 +0,0 @@ -require "./spec_helper" - -class ConcretePendingExample < Spectator::PendingExample - def what : Symbol | String - "PENDING_TEST_EXAMPLE" - end - - def source : ::Spectator::Source - ::Spectator::Source.new(__FILE__, __LINE__) - end - - def symbolic? : Bool - false - end - - def instance - nil - end -end - -def new_pending_example(group : Spectator::ExampleGroup? = nil) - ConcretePendingExample.new(group || new_root_group, Spectator::Internals::SampleValues.empty) -end - -def new_pending_example_with_hooks(hooks) - group = new_root_group(hooks) - new_pending_example(group) -end - -describe Spectator::PendingExample do - describe "#run" do - it "returns a pending result" do - new_pending_example.run.should be_a(Spectator::PendingResult) - end - - it "doesn't run before_all hooks" do - called = false - hooks = new_hooks(before_all: ->{ called = true; nil }) - example = new_pending_example_with_hooks(hooks) - Spectator::Internals::Harness.run(example) - called.should be_false - end - - it "doesn't run before_each hooks" do - called = false - hooks = new_hooks(before_each: ->{ called = true; nil }) - example = new_pending_example_with_hooks(hooks) - Spectator::Internals::Harness.run(example) - called.should be_false - end - - it "doesn't run after_all hooks" do - called = false - hooks = new_hooks(after_all: ->{ called = true; nil }) - example = new_pending_example_with_hooks(hooks) - Spectator::Internals::Harness.run(example) - called.should be_false - end - - it "doesn't run after_each hooks" do - called = false - hooks = new_hooks(after_each: ->{ called = true; nil }) - example = new_pending_example_with_hooks(hooks) - Spectator::Internals::Harness.run(example) - called.should be_false - end - - it "doesn't run around_each hooks" do - called = false - hooks = new_hooks(around_each: ->(proc : ->) { called = true; proc.call }) - example = new_pending_example_with_hooks(hooks) - Spectator::Internals::Harness.run(example) - called.should be_false - end - end - - describe "#finished?" do - it "is initially false" do - new_pending_example.finished?.should be_false - end - - it "is true after #run is called" do - example = new_pending_example - Spectator::Internals::Harness.run(example) - example.finished?.should be_true - end - end - - describe "#group" do - it "is the expected value" do - group = new_root_group - example = new_pending_example(group) - example.group.should eq(group) - end - end - - describe "#example_count" do - it "is one" do - new_pending_example.example_count.should eq(1) - end - end - - describe "#[]" do - it "returns self" do - example = new_pending_example - example[0].should eq(example) - end - end - - describe "#to_s" do - it "contains #what" do - example = new_pending_example - example.to_s.should contain(example.what) - end - - it "contains the group's #what" do - group = new_nested_group - example = new_pending_example(group) - example.to_s.should contain(group.what.to_s) - end - end -end diff --git a/spec/pending_result_spec.cr b/spec/pending_result_spec.cr deleted file mode 100644 index c83b1fb..0000000 --- a/spec/pending_result_spec.cr +++ /dev/null @@ -1,53 +0,0 @@ -require "./spec_helper" - -def new_pending_result(example : Spectator::Example? = nil) - Spectator::PendingResult.new(example || FailingExample.create) -end - -describe Spectator::PendingResult do - describe "#call" do - context "without a block" do - it "invokes #pending on an instance" do - spy = ResultCallSpy.new - new_pending_result.call(spy) - spy.pending?.should be_true - end - - it "returns the value of #pending" do - result = new_pending_result - returned = result.call(ResultCallSpy.new) - returned.should eq(:pending) - end - end - - context "with a block" do - it "invokes #pending on an instance" do - spy = ResultCallSpy.new - new_pending_result.call(spy) { nil } - spy.pending?.should be_true - end - - it "yields itself" do - result = new_pending_result - value = nil.as(Spectator::Result?) - result.call(ResultCallSpy.new) { |r| value = r } - value.should eq(result) - end - - it "returns the value of #pending" do - result = new_pending_result - value = 42 - returned = result.call(ResultCallSpy.new) { value } - returned.should eq(value) - end - end - end - - describe "#example" do - it "is the expected value" do - example = PassingExample.create - result = new_pending_result(example: example) - result.example.should eq(example) - end - end -end diff --git a/spec/report_spec.cr b/spec/report_spec.cr deleted file mode 100644 index 5566674..0000000 --- a/spec/report_spec.cr +++ /dev/null @@ -1,237 +0,0 @@ -require "./spec_helper" - -def new_passing_result - example = PassingExample.create - elapsed = Time::Span.new(nanoseconds: 1_000_000) - expectations = Spectator::Expectations::ExampleExpectations.new([] of Spectator::Expectations::Expectation) - Spectator::SuccessfulResult.new(example, elapsed, expectations) -end - -def new_failure_result(result_type : Spectator::Result.class = Spectator::FailedResult) - example = FailingExample.create # Doesn't matter what type of example is used here. - elapsed = Time::Span.new(nanoseconds: 1_000_000) - expectations = Spectator::Expectations::ExampleExpectations.new([] of Spectator::Expectations::Expectation) - error = Exception.new("foobar") - result_type.new(example, elapsed, expectations, error) -end - -def new_results(successful_count = 5, failed_count = 5, error_count = 5, pending_count = 5) - total = successful_count + failed_count + error_count + pending_count - results = Array(Spectator::Result).new(total) - successful_count.times { results << new_passing_result } - failed_count.times { results << new_failure_result } - error_count.times { results << new_failure_result(Spectator::ErroredResult) } - pending_count.times { results << new_pending_result } - results -end - -def new_report(successful_count = 5, failed_count = 5, error_count = 5, pending_count = 5, overhead_time = 1_000_000i64, fail_blank = false) - results = new_results(successful_count, failed_count, error_count, pending_count) - example_runtime = results.compact_map(&.as?(Spectator::FinishedResult)).sum(&.elapsed) - total_runtime = example_runtime + Time::Span.new(nanoseconds: overhead_time) - Spectator::Report.new(results, total_runtime, fail_blank: fail_blank) -end - -describe Spectator::Report do - describe "#initialize(results)" do - describe "#runtime" do - it "is the sum of all results' runtimes" do - results = new_results - runtime = results.compact_map(&.as?(Spectator::FinishedResult)).sum(&.elapsed) - report = Spectator::Report.new(results) - report.runtime.should eq(runtime) - end - end - end - - describe "#each" do - it "yields all results" do - results = new_results - report = Spectator::Report.new(results) - # The `#each` method is tested through `Enumerable#to_a`. - report.to_a.should eq(results) - end - end - - describe "#runtime" do - it "is the expected value" do - span = Time::Span.new(10, 10, 10) - report = Spectator::Report.new([] of Spectator::Result, span) - report.runtime.should eq(span) - end - end - - describe "#example_count" do - it "is the expected value" do - report = new_report(5, 4, 3, 2) - report.example_count.should eq(14) - end - end - - describe "#examples_ran" do - it "is the number of non-skipped examples" do - report = new_report(5, 4, 3, 2) - report.examples_ran.should eq(12) - end - end - - describe "#successful_count" do - it "is the expected value" do - report = new_report(5, 4, 3, 2) - report.successful_count.should eq(5) - end - end - - describe "#failed_count" do - it "is the expected value" do - report = new_report(5, 4, 3, 2) - report.failed_count.should eq(7) - end - end - - describe "#error_count" do - it "is the expected value" do - report = new_report(5, 4, 3, 2) - report.error_count.should eq(3) - end - end - - describe "#pending_count" do - it "is the expected value" do - report = new_report(5, 4, 3, 2) - report.pending_count.should eq(2) - end - end - - describe "#remaining_count" do - it "is the expected value" do - results = [] of Spectator::Result - remaining = 5 - report = Spectator::Report.new(results, Time::Span.zero, remaining) - report.remaining_count.should eq(remaining) - end - end - - describe "#failed?" do - context "with a failed test suite" do - it "is true" do - report = new_report(5, 4, 3, 2) - report.failed?.should be_true - end - end - - context "with a passing test suite" do - it "is false" do - report = new_report(5, 0, 0, 0) - report.failed?.should be_false - end - end - - context "with fail-blank enabled" do - context "when no tests run" do - it "is true" do - report = new_report(0, 0, 0, 5, fail_blank: true) - report.failed?.should be_true - end - end - - context "when tests run" do - context "and there are failures" do - it "is true" do - report = new_report(5, 4, 3, 2, fail_blank: true) - report.failed?.should be_true - end - end - - context "and there are no failures" do - it "is false" do - report = new_report(5, 0, 0, 2, fail_blank: true) - report.failed?.should be_false - end - end - end - end - end - - describe "#remaining?" do - context "with remaining tests" do - it "is true" do - results = [] of Spectator::Result - report = Spectator::Report.new(results, Time::Span.zero, 5) - report.remaining?.should be_true - end - end - - context "without remaining tests" do - it "is false" do - results = [] of Spectator::Result - report = Spectator::Report.new(results, Time::Span.zero, 0) - report.remaining?.should be_false - end - end - end - - describe "#failures" do - it "returns the expected results" do - results = Array.new(5) { new_failure_result.as(Spectator::Result) } - report = Spectator::Report.new(results, Time::Span.zero) - report.failures.to_a.should eq(results) - end - - it "includes errors" do - results = Array(Spectator::Result).new(5) do |index| - if index.odd? - new_failure_result - else - new_failure_result(Spectator::ErroredResult) - end - end - report = Spectator::Report.new(results, Time::Span.zero) - report.failures.to_a.should eq(results) - end - end - - describe "#errors" do - it "returns the expected results" do - results = Array.new(5) { new_failure_result(Spectator::ErroredResult).as(Spectator::Result) } - report = Spectator::Report.new(results, Time::Span.zero) - report.errors.to_a.should eq(results) - end - - it "does not include failures" do - results = Array(Spectator::Result).new(5) do |index| - if index.odd? - new_failure_result - else - new_failure_result(Spectator::ErroredResult) - end - end - report = Spectator::Report.new(results, Time::Span.zero) - errors_only = results.select(&.is_a?(Spectator::ErroredResult)) - report.errors.to_a.should eq(errors_only) - end - end - - describe "#example_runtime" do - it "is the sum of all example run-times" do - passing_results = Array.new(5) { new_passing_result } - runtime = passing_results.sum(&.elapsed) - results = passing_results.map(&.as(Spectator::Result)) - total_runtime = runtime + Time::Span.new(nanoseconds: 1_234_567) - report = Spectator::Report.new(results, total_runtime) - report.example_runtime.should eq(runtime) - end - end - - describe "#overhead_time" do - it "is the difference between total runtime and the sum of all example run-times" do - passing_results = Array.new(5) { new_passing_result } - runtime = passing_results.sum(&.elapsed) - results = passing_results.map(&.as(Spectator::Result)) - overhead = Time::Span.new(nanoseconds: 1_234_567) - total_runtime = runtime + overhead - report = Spectator::Report.new(results, total_runtime) - report.overhead_time.should eq(overhead) - end - end -end diff --git a/spec/root_example_group_spec.cr b/spec/root_example_group_spec.cr deleted file mode 100644 index d69c80b..0000000 --- a/spec/root_example_group_spec.cr +++ /dev/null @@ -1,810 +0,0 @@ -require "./spec_helper" - -def new_root_group(hooks = Spectator::ExampleHooks.empty, conditions = Spectator::ExampleConditions.empty) - Spectator::RootExampleGroup.new(hooks, conditions).tap do |group| - group.children = [] of Spectator::ExampleComponent - end -end - -def root_group_with_examples(example_count = 5) - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - examples = [] of Spectator::Example - group.children = Array(Spectator::ExampleComponent).new(example_count) do - PassingExample.new(group, Spectator::Internals::SampleValues.empty).tap do |example| - examples << example - end - end - {group, examples} -end - -def root_group_with_sub_groups(sub_group_count = 5, example_count = 5) - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - examples = [] of Spectator::Example - group.children = Array(Spectator::ExampleComponent).new(sub_group_count) do |i| - Spectator::NestedExampleGroup.new(i.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group| - sub_group.children = Array(Spectator::ExampleComponent).new(example_count) do - PassingExample.new(group, Spectator::Internals::SampleValues.empty).tap do |example| - examples << example - end - end - end - end - {group, examples} -end - -def complex_root_group - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - examples = [] of Spectator::Example - group.children = Array(Spectator::ExampleComponent).new(10) do |i| - if i % 2 == 0 - PassingExample.new(group, Spectator::Internals::SampleValues.empty).tap do |example| - examples << example - end - else - Spectator::NestedExampleGroup.new(i.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group1| - sub_group1.children = Array(Spectator::ExampleComponent).new(10) do |j| - if i % 2 == 0 - PassingExample.new(sub_group1, Spectator::Internals::SampleValues.empty).tap do |example| - examples << example - end - else - Spectator::NestedExampleGroup.new(j.to_s, sub_group1, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group2| - sub_group2.children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(sub_group2, Spectator::Internals::SampleValues.empty).tap do |example| - examples << example - end - end - end - end - end - end - end - end - {group, examples} -end - -describe Spectator::RootExampleGroup do - describe "#run_before_hooks" do - it "runs a before_all hook" do - called = false - hooks = new_hooks(before_all: ->{ called = true; nil }) - group = new_root_group(hooks) - group.run_before_hooks - called.should be_true - end - - it "runs a before_each hook" do - called = false - hooks = new_hooks(before_each: ->{ called = true; nil }) - group = new_root_group(hooks) - group.run_before_hooks - called.should be_true - end - - it "runs multiple before_all hooks" do - call_count = 0 - hooks = new_hooks(before_all: [ - ->{ call_count += 1; nil }, - ->{ call_count += 2; nil }, - ->{ call_count += 3; nil }, - ]) - group = new_root_group(hooks) - group.run_before_hooks - call_count.should eq(6) - end - - it "runs multiple before_each hooks" do - call_count = 0 - hooks = new_hooks(before_each: [ - ->{ call_count += 1; nil }, - ->{ call_count += 2; nil }, - ->{ call_count += 3; nil }, - ]) - group = new_root_group(hooks) - group.run_before_hooks - call_count.should eq(6) - end - - it "runs hooks in the correct order" do - calls = [] of Symbol - hooks = new_hooks(before_all: [ - ->{ calls << :a; nil }, - ->{ calls << :b; nil }, - ->{ calls << :c; nil }, - ], - before_each: [ - ->{ calls << :d; nil }, - ->{ calls << :e; nil }, - ->{ calls << :f; nil }, - ]) - group = new_root_group(hooks) - group.run_before_hooks - calls.should eq(%i[a b c d e f]) - end - - it "runs the before_all hooks once" do - call_count = 0 - hooks = new_hooks(before_all: ->{ call_count += 1; nil }) - group = new_root_group(hooks) - 2.times { group.run_before_hooks } - call_count.should eq(1) - end - - it "runs the before_each hooks multiple times" do - call_count = 0 - hooks = new_hooks(before_each: ->{ call_count += 1; nil }) - group = new_root_group(hooks) - 2.times { group.run_before_hooks } - call_count.should eq(2) - end - end - - describe "#run_after_hooks" do - # No children are used for most of these examples. - # That's because `[].all?` is always true. - # Which means that all examples are considered finished, since there are none. - it "runs a single after_all hook" do - called = false - hooks = new_hooks(after_all: ->{ called = true; nil }) - group = new_root_group(hooks) - group.run_after_hooks - called.should be_true - end - - it "runs a single after_each hook" do - called = false - hooks = new_hooks(after_each: ->{ called = true; nil }) - group = new_root_group(hooks) - group.run_after_hooks - called.should be_true - end - - it "runs multiple after_all hooks" do - call_count = 0 - hooks = new_hooks(after_all: [ - ->{ call_count += 1; nil }, - ->{ call_count += 2; nil }, - ->{ call_count += 3; nil }, - ]) - group = new_root_group(hooks) - group.run_after_hooks - call_count.should eq(6) - end - - it "runs multiple after_each hooks" do - call_count = 0 - hooks = new_hooks(after_all: [ - ->{ call_count += 1; nil }, - ->{ call_count += 2; nil }, - ->{ call_count += 3; nil }, - ]) - group = new_root_group(hooks) - group.run_after_hooks - call_count.should eq(6) - end - - it "runs hooks in the correct order" do - calls = [] of Symbol - hooks = new_hooks(after_each: [ - ->{ calls << :a; nil }, - ->{ calls << :b; nil }, - ->{ calls << :c; nil }, - ], - after_all: [ - ->{ calls << :d; nil }, - ->{ calls << :e; nil }, - ->{ calls << :f; nil }, - ]) - group = new_root_group(hooks) - group.run_after_hooks - calls.should eq(%i[a b c d e f]) - end - - it "runs the after_all hooks once" do - call_count = 0 - hooks = new_hooks(after_all: ->{ call_count += 1; nil }) - group = new_root_group(hooks) - 2.times { group.run_after_hooks } - call_count.should eq(1) - end - - it "runs the after_each hooks multiple times" do - call_count = 0 - hooks = new_hooks(after_each: ->{ call_count += 1; nil }) - group = new_root_group(hooks) - 2.times { group.run_after_hooks } - call_count.should eq(2) - end - - context "with no examples finished" do - it "doesn't run the after_all hooks" do - called = false - hooks = new_hooks(after_all: ->{ called = true; nil }) - group = Spectator::RootExampleGroup.new(hooks, Spectator::ExampleConditions.empty) - group.children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(group, Spectator::Internals::SampleValues.empty) - end - group.run_after_hooks - called.should be_false - end - - it "runs the after_each hooks" do - called = false - hooks = new_hooks(after_each: ->{ called = true; nil }) - group = Spectator::RootExampleGroup.new(hooks, Spectator::ExampleConditions.empty) - group.children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(group, Spectator::Internals::SampleValues.empty) - end - group.run_after_hooks - called.should be_true - end - end - - context "with some examples finished" do - it "doesn't run the after_all hooks" do - called = false - examples = [] of Spectator::Example - hooks = new_hooks(after_all: ->{ called = true; nil }) - group = Spectator::RootExampleGroup.new(hooks, Spectator::ExampleConditions.empty) - group.children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(group, Spectator::Internals::SampleValues.empty).tap do |example| - examples << example - end - end - examples.each_with_index do |example, index| - Spectator::Internals::Harness.run(example) if index % 2 == 0 - end - group.run_after_hooks - called.should be_false - end - - it "runs the after_each hooks" do - called = false - examples = [] of Spectator::Example - hooks = new_hooks(after_each: ->{ called = true; nil }) - group = Spectator::RootExampleGroup.new(hooks, Spectator::ExampleConditions.empty) - group.children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(group, Spectator::Internals::SampleValues.empty).tap do |example| - examples << example - end - end - examples.each_with_index do |example, index| - Spectator::Internals::Harness.run(example) if index % 2 == 0 - end - group.run_after_hooks - called.should be_true - end - end - end - - describe "#wrap_around_each_hooks" do - it "wraps the block" do - called = false - wrapper = new_root_group.wrap_around_each_hooks do - called = true - end - wrapper.call - called.should be_true - end - - it "wraps a proc" do - called = false - hooks = new_hooks(around_each: ->(proc : ->) { called = true; proc.call }) - wrapper = new_root_group(hooks).wrap_around_each_hooks { } - wrapper.call - called.should be_true - end - - it "wraps multiple procs" do - call_count = 0 - hooks = new_hooks(around_each: [ - ->(proc : ->) { call_count += 1; proc.call }, - ->(proc : ->) { call_count += 2; proc.call }, - ->(proc : ->) { call_count += 3; proc.call }, - ]) - wrapper = new_root_group(hooks).wrap_around_each_hooks { } - wrapper.call - call_count.should eq(6) - end - - it "wraps procs in the correct order" do - calls = [] of Symbol - hooks = new_hooks(around_each: [ - ->(proc : ->) { calls << :a; proc.call }, - ->(proc : ->) { calls << :b; proc.call }, - ->(proc : ->) { calls << :c; proc.call }, - ]) - wrapper = new_root_group(hooks).wrap_around_each_hooks { } - wrapper.call - calls.should eq(%i[a b c]) - end - end - - describe "#run_pre_conditions" do - it "runs a condition" do - called = false - conditions = new_conditions(pre: ->{ called = true; nil }) - group = new_root_group(conditions: conditions) - group.run_pre_conditions - called.should be_true - end - - it "runs multiple conditions" do - call_count = 0 - conditions = new_conditions(pre: [ - ->{ call_count += 1; nil }, - ->{ call_count += 2; nil }, - ->{ call_count += 3; nil }, - ]) - group = new_root_group(conditions: conditions) - group.run_pre_conditions - call_count.should eq(6) - end - - it "runs conditions in the correct order" do - calls = [] of Symbol - conditions = new_conditions(pre: [ - ->{ calls << :a; nil }, - ->{ calls << :b; nil }, - ->{ calls << :c; nil }, - ]) - group = new_root_group(conditions: conditions) - group.run_pre_conditions - calls.should eq(%i[a b c]) - end - - it "runs the conditions multiple times" do - call_count = 0 - conditions = new_conditions(pre: ->{ call_count += 1; nil }) - group = new_root_group(conditions: conditions) - 2.times { group.run_pre_conditions } - call_count.should eq(2) - end - end - - describe "#run_post_conditions" do - it "runs a single condition" do - called = false - conditions = new_conditions(post: ->{ called = true; nil }) - group = new_root_group(conditions: conditions) - group.run_post_conditions - called.should be_true - end - - it "runs multiple conditions" do - call_count = 0 - conditions = new_conditions(post: [ - ->{ call_count += 1; nil }, - ->{ call_count += 2; nil }, - ->{ call_count += 3; nil }, - ]) - group = new_root_group(conditions: conditions) - group.run_post_conditions - call_count.should eq(6) - end - - it "runs conditions in the correct order" do - calls = [] of Symbol - conditions = new_conditions(post: [ - ->{ calls << :a; nil }, - ->{ calls << :b; nil }, - ->{ calls << :c; nil }, - ]) - group = new_root_group(conditions: conditions) - group.run_post_conditions - calls.should eq(%i[a b c]) - end - - it "runs the conditions multiple times" do - call_count = 0 - conditions = new_conditions(post: ->{ call_count += 1; nil }) - group = new_root_group(conditions: conditions) - 2.times { group.run_post_conditions } - call_count.should eq(2) - end - end - - describe "#to_s" do - it "is empty" do - new_root_group.to_s.should be_empty - end - end - - describe "#children" do - it "raises an error when not set" do - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - expect_raises(Exception) { group.children } - end - - it "returns the expected set" do - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(group, Spectator::Internals::SampleValues.empty) - end - group.children = children - group.children.should eq(children) - end - end - - describe "#children=" do - it "raises an error trying to reset" do - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(group, Spectator::Internals::SampleValues.empty) - end - group.children = children - expect_raises(Exception) { group.children = children } - end - end - - describe "#each" do - it "yields each child" do - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group.children = Array(Spectator::ExampleComponent).new(5) do |i| - if i % 2 == 0 - PassingExample.new(group, Spectator::Internals::SampleValues.empty) - else - Spectator::NestedExampleGroup.new(i.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group| - sub_group.children = [] of Spectator::ExampleComponent - end - end - end - group.to_a.should eq(group.children) - end - - it "doesn't yield children of children" do - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group.children = Array(Spectator::ExampleComponent).new(5) do |i| - if i % 2 == 0 - PassingExample.new(group, Spectator::Internals::SampleValues.empty) - else - Spectator::NestedExampleGroup.new(i.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group| - sub_group.children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(sub_group, Spectator::Internals::SampleValues.empty) - end - end - end - end - group.to_a.should eq(group.children) - end - end - - describe "#each : Iterator" do - it "iterates over each child" do - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group.children = Array(Spectator::ExampleComponent).new(5) do |i| - if i % 2 == 0 - PassingExample.new(group, Spectator::Internals::SampleValues.empty) - else - Spectator::NestedExampleGroup.new(i.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group| - sub_group.children = [] of Spectator::ExampleComponent - end - end - end - group.each.to_a.should eq(group.children) - end - - it "doesn't iterate over children of children" do - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group.children = Array(Spectator::ExampleComponent).new(5) do |i| - if i % 2 == 0 - PassingExample.new(group, Spectator::Internals::SampleValues.empty) - else - Spectator::NestedExampleGroup.new(i.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group| - sub_group.children = Array(Spectator::ExampleComponent).new(5) do - PassingExample.new(sub_group, Spectator::Internals::SampleValues.empty) - end - end - end - end - group.each.to_a.should eq(group.children) - end - end - - describe "#example_count" do - context "with no examples" do - it "is zero" do - new_root_group.example_count.should eq(0) - end - end - - context "with empty sub-groups" do - it "is zero" do - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group.children = Array(Spectator::ExampleComponent).new(5) do |i| - Spectator::NestedExampleGroup.new(i.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - end - group.example_count.should eq(0) - end - end - - context "with direct descendant examples" do - it "equals the number of examples" do - example_count = 10 - group, _ = root_group_with_examples(example_count) - group.example_count.should eq(example_count) - end - end - - context "with examples sub-groups" do - it "equals the total number of examples" do - sub_group_count = 3 - example_count = 10 - group, _ = root_group_with_sub_groups(sub_group_count, example_count) - group.example_count.should eq(sub_group_count * example_count) - end - end - - context "with examples at all levels" do - it "equals the total number of examples" do - group, examples = complex_root_group - group.example_count.should eq(examples.size) - end - end - end - - describe "#[]" do - context "when empty" do - it "raises an error" do - group = new_root_group - expect_raises(IndexError) { group[0] } - end - end - - context "with direct descendant examples" do - context "given 0" do - it "returns the first example" do - group, examples = root_group_with_examples - group[0].should eq(examples.first) - end - end - - context "given -1" do - it "returns the last example" do - group, examples = root_group_with_examples - group[-1].should eq(examples.last) - end - end - - context "given an in-bounds positive index" do - it "returns the expected example" do - group, examples = root_group_with_examples(10) - group[3].should eq(examples[3]) - end - end - - context "given an in-bounds negative index" do - it "returns the expected example" do - group, examples = root_group_with_examples(10) - group[-3].should eq(examples[-3]) - end - end - - context "an out-of-bounds positive index" do - it "raises an index error" do - group, _ = root_group_with_examples(10) - expect_raises(IndexError) { group[15] } - end - - it "handles off-by-one" do - group, _ = root_group_with_examples(10) - expect_raises(IndexError) { group[10] } - end - end - - context "an out-of-bounds negative index" do - it "raises an index error" do - group, _ = root_group_with_examples(10) - expect_raises(IndexError) { group[-15] } - end - - it "handles off-by-one" do - group, _ = root_group_with_examples(10) - expect_raises(IndexError) { group[-11] } - end - end - end - - context "with examples only in sub-groups" do - context "given 0" do - it "returns the first example" do - group, examples = root_group_with_sub_groups - group[0].should eq(examples.first) - end - end - - context "given -1" do - it "returns the last example" do - group, examples = root_group_with_sub_groups - group[-1].should eq(examples.last) - end - end - - context "given an in-bounds positive index" do - it "returns the expected example" do - group, examples = root_group_with_sub_groups(10, 2) - group[6].should eq(examples[6]) - end - end - - context "given an in-bounds negative index" do - it "returns the expected example" do - group, examples = root_group_with_sub_groups(10, 2) - group[-6].should eq(examples[-6]) - end - end - - context "an out-of-bounds positive index" do - it "raises an index error" do - group, _ = root_group_with_sub_groups(10, 2) - expect_raises(IndexError) { group[25] } - end - - it "handles off-by-one" do - group, _ = root_group_with_sub_groups(10, 2) - expect_raises(IndexError) { group[20] } - end - end - - context "an out-of-bounds negative index" do - it "raises an index error" do - group, _ = root_group_with_sub_groups(10, 2) - expect_raises(IndexError) { group[-25] } - end - - it "handles off-by-one" do - group, _ = root_group_with_sub_groups(10, 2) - expect_raises(IndexError) { group[-21] } - end - end - end - - context "with examples at all levels" do - context "given 0" do - it "returns the first example" do - group, examples = complex_root_group - group[0].should eq(examples.first) - end - end - - context "given -1" do - it "returns the last example" do - group, examples = complex_root_group - group[-1].should eq(examples.last) - end - end - - context "given an in-bounds positive index" do - it "returns the expected example" do - group, examples = complex_root_group - group[42].should eq(examples[42]) - end - end - - context "given an in-bounds negative index" do - it "returns the expected example" do - group, examples = complex_root_group - group[-42].should eq(examples[-42]) - end - end - - context "an out-of-bounds positive index" do - it "raises an index error" do - group, examples = complex_root_group - expect_raises(IndexError) { group[examples.size + 5] } - end - - it "handles off-by-one" do - group, examples = complex_root_group - expect_raises(IndexError) { group[examples.size] } - end - end - - context "an out-of-bounds negative index" do - it "raises an index error" do - group, examples = complex_root_group - expect_raises(IndexError) { group[-examples.size - 5] } - end - - it "handles off-by-one" do - group, examples = complex_root_group - expect_raises(IndexError) { group[-examples.size - 1] } - end - end - end - - context "with only sub-groups and no examples" do - it "raises an index error" do - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group.children = Array(Spectator::ExampleComponent).new(5) do |i| - Spectator::NestedExampleGroup.new(i.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group| - sub_group.children = [] of Spectator::ExampleComponent - end - end - expect_raises(IndexError) { group[0] } - end - end - end - - describe "#finished?" do - context "with no children" do - it "is true" do - new_root_group.finished?.should be_true - end - end - - context "with all unfinished children" do - it "is false" do - group, _ = root_group_with_examples - group.finished?.should be_false - end - end - - context "with some finished children" do - it "is false" do - group, examples = root_group_with_examples - examples.each_with_index do |example, index| - Spectator::Internals::Harness.run(example) if index % 2 == 0 - end - group.finished?.should be_false - end - end - - context "with all finished children" do - it "is true" do - group, examples = root_group_with_examples - examples.each do |example| - Spectator::Internals::Harness.run(example) - end - group.finished?.should be_true - end - end - - context "with a sub-group" do - context "with no children" do - it "is true" do - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group.children = Array(Spectator::ExampleComponent).new(5) do |i| - Spectator::NestedExampleGroup.new(i.to_s, group, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty).tap do |sub_group| - sub_group.children = [] of Spectator::ExampleComponent - end - end - group.finished?.should be_true - end - end - - context "with all unfinished children" do - it "is false" do - group, _ = root_group_with_sub_groups - group.finished?.should be_false - end - end - - context "with some finished children" do - it "is false" do - group, examples = root_group_with_sub_groups - examples.each_with_index do |example, index| - Spectator::Internals::Harness.run(example) if index % 2 == 0 - end - group.finished?.should be_false - end - end - - context "with all finished children" do - it "is true" do - group, examples = root_group_with_sub_groups - examples.each do |example| - Spectator::Internals::Harness.run(example) - end - group.finished?.should be_true - end - end - end - end - - describe "#symbolic?" do - it "is true" do - new_root_group.symbolic?.should be_true - end - end -end diff --git a/spec/runnable_example_spec.cr b/spec/runnable_example_spec.cr deleted file mode 100644 index 5907454..0000000 --- a/spec/runnable_example_spec.cr +++ /dev/null @@ -1,1568 +0,0 @@ -require "./spec_helper" - -def new_runnable_example(group : Spectator::ExampleGroup? = nil, symbolic = false) - actual_group = group || Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - PassingExample.new(actual_group, Spectator::Internals::SampleValues.empty, symbolic).tap do |example| - actual_group.children = [example.as(Spectator::ExampleComponent)] - end -end - -def run_example(example_type : Spectator::Example.class, hooks : Spectator::ExampleHooks? = nil, conditions : Spectator::ExampleConditions? = nil) - group = Spectator::RootExampleGroup.new(hooks || Spectator::ExampleHooks.empty, conditions || Spectator::ExampleConditions.empty) - run_example(example_type, group) -end - -def run_example(example_type : Spectator::Example.class, group : Spectator::ExampleGroup? = nil) - actual_group = group || Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - example = example_type.new(actual_group, Spectator::Internals::SampleValues.empty) - actual_group.children = [example.as(Spectator::ExampleComponent)] - Spectator::Internals::Harness.run(example) -end - -def run_example(hooks : Spectator::ExampleHooks? = nil, conditions : Spectator::ExampleConditions? = nil, &block) - example = SpyExample.create(hooks || Spectator::ExampleHooks.empty, conditions || Spectator::ExampleConditions.empty, &block) - Spectator::Internals::Harness.run(example) -end - -describe Spectator::RunnableExample do - describe "#run" do - context "with a passing test" do - it "returns a successful result" do - run_example(PassingExample).should be_a(Spectator::SuccessfulResult) - end - - {% for hook_type in %i[before_all before_each] %} - context "{{hook_type.id}} hooks" do - it "runs a hook" do - called = false - hooks = new_hooks({{hook_type.id}}: ->{ called = true; nil }) - run_example(PassingExample, hooks) - called.should be_true - end - - it "runs multiple hooks" do - call_count = 0 - hooks = new_hooks({{hook_type.id}}: [ - ->{ call_count += 1; nil }, - ->{ call_count += 2; nil }, - ->{ call_count += 3; nil }, - ]) - run_example(PassingExample, hooks) - call_count.should eq(6) - end - - it "runs them in the correct order" do - calls = [] of Symbol - hooks = new_hooks({{hook_type.id}}: [ - ->{ calls << :a; nil }, - ->{ calls << :b; nil }, - ->{ calls << :c; nil }, - ]) - run_example(PassingExample, hooks) - calls.should eq(\%i[a b c]) - end - - it "runs parent group hooks" do - called = false - hooks = new_hooks({{hook_type.id}}: -> { called = true; nil }) - root = Spectator::RootExampleGroup.new(hooks, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(PassingExample, group) - called.should be_true - end - - it "runs parent group hooks first" do - calls = [] of Symbol - root_hooks = new_hooks({{hook_type.id}}: -> { calls << :a; nil }) - group_hooks = new_hooks({{hook_type.id}}: -> { calls << :b; nil }) - root = Spectator::RootExampleGroup.new(root_hooks, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, group_hooks, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(PassingExample, group) - calls.should eq(\%i[a b]) - end - - it "runs the hook before the test code" do - calls = [] of Symbol - hooks = new_hooks({{hook_type.id}}: ->{ calls << :a; nil }) - run_example(hooks) do - calls << :b - end - calls.should eq(\%i[a b]) - end - end - {% end %} - - it "runs before_all hooks prior to before_each hooks" do - calls = [] of Symbol - hooks = new_hooks( - before_all: ->{ calls << :a; nil }, - before_each: ->{ calls << :b; nil } - ) - run_example(PassingExample, hooks) - calls.should eq(%i[a b]) - end - - {% for hook_type in %i[after_all after_each] %} - context "{{hook_type.id}} hooks" do - it "runs a hook" do - called = false - hooks = new_hooks({{hook_type.id}}: ->{ called = true; nil }) - run_example(PassingExample, hooks) - called.should be_true - end - - it "runs multiple hooks" do - call_count = 0 - hooks = new_hooks({{hook_type.id}}: [ - ->{ call_count += 1; nil }, - ->{ call_count += 2; nil }, - ->{ call_count += 3; nil }, - ]) - run_example(PassingExample, hooks) - call_count.should eq(6) - end - - it "runs them in the correct order" do - calls = [] of Symbol - hooks = new_hooks({{hook_type.id}}: [ - ->{ calls << :a; nil }, - ->{ calls << :b; nil }, - ->{ calls << :c; nil }, - ]) - run_example(PassingExample, hooks) - calls.should eq(\%i[a b c]) - end - - it "runs parent group hooks" do - called = false - hooks = new_hooks({{hook_type.id}}: -> { called = true; nil }) - root = Spectator::RootExampleGroup.new(hooks, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(PassingExample, group) - called.should be_true - end - - it "runs parent group hooks last" do - calls = [] of Symbol - root_hooks = new_hooks({{hook_type.id}}: -> { calls << :a; nil }) - group_hooks = new_hooks({{hook_type.id}}: -> { calls << :b; nil }) - root = Spectator::RootExampleGroup.new(root_hooks, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, group_hooks, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(PassingExample, group) - calls.should eq(\%i[b a]) - end - - it "runs the hook after the test code" do - calls = [] of Symbol - hooks = new_hooks({{hook_type.id}}: ->{ calls << :a; nil }) - run_example(hooks) do - calls << :b - end - calls.should eq(\%i[b a]) - end - end - {% end %} - - it "runs after_each hooks prior to after_all hooks" do - calls = [] of Symbol - hooks = new_hooks( - after_each: ->{ calls << :a; nil }, - after_all: ->{ calls << :b; nil } - ) - run_example(PassingExample, hooks) - calls.should eq(%i[a b]) - end - - context "around_each hooks" do - it "runs a hook" do - called = false - hooks = new_hooks(around_each: ->(proc : ->) { called = true; proc.call }) - run_example(PassingExample, hooks) - called.should be_true - end - - it "runs multiple hooks" do - call_count = 0 - hooks = new_hooks(around_each: [ - ->(proc : ->) { call_count += 1; proc.call }, - ->(proc : ->) { call_count += 2; proc.call }, - ->(proc : ->) { call_count += 3; proc.call }, - ]) - run_example(PassingExample, hooks) - call_count.should eq(6) - end - - it "runs them in the correct order" do - calls = [] of Symbol - hooks = new_hooks(around_each: [ - ->(proc : ->) { calls << :a; proc.call }, - ->(proc : ->) { calls << :b; proc.call }, - ->(proc : ->) { calls << :c; proc.call }, - ]) - run_example(PassingExample, hooks) - calls.should eq(%i[a b c]) - end - - it "runs parent group hooks" do - called = false - hooks = new_hooks(around_each: ->(proc : ->) { called = true; proc.call }) - root = Spectator::RootExampleGroup.new(hooks, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(PassingExample, group) - called.should be_true - end - - it "runs parent group hooks first" do - calls = [] of Symbol - root_hooks = new_hooks(around_each: ->(proc : ->) { calls << :a; proc.call }) - group_hooks = new_hooks(around_each: ->(proc : ->) { calls << :b; proc.call }) - root = Spectator::RootExampleGroup.new(root_hooks, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, group_hooks, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(PassingExample, group) - calls.should eq(%i[a b]) - end - - it "runs around_each hooks after the before hooks" do - calls = [] of Symbol - hooks = new_hooks( - before_all: ->{ calls << :b; nil }, - before_each: ->{ calls << :b; nil }, - around_each: ->(proc : ->) { calls << :c; proc.call } - ) - run_example(PassingExample, hooks) - calls.should eq(%i[b b c]) - end - - it "runs around_each hooks prior to after hooks" do - calls = [] of Symbol - hooks = new_hooks( - around_each: ->(proc : ->) { calls << :c; proc.call }, - after_each: ->{ calls << :a; nil }, - after_all: ->{ calls << :a; nil }, - ) - run_example(PassingExample, hooks) - calls.should eq(%i[c a a]) - end - end - - {% for condition in %i[pre post] %} - context "{{condition.id}}-conditions" do - it "checks a single condition" do - called = false - conditions = new_conditions({{condition.id}}: -> { called = true; nil }) - run_example(PassingExample, conditions: conditions) - called.should be_true - end - - it "checks multiple conditions" do - call_count = 0 - conditions = new_conditions({{condition.id}}: [ - -> { call_count += 1; nil }, - -> { call_count += 2; nil }, - -> { call_count += 3; nil }, - ]) - run_example(PassingExample, conditions: conditions) - call_count.should eq(6) - end - - it "checks them in the correct order" do - calls = [] of Symbol - conditions = new_conditions({{condition.id}}: [ - -> { calls << :a; nil }, - -> { calls << :b; nil }, - -> { calls << :c; nil }, - ]) - run_example(PassingExample, conditions: conditions) - calls.should eq(\%i[a b c]) - end - - it "checks parent group conditions" do - called = false - conditions = new_conditions({{condition.id}}: -> { called = true; nil }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, conditions) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(PassingExample, group) - called.should be_true - end - - {% if condition == :pre %} - it "runs prior to the test code" do - calls = [] of Symbol - conditions = new_conditions({{condition.id}}: ->{ calls << :a; nil }) - run_example(conditions: conditions) do - calls << :b - end - calls.should eq(\%i[a b]) - end - - it "checks parent group conditions first" do - calls = [] of Symbol - root_conditions = new_conditions({{condition.id}}: -> { calls << :a; nil }) - group_conditions = new_conditions({{condition.id}}: -> { calls << :b; nil }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, root_conditions) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, group_conditions) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(PassingExample, group) - calls.should eq(\%i[a b]) - end - {% else %} - it "runs after the test code" do - calls = [] of Symbol - conditions = new_conditions({{condition.id}}: ->{ calls << :b; nil }) - run_example(conditions: conditions) do - calls << :a - end - calls.should eq(\%i[a b]) - end - - it "checks parent group conditions last" do - calls = [] of Symbol - root_conditions = new_conditions({{condition.id}}: -> { calls << :a; nil }) - group_conditions = new_conditions({{condition.id}}: -> { calls << :b; nil }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, root_conditions) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, group_conditions) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(PassingExample, group) - calls.should eq(\%i[b a]) - end - {% end %} - end - {% end %} - - it "runs before hooks prior to pre-conditions" do - calls = [] of Symbol - hooks = new_hooks( - before_all: ->{ calls << :b; nil }, - before_each: ->{ calls << :b; nil } - ) - conditions = new_conditions(pre: ->{ calls << :p; nil }) - run_example(PassingExample, hooks, conditions) - calls.should eq(%i[b b p]) - end - - it "runs around_each hooks prior to pre-conditions" do - calls = [] of Symbol - hooks = new_hooks( - around_each: ->(proc : ->) { calls << :c; proc.call } - ) - conditions = new_conditions(pre: ->{ calls << :p; nil }) - run_example(PassingExample, hooks, conditions) - calls.should eq(%i[c p]) - end - - it "runs post-conditions prior to after hooks" do - calls = [] of Symbol - hooks = new_hooks( - after_all: ->{ calls << :a; nil }, - after_each: ->{ calls << :a; nil } - ) - conditions = new_conditions(pre: ->{ calls << :p; nil }) - run_example(PassingExample, hooks, conditions) - calls.should eq(%i[p a a]) - end - - context "failing pre-condition" do - it "fails the test" do - conditions = new_conditions(pre: ->{ report_expectations(0, 1) }) - result = run_example(PassingExample, conditions: conditions) - result.should be_a(Spectator::FailedResult) - end - - it "prevents the test code from running" do - called = false - conditions = new_conditions(pre: ->{ report_expectations(0, 1) }) - run_example(conditions: conditions) do - called = true - end - called.should be_false - end - - it "prevents additional pre-conditions from running" do - called = false - conditions = new_conditions(pre: [ - ->{ report_expectations(0, 1) }, - ->{ called = true; nil }, - ]) - run_example(PassingExample, conditions: conditions) - called.should be_false - end - - it "prevents additional post-conditions from running" do - called = false - conditions = new_conditions( - pre: ->{ report_expectations(0, 1) }, - post: ->{ called = true; nil } - ) - run_example(PassingExample, conditions: conditions) - called.should be_false - end - - context "in a parent group" do - it "fails the test" do - conditions = new_conditions(pre: ->{ report_expectations(0, 1) }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, conditions) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - result = run_example(PassingExample, group) - result.should be_a(Spectator::FailedResult) - end - - it "prevents the test code from running" do - called = false - conditions = new_conditions(pre: ->{ report_expectations(0, 1) }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, conditions) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - example = SpyExample.new(group, Spectator::Internals::SampleValues.empty) - group.children = [example.as(Spectator::ExampleComponent)] - example.block = ->{ called = true; nil } - Spectator::Internals::Harness.run(example) - called.should be_false - end - - it "doesn't run child pre-conditions" do - called = false - root_conditions = new_conditions(pre: ->{ report_expectations(0, 1) }) - group_conditions = new_conditions(pre: ->{ called = true; nil }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, root_conditions) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, group_conditions) - root.children = [group.as(Spectator::ExampleComponent)] - example = PassingExample.new(group, Spectator::Internals::SampleValues.empty) - group.children = [example.as(Spectator::ExampleComponent)] - Spectator::Internals::Harness.run(example) - called.should be_false - end - - it "doesn't run child post-conditions" do - called = false - root_conditions = new_conditions(pre: ->{ report_expectations(0, 1) }) - group_conditions = new_conditions(post: ->{ called = true; nil }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, root_conditions) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, group_conditions) - root.children = [group.as(Spectator::ExampleComponent)] - example = PassingExample.new(group, Spectator::Internals::SampleValues.empty) - group.children = [example.as(Spectator::ExampleComponent)] - Spectator::Internals::Harness.run(example) - called.should be_false - end - end - end - - context "failing post-condition" do - it "fails the test" do - conditions = new_conditions(post: ->{ report_expectations(0, 1) }) - result = run_example(PassingExample, conditions: conditions) - result.should be_a(Spectator::FailedResult) - end - - it "prevents additional post-conditions from running" do - called = false - conditions = new_conditions(post: [ - ->{ report_expectations(0, 1) }, - ->{ called = true; nil }, - ]) - run_example(PassingExample, conditions: conditions) - called.should be_false - end - - context "in a parent group" do - it "fails the test" do - conditions = new_conditions(post: ->{ report_expectations(0, 1) }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, conditions) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - result = run_example(PassingExample, group) - result.should be_a(Spectator::FailedResult) - end - - it "doesn't run parent post-conditions" do - called = false - root_conditions = new_conditions(post: ->{ called = true; nil }) - group_conditions = new_conditions(post: ->{ report_expectations(0, 1) }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, root_conditions) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, group_conditions) - root.children = [group.as(Spectator::ExampleComponent)] - example = PassingExample.new(group, Spectator::Internals::SampleValues.empty) - group.children = [example.as(Spectator::ExampleComponent)] - Spectator::Internals::Harness.run(example) - called.should be_false - end - end - end - end - - context "with a failing test" do - it "returns a failed result" do - run_example(FailingExample).should be_a(Spectator::FailedResult) - end - - {% for hook_type in %i[before_all before_each] %} - context "{{hook_type.id}} hooks" do - it "runs a hook" do - called = false - hooks = new_hooks({{hook_type.id}}: ->{ called = true; nil }) - run_example(FailingExample, hooks) - called.should be_true - end - - it "runs multiple hooks" do - call_count = 0 - hooks = new_hooks({{hook_type.id}}: [ - ->{ call_count += 1; nil }, - ->{ call_count += 2; nil }, - ->{ call_count += 3; nil }, - ]) - run_example(FailingExample, hooks) - call_count.should eq(6) - end - - it "runs them in the correct order" do - calls = [] of Symbol - hooks = new_hooks({{hook_type.id}}: [ - ->{ calls << :a; nil }, - ->{ calls << :b; nil }, - ->{ calls << :c; nil }, - ]) - run_example(FailingExample, hooks) - calls.should eq(\%i[a b c]) - end - - it "runs parent group hooks" do - called = false - hooks = new_hooks({{hook_type.id}}: -> { called = true; nil }) - root = Spectator::RootExampleGroup.new(hooks, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(FailingExample, group) - called.should be_true - end - - it "runs parent group hooks first" do - calls = [] of Symbol - root_hooks = new_hooks({{hook_type.id}}: -> { calls << :a; nil }) - group_hooks = new_hooks({{hook_type.id}}: -> { calls << :b; nil }) - root = Spectator::RootExampleGroup.new(root_hooks, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, group_hooks, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(FailingExample, group) - calls.should eq(\%i[a b]) - end - end - {% end %} - - it "runs before_all hooks prior to before_each hooks" do - calls = [] of Symbol - hooks = new_hooks( - before_all: ->{ calls << :a; nil }, - before_each: ->{ calls << :b; nil } - ) - run_example(FailingExample, hooks) - calls.should eq(%i[a b]) - end - - {% for hook_type in %i[after_all after_each] %} - context "{{hook_type.id}} hooks" do - it "runs a hook" do - called = false - hooks = new_hooks({{hook_type.id}}: ->{ called = true; nil }) - run_example(FailingExample, hooks) - called.should be_true - end - - it "runs multiple hooks" do - call_count = 0 - hooks = new_hooks({{hook_type.id}}: [ - ->{ call_count += 1; nil }, - ->{ call_count += 2; nil }, - ->{ call_count += 3; nil }, - ]) - run_example(FailingExample, hooks) - call_count.should eq(6) - end - - it "runs them in the correct order" do - calls = [] of Symbol - hooks = new_hooks({{hook_type.id}}: [ - ->{ calls << :a; nil }, - ->{ calls << :b; nil }, - ->{ calls << :c; nil }, - ]) - run_example(FailingExample, hooks) - calls.should eq(\%i[a b c]) - end - - it "runs parent group hooks" do - called = false - hooks = new_hooks({{hook_type.id}}: -> { called = true; nil }) - root = Spectator::RootExampleGroup.new(hooks, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(FailingExample, group) - called.should be_true - end - - it "runs parent group hooks last" do - calls = [] of Symbol - root_hooks = new_hooks({{hook_type.id}}: -> { calls << :a; nil }) - group_hooks = new_hooks({{hook_type.id}}: -> { calls << :b; nil }) - root = Spectator::RootExampleGroup.new(root_hooks, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, group_hooks, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(FailingExample, group) - calls.should eq(\%i[b a]) - end - end - {% end %} - - it "runs after_each hooks prior to after_all hooks" do - calls = [] of Symbol - hooks = new_hooks( - after_each: ->{ calls << :a; nil }, - after_all: ->{ calls << :b; nil } - ) - run_example(FailingExample, hooks) - calls.should eq(%i[a b]) - end - - context "around_each hooks" do - it "runs a hook" do - called = false - hooks = new_hooks(around_each: ->(proc : ->) { called = true; proc.call }) - run_example(FailingExample, hooks) - called.should be_true - end - - it "runs multiple hooks" do - call_count = 0 - hooks = new_hooks(around_each: [ - ->(proc : ->) { call_count += 1; proc.call }, - ->(proc : ->) { call_count += 2; proc.call }, - ->(proc : ->) { call_count += 3; proc.call }, - ]) - run_example(FailingExample, hooks) - call_count.should eq(6) - end - - it "runs them in the correct order" do - calls = [] of Symbol - hooks = new_hooks(around_each: [ - ->(proc : ->) { calls << :a; proc.call }, - ->(proc : ->) { calls << :b; proc.call }, - ->(proc : ->) { calls << :c; proc.call }, - ]) - run_example(FailingExample, hooks) - calls.should eq(%i[a b c]) - end - - it "runs parent group hooks" do - called = false - hooks = new_hooks(around_each: ->(proc : ->) { called = true; proc.call }) - root = Spectator::RootExampleGroup.new(hooks, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(FailingExample, group) - called.should be_true - end - - it "runs parent group hooks first" do - calls = [] of Symbol - root_hooks = new_hooks(around_each: ->(proc : ->) { calls << :a; proc.call }) - group_hooks = new_hooks(around_each: ->(proc : ->) { calls << :b; proc.call }) - root = Spectator::RootExampleGroup.new(root_hooks, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, group_hooks, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(FailingExample, group) - calls.should eq(%i[a b]) - end - - it "runs around_each hooks after the before hooks" do - calls = [] of Symbol - hooks = new_hooks( - before_all: ->{ calls << :b; nil }, - before_each: ->{ calls << :b; nil }, - around_each: ->(proc : ->) { calls << :c; proc.call } - ) - run_example(FailingExample, hooks) - calls.should eq(%i[b b c]) - end - - it "runs around_each hooks prior to after hooks" do - calls = [] of Symbol - hooks = new_hooks( - around_each: ->(proc : ->) { calls << :c; proc.call }, - after_each: ->{ calls << :a; nil }, - after_all: ->{ calls << :a; nil }, - ) - run_example(FailingExample, hooks) - calls.should eq(%i[c a a]) - end - end - - context "pre-conditions" do - it "checks a single condition" do - called = false - conditions = new_conditions(pre: ->{ called = true; nil }) - run_example(FailingExample, conditions: conditions) - called.should be_true - end - - it "checks multiple conditions" do - call_count = 0 - conditions = new_conditions(pre: [ - ->{ call_count += 1; nil }, - ->{ call_count += 2; nil }, - ->{ call_count += 3; nil }, - ]) - run_example(FailingExample, conditions: conditions) - call_count.should eq(6) - end - - it "checks them in the correct order" do - calls = [] of Symbol - conditions = new_conditions(pre: [ - ->{ calls << :a; nil }, - ->{ calls << :b; nil }, - ->{ calls << :c; nil }, - ]) - run_example(FailingExample, conditions: conditions) - calls.should eq(%i[a b c]) - end - - it "checks parent group conditions" do - called = false - conditions = new_conditions(pre: ->{ called = true; nil }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, conditions) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(FailingExample, group) - called.should be_true - end - - it "checks parent group conditions first" do - calls = [] of Symbol - root_conditions = new_conditions(pre: ->{ calls << :a; nil }) - group_conditions = new_conditions(pre: ->{ calls << :b; nil }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, root_conditions) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, group_conditions) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(FailingExample, group) - calls.should eq(%i[a b]) - end - end - - it "runs before hooks prior to pre-conditions" do - calls = [] of Symbol - hooks = new_hooks( - before_all: ->{ calls << :b; nil }, - before_each: ->{ calls << :b; nil } - ) - conditions = new_conditions(pre: ->{ calls << :p; nil }) - run_example(FailingExample, hooks, conditions) - calls.should eq(%i[b b p]) - end - - it "runs around_each hooks prior to pre-conditions" do - calls = [] of Symbol - hooks = new_hooks( - around_each: ->(proc : ->) { calls << :c; proc.call } - ) - conditions = new_conditions(pre: ->{ calls << :p; nil }) - run_example(FailingExample, hooks, conditions) - calls.should eq(%i[c p]) - end - - context "failing pre-condition" do - it "fails the test" do - conditions = new_conditions(pre: ->{ report_expectations(0, 1) }) - result = run_example(FailingExample, conditions: conditions) - result.should be_a(Spectator::FailedResult) - end - - it "prevents additional pre-conditions from running" do - called = false - conditions = new_conditions(pre: [ - ->{ report_expectations(0, 1) }, - ->{ called = true; nil }, - ]) - run_example(FailingExample, conditions: conditions) - called.should be_false - end - - it "prevents additional post-conditions from running" do - called = false - conditions = new_conditions( - pre: ->{ report_expectations(0, 1) }, - post: ->{ called = true; nil } - ) - run_example(FailingExample, conditions: conditions) - called.should be_false - end - - context "in a parent group" do - it "fails the test" do - conditions = new_conditions(pre: ->{ report_expectations(0, 1) }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, conditions) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - result = run_example(FailingExample, group) - result.should be_a(Spectator::FailedResult) - end - - it "doesn't run child pre-conditions" do - called = false - root_conditions = new_conditions(pre: ->{ report_expectations(0, 1) }) - group_conditions = new_conditions(pre: ->{ called = true; nil }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, root_conditions) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, group_conditions) - root.children = [group.as(Spectator::ExampleComponent)] - example = FailingExample.new(group, Spectator::Internals::SampleValues.empty) - group.children = [example.as(Spectator::ExampleComponent)] - Spectator::Internals::Harness.run(example) - called.should be_false - end - - it "doesn't run child post-conditions" do - called = false - root_conditions = new_conditions(pre: ->{ report_expectations(0, 1) }) - group_conditions = new_conditions(post: ->{ called = true; nil }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, root_conditions) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, group_conditions) - root.children = [group.as(Spectator::ExampleComponent)] - example = FailingExample.new(group, Spectator::Internals::SampleValues.empty) - group.children = [example.as(Spectator::ExampleComponent)] - Spectator::Internals::Harness.run(example) - called.should be_false - end - end - end - - it "doesn't run post-conditions" do - called = false - conditions = new_conditions(post: ->{ called = true; nil }) - run_example(FailingExample, conditions: conditions) - called.should be_false - end - - it "doesn't run parent group post-conditions" do - called = false - conditions = new_conditions(post: ->{ called = true; nil }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, conditions) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(FailingExample, group) - called.should be_false - end - end - - context "with an errored test" do - it "returns an errored result" do - run_example(ErroredExample).should be_a(Spectator::ErroredResult) - end - - {% for hook_type in %i[before_all before_each] %} - context "{{hook_type.id}} hooks" do - it "runs a hook" do - called = false - hooks = new_hooks({{hook_type.id}}: ->{ called = true; nil }) - run_example(ErroredExample, hooks) - called.should be_true - end - - it "runs multiple hooks" do - call_count = 0 - hooks = new_hooks({{hook_type.id}}: [ - ->{ call_count += 1; nil }, - ->{ call_count += 2; nil }, - ->{ call_count += 3; nil }, - ]) - run_example(ErroredExample, hooks) - call_count.should eq(6) - end - - it "runs them in the correct order" do - calls = [] of Symbol - hooks = new_hooks({{hook_type.id}}: [ - ->{ calls << :a; nil }, - ->{ calls << :b; nil }, - ->{ calls << :c; nil }, - ]) - run_example(ErroredExample, hooks) - calls.should eq(\%i[a b c]) - end - - it "runs parent group hooks" do - called = false - hooks = new_hooks({{hook_type.id}}: -> { called = true; nil }) - root = Spectator::RootExampleGroup.new(hooks, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(ErroredExample, group) - called.should be_true - end - - it "runs parent group hooks first" do - calls = [] of Symbol - root_hooks = new_hooks({{hook_type.id}}: -> { calls << :a; nil }) - group_hooks = new_hooks({{hook_type.id}}: -> { calls << :b; nil }) - root = Spectator::RootExampleGroup.new(root_hooks, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, group_hooks, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(ErroredExample, group) - calls.should eq(\%i[a b]) - end - end - {% end %} - - it "runs before_all hooks prior to before_each hooks" do - calls = [] of Symbol - hooks = new_hooks( - before_all: ->{ calls << :a; nil }, - before_each: ->{ calls << :b; nil } - ) - run_example(ErroredExample, hooks) - calls.should eq(%i[a b]) - end - - {% for hook_type in %i[after_all after_each] %} - context "{{hook_type.id}} hooks" do - it "runs a hook" do - called = false - hooks = new_hooks({{hook_type.id}}: ->{ called = true; nil }) - run_example(ErroredExample, hooks) - called.should be_true - end - - it "runs multiple hooks" do - call_count = 0 - hooks = new_hooks({{hook_type.id}}: [ - ->{ call_count += 1; nil }, - ->{ call_count += 2; nil }, - ->{ call_count += 3; nil }, - ]) - run_example(ErroredExample, hooks) - call_count.should eq(6) - end - - it "runs them in the correct order" do - calls = [] of Symbol - hooks = new_hooks({{hook_type.id}}: [ - ->{ calls << :a; nil }, - ->{ calls << :b; nil }, - ->{ calls << :c; nil }, - ]) - run_example(ErroredExample, hooks) - calls.should eq(\%i[a b c]) - end - - it "runs parent group hooks" do - called = false - hooks = new_hooks({{hook_type.id}}: -> { called = true; nil }) - root = Spectator::RootExampleGroup.new(hooks, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(ErroredExample, group) - called.should be_true - end - - it "runs parent group hooks last" do - calls = [] of Symbol - root_hooks = new_hooks({{hook_type.id}}: -> { calls << :a; nil }) - group_hooks = new_hooks({{hook_type.id}}: -> { calls << :b; nil }) - root = Spectator::RootExampleGroup.new(root_hooks, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, group_hooks, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(ErroredExample, group) - calls.should eq(\%i[b a]) - end - end - {% end %} - - it "runs after_each hooks prior to after_all hooks" do - calls = [] of Symbol - hooks = new_hooks( - after_each: ->{ calls << :a; nil }, - after_all: ->{ calls << :b; nil } - ) - run_example(ErroredExample, hooks) - calls.should eq(%i[a b]) - end - - context "around_each hooks" do - it "runs a hook" do - called = false - hooks = new_hooks(around_each: ->(proc : ->) { called = true; proc.call }) - run_example(ErroredExample, hooks) - called.should be_true - end - - it "runs multiple hooks" do - call_count = 0 - hooks = new_hooks(around_each: [ - ->(proc : ->) { call_count += 1; proc.call }, - ->(proc : ->) { call_count += 2; proc.call }, - ->(proc : ->) { call_count += 3; proc.call }, - ]) - run_example(ErroredExample, hooks) - call_count.should eq(6) - end - - it "runs them in the correct order" do - calls = [] of Symbol - hooks = new_hooks(around_each: [ - ->(proc : ->) { calls << :a; proc.call }, - ->(proc : ->) { calls << :b; proc.call }, - ->(proc : ->) { calls << :c; proc.call }, - ]) - run_example(ErroredExample, hooks) - calls.should eq(%i[a b c]) - end - - it "runs parent group hooks" do - called = false - hooks = new_hooks(around_each: ->(proc : ->) { called = true; proc.call }) - root = Spectator::RootExampleGroup.new(hooks, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(ErroredExample, group) - called.should be_true - end - - it "runs parent group hooks first" do - calls = [] of Symbol - root_hooks = new_hooks(around_each: ->(proc : ->) { calls << :a; proc.call }) - group_hooks = new_hooks(around_each: ->(proc : ->) { calls << :b; proc.call }) - root = Spectator::RootExampleGroup.new(root_hooks, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("what", root, group_hooks, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(ErroredExample, group) - calls.should eq(%i[a b]) - end - - it "runs around_each hooks after the before hooks" do - calls = [] of Symbol - hooks = new_hooks( - before_all: ->{ calls << :b; nil }, - before_each: ->{ calls << :b; nil }, - around_each: ->(proc : ->) { calls << :c; proc.call } - ) - run_example(ErroredExample, hooks) - calls.should eq(%i[b b c]) - end - - it "runs around_each hooks prior to after hooks" do - calls = [] of Symbol - hooks = new_hooks( - around_each: ->(proc : ->) { calls << :c; proc.call }, - after_each: ->{ calls << :a; nil }, - after_all: ->{ calls << :a; nil }, - ) - run_example(ErroredExample, hooks) - calls.should eq(%i[c a a]) - end - end - - context "pre-conditions" do - it "checks a single condition" do - called = false - conditions = new_conditions(pre: ->{ called = true; nil }) - run_example(ErroredExample, conditions: conditions) - called.should be_true - end - - it "checks multiple conditions" do - call_count = 0 - conditions = new_conditions(pre: [ - ->{ call_count += 1; nil }, - ->{ call_count += 2; nil }, - ->{ call_count += 3; nil }, - ]) - run_example(ErroredExample, conditions: conditions) - call_count.should eq(6) - end - - it "checks them in the correct order" do - calls = [] of Symbol - conditions = new_conditions(pre: [ - ->{ calls << :a; nil }, - ->{ calls << :b; nil }, - ->{ calls << :c; nil }, - ]) - run_example(ErroredExample, conditions: conditions) - calls.should eq(%i[a b c]) - end - - it "checks parent group conditions" do - called = false - conditions = new_conditions(pre: ->{ called = true; nil }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, conditions) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(ErroredExample, group) - called.should be_true - end - - it "checks parent group conditions first" do - calls = [] of Symbol - root_conditions = new_conditions(pre: ->{ calls << :a; nil }) - group_conditions = new_conditions(pre: ->{ calls << :b; nil }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, root_conditions) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, group_conditions) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(ErroredExample, group) - calls.should eq(%i[a b]) - end - end - - it "runs before hooks prior to pre-conditions" do - calls = [] of Symbol - hooks = new_hooks( - before_all: ->{ calls << :b; nil }, - before_each: ->{ calls << :b; nil } - ) - conditions = new_conditions(pre: ->{ calls << :p; nil }) - run_example(ErroredExample, hooks, conditions) - calls.should eq(%i[b b p]) - end - - it "runs around_each hooks prior to pre-conditions" do - calls = [] of Symbol - hooks = new_hooks( - around_each: ->(proc : ->) { calls << :c; proc.call } - ) - conditions = new_conditions(pre: ->{ calls << :p; nil }) - run_example(ErroredExample, hooks, conditions) - calls.should eq(%i[c p]) - end - - context "failing pre-condition" do - it "fails the test" do - conditions = new_conditions(pre: ->{ report_expectations(0, 1) }) - result = run_example(ErroredExample, conditions: conditions) - result.should be_a(Spectator::FailedResult) - end - - it "prevents additional pre-conditions from running" do - called = false - conditions = new_conditions(pre: [ - ->{ report_expectations(0, 1) }, - ->{ called = true; nil }, - ]) - run_example(ErroredExample, conditions: conditions) - called.should be_false - end - - it "prevents additional post-conditions from running" do - called = false - conditions = new_conditions( - pre: ->{ report_expectations(0, 1) }, - post: ->{ called = true; nil } - ) - run_example(ErroredExample, conditions: conditions) - called.should be_false - end - - context "in a parent group" do - it "fails the test" do - conditions = new_conditions(pre: ->{ report_expectations(0, 1) }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, conditions) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - result = run_example(ErroredExample, group) - result.should be_a(Spectator::FailedResult) - end - - it "doesn't run child pre-conditions" do - called = false - root_conditions = new_conditions(pre: ->{ report_expectations(0, 1) }) - group_conditions = new_conditions(pre: ->{ called = true; nil }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, root_conditions) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, group_conditions) - root.children = [group.as(Spectator::ExampleComponent)] - example = ErroredExample.new(group, Spectator::Internals::SampleValues.empty) - group.children = [example.as(Spectator::ExampleComponent)] - Spectator::Internals::Harness.run(example) - called.should be_false - end - - it "doesn't run child post-conditions" do - called = false - root_conditions = new_conditions(pre: ->{ report_expectations(0, 1) }) - group_conditions = new_conditions(post: ->{ called = true; nil }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, root_conditions) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, group_conditions) - root.children = [group.as(Spectator::ExampleComponent)] - example = ErroredExample.new(group, Spectator::Internals::SampleValues.empty) - group.children = [example.as(Spectator::ExampleComponent)] - Spectator::Internals::Harness.run(example) - called.should be_false - end - end - end - - it "doesn't run post-conditions" do - called = false - conditions = new_conditions(post: ->{ called = true; nil }) - run_example(ErroredExample, conditions: conditions) - called.should be_false - end - - it "doesn't run parent group post-conditions" do - called = false - conditions = new_conditions(post: ->{ called = true; nil }) - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, conditions) - group = Spectator::NestedExampleGroup.new("what", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - run_example(ErroredExample, group) - called.should be_false - end - end - - context "when an error is raised in a before_all hook" do - it "raises the exception" do - hooks = new_hooks(before_all: ->{ raise "oops"; nil }) - expect_raises(Exception) do - run_example(PassingExample, hooks) - end - end - - it "passes along the original exception" do - error = Exception.new("oops") - hooks = new_hooks(before_all: ->{ raise error; nil }) - begin - run_example(PassingExample, hooks) - rescue ex - ex.cause.should eq(error) - end - end - - it "doesn't run the test code" do - called = false - hooks = new_hooks(before_all: ->{ raise "oops"; nil }) - expect_raises(Exception) do - run_example(hooks) do - called = true - end - end - called.should be_false - end - - it "doesn't run any additional before_all hooks" do - called = false - hooks = new_hooks(before_all: [ - ->{ raise "oops"; nil }, - ->{ called = true; nil }, - ]) - expect_raises(Exception) do - run_example(PassingExample, hooks) - end - called.should be_false - end - - it "doesn't run any additional hooks" do - called = :none - hooks = new_hooks( - before_all: ->{ raise "oops"; nil }, - before_each: ->{ called = :before_each; nil }, - after_all: ->{ called = :after_all; nil }, - after_each: ->{ called = :after_each; nil }, - around_each: ->(proc : ->) { called = :around_each; proc.call }) - expect_raises(Exception) do - run_example(PassingExample, hooks) - end - called.should eq(:none) - end - end - - context "when an error is raised in a before_each hook" do - it "raises the exception" do - hooks = new_hooks(before_each: ->{ raise "oops"; nil }) - expect_raises(Exception) do - run_example(PassingExample, hooks) - end - end - - it "passes along the original exception" do - error = Exception.new("oops") - hooks = new_hooks(before_each: ->{ raise error; nil }) - begin - run_example(PassingExample, hooks) - rescue ex - ex.cause.should eq(error) - end - end - - it "doesn't run the test code" do - called = false - hooks = new_hooks(before_each: ->{ raise "oops"; nil }) - expect_raises(Exception) do - run_example(hooks) do - called = true - end - end - called.should be_false - end - - it "doesn't run any additional before_each hooks" do - called = false - hooks = new_hooks(before_each: [ - ->{ raise "oops"; nil }, - ->{ called = true; nil }, - ]) - expect_raises(Exception) do - run_example(PassingExample, hooks) - end - called.should be_false - end - - it "doesn't run any additional hooks" do - called = :none - hooks = new_hooks( - before_each: ->{ raise "oops"; nil }, - after_all: ->{ called = :after_all; nil }, - after_each: ->{ called = :after_each; nil }, - around_each: ->(proc : ->) { called = :around_each; proc.call }) - expect_raises(Exception) do - run_example(PassingExample, hooks) - end - called.should eq(:none) - end - end - - context "when an error is raised in an after_all hook" do - it "raises the exception" do - hooks = new_hooks(after_all: ->{ raise "oops"; nil }) - expect_raises(Exception) do - run_example(PassingExample, hooks) - end - end - - it "passes along the original exception" do - error = Exception.new("oops") - hooks = new_hooks(after_all: ->{ raise error; nil }) - begin - run_example(PassingExample, hooks) - rescue ex - ex.cause.should eq(error) - end - end - - it "doesn't run any additional after_all hooks" do - called = false - hooks = new_hooks(after_all: [ - ->{ raise "oops"; nil }, - ->{ called = true; nil }, - ]) - expect_raises(Exception) do - run_example(PassingExample, hooks) - end - called.should be_false - end - end - - context "when an error is raised in an after_each hook" do - it "raises the exception" do - hooks = new_hooks(after_each: ->{ raise "oops"; nil }) - expect_raises(Exception) do - run_example(PassingExample, hooks) - end - end - - it "passes along the original exception" do - error = Exception.new("oops") - hooks = new_hooks(after_each: ->{ raise error; nil }) - begin - run_example(PassingExample, hooks) - rescue ex - ex.cause.should eq(error) - end - end - - it "doesn't run any additional after_each hooks" do - called = false - hooks = new_hooks(after_each: [ - ->{ raise "oops"; nil }, - ->{ called = true; nil }, - ]) - expect_raises(Exception) do - run_example(PassingExample, hooks) - end - called.should be_false - end - - it "doesn't run any additional hooks" do - called = :none - hooks = new_hooks( - before_all: ->{ nil }, - before_each: ->{ nil }, - after_all: ->{ called = :after_all; nil }, - after_each: ->{ raise "oops"; nil }, - around_each: ->(proc : ->) { proc.call }) - expect_raises(Exception) do - run_example(PassingExample, hooks) - end - called.should eq(:none) - end - end - - context "when an error is raised in an around_each hook" do - it "raises the exception" do - hooks = new_hooks(around_each: ->(proc : ->) { raise "oops"; proc.call }) - expect_raises(Exception) do - run_example(PassingExample, hooks) - end - end - - it "passes along the original exception" do - error = Exception.new("oops") - hooks = new_hooks(around_each: ->(proc : ->) { raise error; proc.call }) - begin - run_example(PassingExample, hooks) - rescue ex - ex.cause.should eq(error) - end - end - - it "doesn't run the test code" do - called = false - hooks = new_hooks(around_each: ->(proc : ->) { raise "oops"; proc.call }) - expect_raises(Exception) do - run_example(hooks) do - called = true - end - end - called.should be_false - end - - it "doesn't run any additional around_each hooks" do - called = false - hooks = new_hooks(around_each: [ - ->(proc : ->) { raise "oops"; proc.call }, - ->(proc : ->) { called = true; proc.call }, - ]) - expect_raises(Exception) do - run_example(PassingExample, hooks) - end - called.should be_false - end - - it "doesn't run any additional hooks" do - called = :none - hooks = new_hooks( - after_all: ->{ called = :after_all; nil }, - after_each: ->{ called = :after_each; nil }, - around_each: ->(proc : ->) { raise "oops"; proc.call }) - expect_raises(Exception) do - run_example(PassingExample, hooks) - end - called.should eq(:none) - end - end - end - - describe "#finished?" do - it "is initially false" do - new_runnable_example.finished?.should be_false - end - - it "is true after #run is called" do - example = new_runnable_example - Spectator::Internals::Harness.run(example) - example.finished?.should be_true - end - end - - describe "#group" do - it "is the expected value" do - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - example = new_runnable_example(group) - example.group.should eq(group) - end - end - - describe "#example_count" do - it "is one" do - new_runnable_example.example_count.should eq(1) - end - end - - describe "#[]" do - it "returns self" do - example = new_runnable_example - example[0].should eq(example) - end - end - - describe "#to_s" do - it "contains #what" do - example = new_runnable_example - example.to_s.should contain(example.what) - end - - it "contains the group's #what" do - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("GROUP", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - example = new_runnable_example(group) - example.to_s.should contain(group.what.to_s) - end - - context "when #symbolic? is true" do - context "and the group is symbolic" do - it "omits the space" do - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new(:Group, root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - example = new_runnable_example(group, true) - group.symbolic?.should be_true - example.symbolic?.should be_true - example.to_s.should_not contain(' ') - end - end - - context "and the group isn't symbolic" do - it "inserts a space" do - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("GROUP", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - example = new_runnable_example(group, true) - group.symbolic?.should be_false - example.symbolic?.should be_true - example.to_s.should contain(' ') - end - end - end - - context "when #symbolic? is false" do - context "and the group is symbolic" do - it "inserts a space" do - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new(:Group, root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - example = new_runnable_example(group, false) - group.symbolic?.should be_true - example.symbolic?.should be_false - example.to_s.should contain(' ') - end - end - - context "and the group isn't symbolic" do - it "inserts a space" do - root = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group = Spectator::NestedExampleGroup.new("GROUP", root, Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - root.children = [group.as(Spectator::ExampleComponent)] - example = new_runnable_example(group, false) - group.symbolic?.should be_false - example.symbolic?.should be_false - example.to_s.should contain(' ') - end - end - end - end -end diff --git a/spec/runner_spec.cr b/spec/runner_spec.cr deleted file mode 100644 index c2c4dfb..0000000 --- a/spec/runner_spec.cr +++ /dev/null @@ -1,270 +0,0 @@ -require "./spec_helper" - -# Creates a `Config` for Spectator that is suited for testing it. -def spectator_test_config(formatter : Spectator::Formatting::Formatter? = nil, fail_fast = false) - builder = Spectator::ConfigBuilder.new - builder.formatter = formatter || Spectator::Formatting::SilentFormatter.new - builder.fail_fast = fail_fast - builder.build -end - -def new_test_suite(group : Spectator::ExampleGroup? = nil) - filter = Spectator::NullExampleFilter.new - Spectator::TestSuite.new(group || PassingExample.create.group, filter) -end - -def suite_with_nested_failures(hooks) - conditions = Spectator::ExampleConditions.empty - values = Spectator::Internals::SampleValues.empty - root = Spectator::RootExampleGroup.new(hooks, conditions) - root.children = Array(Spectator::ExampleComponent).new(5) do |index| - Spectator::NestedExampleGroup.new(index.to_s, root, hooks, conditions).tap do |group| - group.children = Array(Spectator::ExampleComponent).new(5) { FailingExample.new(group, values) } - end - end - new_test_suite(root) -end - -describe Spectator::Runner do - describe "#run" do - it "runs all examples in the suite" do - called = [] of Int32 - group = SpyExample.create_group(5) do |index| - called << index - nil - end - suite = new_test_suite(group) - runner = Spectator::Runner.new(suite, spectator_test_config) - runner.run - called.should eq([0, 1, 2, 3, 4]) - end - - context "with fail-fast enabled" do - it "stops on the first failure" do - called = [] of Int32 - group = SpyExample.create_group(10) do |index| - called << index - raise "Failure" if index > 5 - end - suite = new_test_suite(group) - runner = Spectator::Runner.new(suite, spectator_test_config(fail_fast: true)) - runner.run - called.should eq([0, 1, 2, 3, 4, 5, 6]) - end - - it "runs after_each hooks" do - called = false - hooks = new_hooks(after_each: ->{ called = true; nil }) - group = FailingExample.create_group(hooks: hooks) - suite = new_test_suite(group) - runner = Spectator::Runner.new(suite, spectator_test_config(fail_fast: true)) - runner.run - called.should be_true - end - - it "runs after_all hooks" do - called = false - hooks = new_hooks(after_all: ->{ called = true; nil }) - group = FailingExample.create_group(hooks: hooks) - suite = new_test_suite(group) - runner = Spectator::Runner.new(suite, spectator_test_config(fail_fast: true)) - runner.run - called.should be_true - end - - it "runs the remaining around_each hook code" do - called = false - hooks = new_hooks(around_each: ->(proc : ->) { - proc.call - called = true - nil - }) - group = FailingExample.create_group(hooks: hooks) - suite = new_test_suite(group) - runner = Spectator::Runner.new(suite, spectator_test_config(fail_fast: true)) - runner.run - called.should be_true - end - - context "with nested groups" do - it "runs after_each hooks" do - call_count = 0 - hooks = new_hooks(after_each: ->{ call_count += 1; nil }) - suite = suite_with_nested_failures(hooks) - runner = Spectator::Runner.new(suite, spectator_test_config(fail_fast: true)) - runner.run - call_count.should eq(2) - end - - it "runs after_all hooks" do - call_count = 0 - hooks = new_hooks(after_all: ->{ call_count += 1; nil }) - suite = suite_with_nested_failures(hooks) - runner = Spectator::Runner.new(suite, spectator_test_config(fail_fast: true)) - runner.run - call_count.should eq(2) - end - - it "runs the remaining around_each hook code" do - call_count = 0 - hooks = new_hooks(around_each: ->(proc : ->) { - proc.call - call_count += 1 - nil - }) - suite = suite_with_nested_failures(hooks) - runner = Spectator::Runner.new(suite, spectator_test_config(fail_fast: true)) - runner.run - call_count.should eq(2) - end - end - - context "the report" do - it "has the remaining tests" do - spy = SpyFormatter.new - group = SpyExample.create_group(10) do |index| - raise "Failure" if index > 5 - end - suite = new_test_suite(group) - runner = Spectator::Runner.new(suite, spectator_test_config(spy, true)) - runner.run - args = spy.end_suite_calls.first - args[:report].remaining_count.should eq(3) - end - end - end - - context "the formatter" do - it "#start_suite is called once" do - spy = SpyFormatter.new - runner = Spectator::Runner.new(new_test_suite, spectator_test_config(spy)) - runner.run - spy.start_suite_call_count.should eq(1) - end - - it "#start_suite is called at the beginning" do - spy = SpyFormatter.new - runner = Spectator::Runner.new(new_test_suite, spectator_test_config(spy)) - runner.run - spy.all_calls.first.should eq(:start_suite) - end - - it "passes the test suite to #start_suite" do - test_suite = new_test_suite - spy = SpyFormatter.new - runner = Spectator::Runner.new(test_suite, spectator_test_config(spy)) - runner.run - spy.start_suite_calls.first.should eq(test_suite) - end - - it "#end_suite is called once" do - spy = SpyFormatter.new - runner = Spectator::Runner.new(new_test_suite, spectator_test_config(spy)) - runner.run - spy.end_suite_call_count.should eq(1) - end - - it "#end_suite is called at the end" do - spy = SpyFormatter.new - runner = Spectator::Runner.new(new_test_suite, spectator_test_config(spy)) - runner.run - spy.all_calls.last.should eq(:end_suite) - end - - it "#start_example is called" do - spy = SpyFormatter.new - runner = Spectator::Runner.new(new_test_suite, spectator_test_config(spy)) - runner.run - spy.start_example_call_count.should be > 0 - end - - it "#start_example is called for each example" do - group = SpyExample.create_group(5) { nil } - suite = new_test_suite(group) - spy = SpyFormatter.new - runner = Spectator::Runner.new(suite, spectator_test_config(spy)) - runner.run - spy.start_example_call_count.should eq(5) - end - - it "passes the correct example to #start_example" do - group = SpyExample.create_group(5) { nil } - suite = new_test_suite(group) - spy = SpyFormatter.new - runner = Spectator::Runner.new(suite, spectator_test_config(spy)) - runner.run - spy.start_example_calls.should eq(group.children) - end - - it "calls #end_example" do - spy = SpyFormatter.new - runner = Spectator::Runner.new(new_test_suite, spectator_test_config(spy)) - runner.run - spy.end_example_call_count.should be > 0 - end - - it "calls #end_example for each example" do - group = SpyExample.create_group(5) { nil } - suite = new_test_suite(group) - spy = SpyFormatter.new - runner = Spectator::Runner.new(suite, spectator_test_config(spy)) - runner.run - spy.end_example_call_count.should eq(5) - end - - it "passes the correct result to #end_example" do - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group.children = Array.new(5) do |index| - if index.odd? - PassingExample.new(group, Spectator::Internals::SampleValues.empty) - else - FailingExample.new(group, Spectator::Internals::SampleValues.empty) - end.as(Spectator::ExampleComponent) - end - suite = new_test_suite(group) - spy = SpyFormatter.new - runner = Spectator::Runner.new(suite, spectator_test_config(spy)) - runner.run - spy.end_example_calls.map(&.example).should eq(group.children) - end - end - - context "the report" do - it "contains the expected results" do - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group.children = Array.new(5) do |index| - if index.odd? - PassingExample.new(group, Spectator::Internals::SampleValues.empty) - else - FailingExample.new(group, Spectator::Internals::SampleValues.empty) - end.as(Spectator::ExampleComponent) - end - suite = new_test_suite(group) - spy = SpyFormatter.new - runner = Spectator::Runner.new(suite, spectator_test_config(spy)) - runner.run - args = spy.end_suite_calls.first - args[:report].each_with_index do |result, index| - if index.odd? - result.should be_a(Spectator::SuccessfulResult) - else - result.should be_a(Spectator::FailedResult) - end - end - end - - it "contains the expected time span" do - group = SpyExample.create_group(5) { nil } - suite = new_test_suite(group) - spy = SpyFormatter.new - runner = Spectator::Runner.new(suite, spectator_test_config(spy)) - max_time = Time.measure { runner.run } - min_time = spy.end_example_calls.each.map(&.as(Spectator::FinishedResult)).sum(&.elapsed) - args = spy.end_suite_calls.first - report = args[:report] - report.runtime.should be <= max_time - report.runtime.should be >= min_time - end - end - end -end diff --git a/spec/source_example_filter_spec.cr b/spec/source_example_filter_spec.cr deleted file mode 100644 index f8e5e0a..0000000 --- a/spec/source_example_filter_spec.cr +++ /dev/null @@ -1,22 +0,0 @@ -require "./spec_helper" - -describe Spectator::SourceExampleFilter do - describe "#includes?" do - context "with a matching example" do - it "is true" do - example = PassingExample.create - filter = Spectator::SourceExampleFilter.new(example.source) - filter.includes?(example).should be_true - end - end - - context "with a non-matching example" do - it "is false" do - example = PassingExample.create - source = Spectator::Source.new(__FILE__, __LINE__) - filter = Spectator::SourceExampleFilter.new(source) - filter.includes?(example).should be_false - end - end - end -end diff --git a/spec/source_spec.cr b/spec/source_spec.cr deleted file mode 100644 index 1ad6950..0000000 --- a/spec/source_spec.cr +++ /dev/null @@ -1,76 +0,0 @@ -require "./spec_helper" - -describe Spectator::Source do - describe "#file" do - it "is the expected value" do - file = __FILE__ - source = Spectator::Source.new(file, __LINE__) - source.file.should eq(file) - end - end - - describe "#line" do - it "is the expected value" do - line = __LINE__ - source = Spectator::Source.new(__FILE__, line) - source.line.should eq(line) - end - end - - describe "#path" do - context "with a relative file" do - it "is shortened" do - file = "test.cr" - absolute = File.join(Dir.current, file) - source = Spectator::Source.new(absolute, __LINE__) - source.path.should eq(file) - end - end - - context "with a different directory" do - it "is the absolute path" do - file = "/foo/bar/baz.cr" - source = Spectator::Source.new(file, __LINE__) - source.path.should eq(file) - end - end - end - - describe "#to_s" do - it "contains #path" do - file = __FILE__ - source = Spectator::Source.new(file, __LINE__) - source.to_s.should contain(source.path) - end - - it "contains #line" do - line = __LINE__ - source = Spectator::Source.new(__FILE__, line) - source.to_s.should contain(line.to_s) - end - - it "is formatted correctly" do - source = Spectator::Source.new(__FILE__, __LINE__) - source.to_s.should match(/^(.+?)\:(\d+)$/) - end - end - - describe "#parse" do - it "gets the absolute path" do - file = "foo.cr" - path = File.expand_path(file) - source = Spectator::Source.parse("#{file}:42") - source.file.should eq(path) - end - - it "gets the relative path" do - source = Spectator::Source.parse("foo.cr:42") - source.path.should eq("foo.cr") - end - - it "gets the line number" do - source = Spectator::Source.parse("foo.cr:42") - source.line.should eq(42) - end - end -end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 32978f5..30b0ee7 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -1,6 +1,4 @@ -require "spec" require "../src/spectator" -require "./helpers/*" -# Prevent Spectator from trying to run tests. +# Prevent Spectator from trying to run tests on its own. Spectator.autorun = false diff --git a/spec/successful_result_spec.cr b/spec/successful_result_spec.cr deleted file mode 100644 index 2f6be49..0000000 --- a/spec/successful_result_spec.cr +++ /dev/null @@ -1,77 +0,0 @@ -require "./spec_helper" - -def new_successful_result( - example : Spectator::Example? = nil, - elapsed : Time::Span? = nil, - expectations : Spectator::Expectations::ExampleExpectations? = nil -) - Spectator::SuccessfulResult.new( - example || PassingExample.create, - elapsed || Time::Span.zero, - expectations || Spectator::Expectations::ExampleExpectations.new([new_satisfied_expectation]) - ) -end - -describe Spectator::SuccessfulResult do - describe "#call" do - context "without a block" do - it "invokes #success on an instance" do - spy = ResultCallSpy.new - new_successful_result.call(spy) - spy.success?.should be_true - end - - it "returns the value of #success" do - result = new_successful_result - returned = result.call(ResultCallSpy.new) - returned.should eq(:success) - end - end - - context "with a block" do - it "invokes #success on an instance" do - spy = ResultCallSpy.new - new_successful_result.call(spy) { nil } - spy.success?.should be_true - end - - it "yields itself" do - result = new_successful_result - value = nil.as(Spectator::Result?) - result.call(ResultCallSpy.new) { |r| value = r } - value.should eq(result) - end - - it "returns the value of #success" do - result = new_successful_result - value = 42 - returned = result.call(ResultCallSpy.new) { value } - returned.should eq(value) - end - end - end - - describe "#example" do - it "is the expected value" do - example = PassingExample.create - result = new_successful_result(example: example) - result.example.should eq(example) - end - end - - describe "#elapsed" do - it "is the expected value" do - elapsed = Time::Span.new(10, 10, 10) - result = new_successful_result(elapsed: elapsed) - result.elapsed.should eq(elapsed) - end - end - - describe "#expectations" do - it "is the expected value" do - expectations = Spectator::Expectations::ExampleExpectations.new(create_expectations(5, 0)) - result = new_successful_result(expectations: expectations) - result.expectations.should eq(expectations) - end - end -end diff --git a/spec/test_suite_spec.cr b/spec/test_suite_spec.cr deleted file mode 100644 index 5e41278..0000000 --- a/spec/test_suite_spec.cr +++ /dev/null @@ -1,31 +0,0 @@ -require "./spec_helper" - -describe Spectator::TestSuite do - describe "#each" do - it "yields each example" do - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group.children = Array.new(5) do - PassingExample.new(group, Spectator::Internals::SampleValues.empty).as(Spectator::ExampleComponent) - end - test_suite = Spectator::TestSuite.new(group, Spectator::NullExampleFilter.new) - examples = [] of Spectator::Example - test_suite.each do |example| - examples << example - end - examples.should eq(group.children) - end - - it "skips examples not in the filter" do - group = Spectator::RootExampleGroup.new(Spectator::ExampleHooks.empty, Spectator::ExampleConditions.empty) - group.children = Array.new(5) do - PassingExample.new(group, Spectator::Internals::SampleValues.empty).as(Spectator::ExampleComponent) - end - test_suite = Spectator::TestSuite.new(group, Spectator::CompositeExampleFilter.new([] of Spectator::ExampleFilter)) - examples = [] of Spectator::Example - test_suite.each do |example| - examples << example - end - examples.should be_empty - end - end -end From 1eec9190dd3653c43727776b71035bbc76a11433 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 21:24:50 -0600 Subject: [PATCH 060/205] Workaround for subject evaluation --- src/spectator/dsl/values.cr | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/spectator/dsl/values.cr b/src/spectator/dsl/values.cr index 6b8db5c..194e853 100644 --- a/src/spectator/dsl/values.cr +++ b/src/spectator/dsl/values.cr @@ -28,7 +28,11 @@ module Spectator end macro subject(&block) - let(:subject) {{block}} + {% if block.is_a?(Nop) %} + self.subject + {% else %} + let(:subject) {{block}} + {% end %} end macro subject(name, &block) From f47c48b483c8114cb0e50e350fb4434e6b01a601 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 21:37:29 -0600 Subject: [PATCH 061/205] Add pre- and post-conditions --- src/spectator/dsl/hooks.cr | 30 +++++++++++++++++++ src/spectator/example_conditions.cr | 20 ++++++++----- src/spectator/runnable_example.cr | 5 +++- src/spectator/spec_builder.cr | 4 +-- .../spec_builder/example_group_builder.cr | 17 +++++++++++ .../nested_example_group_builder.cr | 2 +- .../root_example_group_builder.cr | 2 +- .../sample_example_group_builder.cr | 4 +-- src/spectator/test_context.cr | 12 +++++++- 9 files changed, 80 insertions(+), 16 deletions(-) diff --git a/src/spectator/dsl/hooks.cr b/src/spectator/dsl/hooks.cr index 00bdd14..5254068 100644 --- a/src/spectator/dsl/hooks.cr +++ b/src/spectator/dsl/hooks.cr @@ -45,5 +45,35 @@ module Spectator ::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/example_conditions.cr b/src/spectator/example_conditions.cr index d1fc95a..0fbd03d 100644 --- a/src/spectator/example_conditions.cr +++ b/src/spectator/example_conditions.cr @@ -10,28 +10,32 @@ module Spectator # This will effectively run nothing extra while running a test. def self.empty new( - [] of ->, - [] of -> + [] of TestMetaMethod, + [] of TestMetaMethod ) end # Creates a new set of conditions. def initialize( - @pre_conditions : Array(->), - @post_conditions : Array(->) + @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 - @pre_conditions.each &.call + 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 - @post_conditions.each &.call + 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/runnable_example.cr b/src/spectator/runnable_example.cr index c608619..d981689 100644 --- a/src/spectator/runnable_example.cr +++ b/src/spectator/runnable_example.cr @@ -25,12 +25,15 @@ module Spectator # Runs the test code and captures the result. private def run_example(result) - wrapper = test_wrapper.around_hook(group.context) + 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 diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index ed897da..e8ec95e 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -87,12 +87,12 @@ module Spectator end # Adds a pre-condition to run at the start of every example in the current group. - def add_pre_condition(&block : ->) : Nil + 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 : ->) : Nil + def add_post_condition(&block : TestMetaMethod) : Nil @@stack.current.add_post_condition(block) end diff --git a/src/spectator/spec_builder/example_group_builder.cr b/src/spectator/spec_builder/example_group_builder.cr index 344ea04..f9ed5bf 100644 --- a/src/spectator/spec_builder/example_group_builder.cr +++ b/src/spectator/spec_builder/example_group_builder.cr @@ -12,6 +12,8 @@ module Spectator::SpecBuilder @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 def add_child(child : Child) @children << child @@ -37,6 +39,14 @@ module Spectator::SpecBuilder @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 + private def build_hooks ExampleHooks.new( @before_all_hooks.to_a, @@ -46,5 +56,12 @@ module Spectator::SpecBuilder @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/nested_example_group_builder.cr b/src/spectator/spec_builder/nested_example_group_builder.cr index efe62e5..dc08cff 100644 --- a/src/spectator/spec_builder/nested_example_group_builder.cr +++ b/src/spectator/spec_builder/nested_example_group_builder.cr @@ -7,7 +7,7 @@ module Spectator::SpecBuilder end def build(parent_group) - context = TestContext.new(parent_group.context, build_hooks, parent_group.context.values) + context = TestContext.new(parent_group.context, build_hooks, build_conditions, parent_group.context.values) NestedExampleGroup.new(@description, @source, parent_group, context).tap do |group| group.children = children.map do |child| child.build(group).as(ExampleComponent) diff --git a/src/spectator/spec_builder/root_example_group_builder.cr b/src/spectator/spec_builder/root_example_group_builder.cr index 09cb104..02f4914 100644 --- a/src/spectator/spec_builder/root_example_group_builder.cr +++ b/src/spectator/spec_builder/root_example_group_builder.cr @@ -4,7 +4,7 @@ require "./example_group_builder" module Spectator::SpecBuilder class RootExampleGroupBuilder < ExampleGroupBuilder def build - context = TestContext.new(nil, build_hooks, TestValues.empty) + context = TestContext.new(nil, build_hooks, build_conditions, TestValues.empty) RootExampleGroup.new(context).tap do |group| group.children = children.map do |child| child.build(group).as(ExampleComponent) diff --git a/src/spectator/spec_builder/sample_example_group_builder.cr b/src/spectator/spec_builder/sample_example_group_builder.cr index c8013e3..eacd596 100644 --- a/src/spectator/spec_builder/sample_example_group_builder.cr +++ b/src/spectator/spec_builder/sample_example_group_builder.cr @@ -9,7 +9,7 @@ module Spectator::SpecBuilder def build(parent_group) values = parent_group.context.values collection = @collection_builder.call(values) - context = TestContext.new(parent_group.context, build_hooks, values) + context = TestContext.new(parent_group.context, build_hooks, build_conditions, values) NestedExampleGroup.new(@description, @source, parent_group, context).tap do |group| group.children = collection.map do |element| build_sub_group(group, element).as(ExampleComponent) @@ -19,7 +19,7 @@ module Spectator::SpecBuilder 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, values) + context = TestContext.new(parent_group.context, ExampleHooks.empty, ExampleConditions.empty, values) NestedExampleGroup.new("#{@label} = #{element.inspect}", @source, parent_group, context).tap do |group| group.children = children.map do |child| child.build(group).as(ExampleComponent) diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr index 0bc421d..fa7a720 100644 --- a/src/spectator/test_context.cr +++ b/src/spectator/test_context.cr @@ -5,7 +5,7 @@ module Spectator class TestContext getter values - def initialize(@parent : TestContext?, @hooks : ExampleHooks, @values : TestValues) + def initialize(@parent : TestContext?, @hooks : ExampleHooks, @conditions : ExampleConditions, @values : TestValues) @before_all_hooks_run = false @after_all_hooks_run = false end @@ -56,5 +56,15 @@ module Spectator 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 From 60ca4361de022972330c14fd1c6d7c6ad99d4cf0 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 21:44:25 -0600 Subject: [PATCH 062/205] Fix implicit subject --- src/spectator/dsl/groups.cr | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 1e1386c..8153b0e 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -2,26 +2,26 @@ require "../spec_builder" module Spectator module DSL - macro context(description, _source_file = __FILE__, _source_line = __LINE__, &block) + macro context(what, _source_file = __FILE__, _source_line = __LINE__, &block) class Context%context < {{@type.id}} {% - description = if description.is_a?(StringLiteral) - if description.starts_with?("#") || description.starts_with?(".") - description.id.symbolize + description = if what.is_a?(StringLiteral) + if what.starts_with?("#") || what.starts_with?(".") + what.id.symbolize else - description + what end else - description.symbolize + what.symbolize end %} %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) ::Spectator::SpecBuilder.start_group({{description}}, %source) - {% if description.is_a?(Path) || description.is_a?(Generic) %} + {% if what.is_a?(Path) || what.is_a?(Generic) %} macro described_class - {{description}} + {{what}} end def subject(*args) @@ -35,8 +35,8 @@ module Spectator end end - macro describe(description, &block) - context({{description}}) {{block}} + macro describe(what, &block) + context({{what}}) {{block}} end macro sample(collection, count = nil, _source_file = __FILE__, _source_line = __LINE__, &block) From 0e3727b504598490da198b4d7426670f10629fa7 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 26 Sep 2019 22:53:16 -0600 Subject: [PATCH 063/205] Run after-all hooks correctly --- src/spectator/runner.cr | 2 +- src/spectator/test_context.cr | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/spectator/runner.cr b/src/spectator/runner.cr index bf2e53c..8bde725 100644 --- a/src/spectator/runner.cr +++ b/src/spectator/runner.cr @@ -37,7 +37,7 @@ module Spectator result = run_example(example).as(Result) results << result if @config.fail_fast? && result.is_a?(FailedResult) - # TODO: example.group.run_after_all_hooks(ignore_unfinished: true) + example.group.context.run_after_all_hooks(example.group, ignore_unfinished: true) break end end diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr index fa7a720..8d16fee 100644 --- a/src/spectator/test_context.cr +++ b/src/spectator/test_context.cr @@ -31,14 +31,18 @@ module Spectator def run_after_hooks(example : Example) run_after_each_hooks(example) - run_after_all_hooks + run_after_all_hooks(example.group) end - protected def run_after_all_hooks + 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 &.run_after_all_hooks + @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 From c458a490bf5db68537041258c056e22176a7f5dc Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 4 Oct 2019 23:01:46 -0600 Subject: [PATCH 064/205] Remove usage of sample values --- src/spectator/example.cr | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 8399a3b..e437bc4 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -34,8 +34,6 @@ module Spectator abstract def run_impl - protected getter sample_values : Internals::SampleValues - # Runs the example code. # A result is returned, which represents the outcome of the test. # An example can be run only once. From bff2668ad4040df7991ebaa3993a35c5f1768f8b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 4 Oct 2019 23:16:22 -0600 Subject: [PATCH 065/205] Remove old files --- src/spectator/dsl/double_factory.cr | 17 ----------------- src/spectator/dsl/mock_dsl.cr | 4 ---- 2 files changed, 21 deletions(-) delete mode 100644 src/spectator/dsl/double_factory.cr delete mode 100644 src/spectator/dsl/mock_dsl.cr diff --git a/src/spectator/dsl/double_factory.cr b/src/spectator/dsl/double_factory.cr deleted file mode 100644 index e55c2f3..0000000 --- a/src/spectator/dsl/double_factory.cr +++ /dev/null @@ -1,17 +0,0 @@ -require "../double" - -module Spectator::DSL - # Creates instances of doubles from a specified type. - class DoubleFactory - # Creates the factory. - # The type passed to this constructor must be a double. - def initialize(@double_type : Double.class) - end - - # Constructs a new double instance and returns it. - # The *sample_values* are passed to `Double#initialize`. - def build(sample_values : Internals::SampleValues) : Double - @double_type.new - end - end -end diff --git a/src/spectator/dsl/mock_dsl.cr b/src/spectator/dsl/mock_dsl.cr deleted file mode 100644 index 8b165ba..0000000 --- a/src/spectator/dsl/mock_dsl.cr +++ /dev/null @@ -1,4 +0,0 @@ -module Spectator::DSL - module MockDSL - end -end From d08ec75db7672cf89a0953fc93018878a2cb471b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 4 Oct 2019 23:16:40 -0600 Subject: [PATCH 066/205] Minimal working double --- src/spectator/double.cr | 2 +- src/spectator/dsl/mocks.cr | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 src/spectator/dsl/mocks.cr diff --git a/src/spectator/double.cr b/src/spectator/double.cr index a50be76..081effb 100644 --- a/src/spectator/double.cr +++ b/src/spectator/double.cr @@ -1,5 +1,5 @@ module Spectator - abstract struct Double + abstract class Double macro stub(definition) def {{definition.name.id}} {{definition.block.body}} diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr new file mode 100644 index 0000000..6144e13 --- /dev/null +++ b/src/spectator/dsl/mocks.cr @@ -0,0 +1,13 @@ +require "../double" + +module Spectator::DSL + macro double(name, &block) + {% if block.is_a?(Nop) %} + Double{{name.id}}.new + {% else %} + class Double{{name.id}} < ::Spectator::Double + {{block.body}} + end + {% end %} + end +end From 00c055342695f9c3ac598e6375150d27071f59f5 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 5 Oct 2019 07:38:13 -0600 Subject: [PATCH 067/205] Get contextual values working with doubles --- src/spectator/double.cr | 8 +++++++- src/spectator/dsl/mocks.cr | 9 ++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/spectator/double.cr b/src/spectator/double.cr index 081effb..d8aa3c9 100644 --- a/src/spectator/double.cr +++ b/src/spectator/double.cr @@ -2,7 +2,13 @@ module Spectator abstract class Double macro stub(definition) def {{definition.name.id}} - {{definition.block.body}} + @internal.{{definition.name.id}} + end + + class Internal + def {{definition.name.id}} + {{definition.block.body}} + end end end end diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 6144e13..7f4519f 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -3,9 +3,16 @@ require "../double" module Spectator::DSL macro double(name, &block) {% if block.is_a?(Nop) %} - Double{{name.id}}.new + Double{{name.id}}.new(@spectator_test_values) {% else %} class Double{{name.id}} < ::Spectator::Double + class Internal < {{@type.id}} + end + + def initialize(test_values : ::Spectator::TestValues) + @internal = Internal.new(test_values) + end + {{block.body}} end {% end %} From 47ca96801b29044bf77992d07167ade650543598 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 5 Oct 2019 22:06:03 -0600 Subject: [PATCH 068/205] Mark internal class as private Minimal doubles should be done. --- README.md | 2 +- src/spectator/double.cr | 2 +- src/spectator/dsl/mocks.cr | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7617a01..7218535 100644 --- a/README.md +++ b/README.md @@ -311,7 +311,7 @@ Items not marked as completed may have partial implementations. - [ ] Compound - `and`, `or` - [ ] Mocks and Doubles - [ ] Mocks (Stub real types) - `mock TYPE { }` - - [ ] Doubles (Stand-ins for real types) - `double NAME { }` + - [X] Doubles (Stand-ins for real types) - `double NAME { }` - [ ] Method stubs - `allow().to receive()`, `allow().to receive().and_return()` - [ ] Spies - `expect().to receive()` - [ ] Message expectations - `expect().to receive().at_least()` diff --git a/src/spectator/double.cr b/src/spectator/double.cr index d8aa3c9..2991639 100644 --- a/src/spectator/double.cr +++ b/src/spectator/double.cr @@ -5,7 +5,7 @@ module Spectator @internal.{{definition.name.id}} end - class Internal + private class Internal def {{definition.name.id}} {{definition.block.body}} end diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 7f4519f..18ff002 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -6,7 +6,7 @@ module Spectator::DSL Double{{name.id}}.new(@spectator_test_values) {% else %} class Double{{name.id}} < ::Spectator::Double - class Internal < {{@type.id}} + private class Internal < {{@type.id}} end def initialize(test_values : ::Spectator::TestValues) From 0528ddc2abdc27ab153e7e731da64da252a3488b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 5 Oct 2019 22:22:31 -0600 Subject: [PATCH 069/205] Handle stubbed methods with arguments --- src/spectator/double.cr | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/spectator/double.cr b/src/spectator/double.cr index 2991639..8e4237d 100644 --- a/src/spectator/double.cr +++ b/src/spectator/double.cr @@ -1,12 +1,10 @@ module Spectator abstract class Double macro stub(definition) - def {{definition.name.id}} - @internal.{{definition.name.id}} - end + delegate {{definition.name.id}}, to: @internal private class Internal - def {{definition.name.id}} + def {{definition.name.id}}({{definition.args.splat}}) {{definition.block.body}} end end From 6b85bb7ed75160da35472b65710be98e13651ffa Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 5 Oct 2019 22:42:33 -0600 Subject: [PATCH 070/205] Default raise if stub is called without defintion --- src/spectator/double.cr | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/spectator/double.cr b/src/spectator/double.cr index 8e4237d..aeed0c1 100644 --- a/src/spectator/double.cr +++ b/src/spectator/double.cr @@ -5,7 +5,11 @@ module Spectator private class Internal def {{definition.name.id}}({{definition.args.splat}}) - {{definition.block.body}} + {% if definition.block.is_a?(Nop) %} + raise "Stubbed method called without being allowed" + {% else %} + {{definition.block.body}} + {% end %} end end end From 49764e5873772b5effa6e4d6acaa16b8a4a63361 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 5 Oct 2019 22:43:40 -0600 Subject: [PATCH 071/205] Use do...end for multi-line stub def --- src/spectator/double.cr | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/spectator/double.cr b/src/spectator/double.cr index aeed0c1..c97557c 100644 --- a/src/spectator/double.cr +++ b/src/spectator/double.cr @@ -1,12 +1,16 @@ module Spectator abstract class Double - macro stub(definition) + macro stub(definition, &block) delegate {{definition.name.id}}, to: @internal private class Internal def {{definition.name.id}}({{definition.args.splat}}) {% if definition.block.is_a?(Nop) %} - raise "Stubbed method called without being allowed" + {% if block.is_a?(Nop) %} + raise "Stubbed method called without being allowed" + {% else %} + {{block.body}} + {% end %} {% else %} {{definition.block.body}} {% end %} From 1bbe5067b6a50a7265f4aada75b013187d1c16fa Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 6 Oct 2019 10:24:51 -0600 Subject: [PATCH 072/205] Handle deferred stub with type --- src/spectator/double.cr | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/spectator/double.cr b/src/spectator/double.cr index c97557c..901683b 100644 --- a/src/spectator/double.cr +++ b/src/spectator/double.cr @@ -1,18 +1,30 @@ module Spectator abstract class Double macro stub(definition, &block) - delegate {{definition.name.id}}, to: @internal + {% + name = nil + args = nil + body = nil + if definition.is_a?(Call) # stub foo { :bar } + name = definition.name.id + args = definition.args + body = definition.block.is_a?(Nop) ? block : definition.block + elsif definition.is_a?(TypeDeclaration) # stub foo : Symbol + name = definition.var + args = [] of MacroId + body = block + else + raise "Unrecognized stub format" + end + %} + delegate {{name}}, to: @internal private class Internal - def {{definition.name.id}}({{definition.args.splat}}) - {% if definition.block.is_a?(Nop) %} - {% if block.is_a?(Nop) %} - raise "Stubbed method called without being allowed" - {% else %} - {{block.body}} - {% end %} + def {{name}}({{args.splat}}) + {% if body && !body.is_a?(Nop) %} + {{body.body}} {% else %} - {{definition.block.body}} + raise "Stubbed method called without being allowed" {% end %} end end From 6e50e54e05c20975a5989d3a5309dac67db429de Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 6 Oct 2019 11:02:52 -0600 Subject: [PATCH 073/205] Add HTML as planned output type --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7218535..3c66f40 100644 --- a/README.md +++ b/README.md @@ -326,11 +326,12 @@ Items not marked as completed may have partial implementations. - [X] Dry run - for validation and checking formatted output - [X] Config block in `spec_helper.cr` - [X] Config file - `.spectator` -- [X] Reporter and formatting +- [ ] Reporter and formatting - [X] RSpec/Crystal Spec default - [X] JSON - [X] JUnit - [X] TAP + - [ ] HTML ### How it Works (in a nutshell) From 4b57ddab806cfded9cfd778099c810a3d8f54e57 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 6 Oct 2019 11:12:06 -0600 Subject: [PATCH 074/205] Handle changing test scope This matches the current pattern, where a nested scope can override values from a parent scope. I think this solution is quite clever :) --- src/spectator/dsl/mocks.cr | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 18ff002..0e9ea36 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -3,14 +3,18 @@ require "../double" module Spectator::DSL macro double(name, &block) {% if block.is_a?(Nop) %} - Double{{name.id}}.new(@spectator_test_values) + Double{{name.id}}.new(self) {% else %} class Double{{name.id}} < ::Spectator::Double - private class Internal < {{@type.id}} + private class Internal + def initialize(@test : {{@type.id}}) + end + + forward_missing_to @test end - def initialize(test_values : ::Spectator::TestValues) - @internal = Internal.new(test_values) + def initialize(test : {{@type.id}}) + @internal = Internal.new(test) end {{block.body}} From 0b6465e6bc12098745381984cf1748280a9d3c0b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Oct 2019 16:30:46 -0600 Subject: [PATCH 075/205] Started playing around with method stubs --- src/spectator/double.cr | 34 +++++++++++++++++++++++++--- src/spectator/dsl/mocks.cr | 11 +++++++++ src/spectator/generic_method_stub.cr | 13 +++++++++++ src/spectator/method_stub.cr | 15 ++++++++++++ src/spectator/open_mock.cr | 10 ++++++++ 5 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 src/spectator/generic_method_stub.cr create mode 100644 src/spectator/method_stub.cr create mode 100644 src/spectator/open_mock.cr diff --git a/src/spectator/double.cr b/src/spectator/double.cr index 901683b..7423d0a 100644 --- a/src/spectator/double.cr +++ b/src/spectator/double.cr @@ -1,26 +1,50 @@ +require "./method_stub" + module Spectator abstract class Double + @stubs = Deque(MethodStub).new + + private macro delegate_internal(method, *args) + # Modified version of Object#delegate + {% if method.id.ends_with?('=') && method.id != "[]=" %} + @internal.{{method.id}} {{args.splat}} + {% else %} + @internal.{{method.id}}({{args.splat}}) + {% end %} + end + macro stub(definition, &block) {% name = nil + params = nil args = nil body = nil if definition.is_a?(Call) # stub foo { :bar } name = definition.name.id - args = definition.args + params = definition.args + args = params.map { |p| p.is_a?(TypeDeclaration) ? p.var : p.id } body = definition.block.is_a?(Nop) ? block : definition.block elsif definition.is_a?(TypeDeclaration) # stub foo : Symbol name = definition.var + params = [] of MacroId args = [] of MacroId body = block else raise "Unrecognized stub format" end %} - delegate {{name}}, to: @internal + + def {{name}}({{params.splat}}) + %stub = @stubs.find(&.callable?({{name.symbolize}}{% unless args.empty? %}, {{args.splat}}{% end %})) + if %stub + %stub.call({{args.splat}}) + else + delegate_internal({{name}}{% unless args.empty? %}, {{args.splat}}{% end %}) + end + end private class Internal - def {{name}}({{args.splat}}) + def {{name}}({{params.splat}}) {% if body && !body.is_a?(Nop) %} {{body.body}} {% else %} @@ -29,5 +53,9 @@ module Spectator end end end + + protected def spectator_define_stub(stub : MethodStub) : Nil + @stubs << stub + end end end diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 0e9ea36..f9088e6 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -1,4 +1,6 @@ require "../double" +require "../generic_method_stub" +require "../open_mock" module Spectator::DSL macro double(name, &block) @@ -21,4 +23,13 @@ module Spectator::DSL end {% end %} end + + def allow(double : ::Spectator::Double) + OpenMock.new(double) + end + + macro receive(method_name, _source_file = __FILE__, _source_line = __LINE__) + %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) + ::Spectator::GenericMethodStub(Nil).new({{method_name.symbolize}}, %source) + end end diff --git a/src/spectator/generic_method_stub.cr b/src/spectator/generic_method_stub.cr new file mode 100644 index 0000000..e9682fd --- /dev/null +++ b/src/spectator/generic_method_stub.cr @@ -0,0 +1,13 @@ +require "./method_stub" +require "./source" + +module Spectator + class GenericMethodStub(ReturnType, *ArgumentTypes) < MethodStub + def callable?(name : Symbol, *args) : Bool + super + end + + def call(*args) + end + end +end diff --git a/src/spectator/method_stub.cr b/src/spectator/method_stub.cr new file mode 100644 index 0000000..7a710a0 --- /dev/null +++ b/src/spectator/method_stub.cr @@ -0,0 +1,15 @@ +require "./source" + +module Spectator + abstract class MethodStub + def initialize(@name : Symbol, @source : Source) + end + + def callable?(name : Symbol, *args) : Bool + name == @name + end + + def call(*args) + end + end +end diff --git a/src/spectator/open_mock.cr b/src/spectator/open_mock.cr new file mode 100644 index 0000000..64068ce --- /dev/null +++ b/src/spectator/open_mock.cr @@ -0,0 +1,10 @@ +module Spectator + struct OpenMock + def initialize(@mock : Double) + end + + def to(stub : MethodStub) : Nil + @mock.spectator_define_stub(stub) + end + end +end From c80a28d616758d20987e73a8fe37795ba2caaf5d Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Oct 2019 20:53:17 -0600 Subject: [PATCH 076/205] More work on method stubs --- src/spectator/double.cr | 28 ++++++++++++---------------- src/spectator/dsl/mocks.cr | 2 +- src/spectator/generic_method_stub.cr | 12 +++++++++--- src/spectator/method_call.cr | 12 ++++++++++++ src/spectator/method_stub.cr | 7 ++----- 5 files changed, 36 insertions(+), 25 deletions(-) create mode 100644 src/spectator/method_call.cr diff --git a/src/spectator/double.cr b/src/spectator/double.cr index 7423d0a..4301370 100644 --- a/src/spectator/double.cr +++ b/src/spectator/double.cr @@ -4,16 +4,7 @@ module Spectator abstract class Double @stubs = Deque(MethodStub).new - private macro delegate_internal(method, *args) - # Modified version of Object#delegate - {% if method.id.ends_with?('=') && method.id != "[]=" %} - @internal.{{method.id}} {{args.splat}} - {% else %} - @internal.{{method.id}}({{args.splat}}) - {% end %} - end - - macro stub(definition, &block) + private macro stub(definition, &block) {% name = nil params = nil @@ -34,17 +25,22 @@ module Spectator end %} - def {{name}}({{params.splat}}) - %stub = @stubs.find(&.callable?({{name.symbolize}}{% unless args.empty? %}, {{args.splat}}{% end %})) - if %stub - %stub.call({{args.splat}}) + def {{name}}(*args, **options){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} + call = ::Spectator::MethodCall.new({{name.symbolize}}, args, options) + stub = @stubs.find(&.callable?(call)) + if stub + stub.as(::Spectator::GenericMethodStub(typeof(@internal.{{name}}(*args, **options)))).call(call) else - delegate_internal({{name}}{% unless args.empty? %}, {{args.splat}}{% end %}) + @internal.{{name}}(*args, **options) end end + def {{name}}(*args, **options, &block){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} + @internal.{{name}}(*args, **options, &block) + end + private class Internal - def {{name}}({{params.splat}}) + def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} {% if body && !body.is_a?(Nop) %} {{body.body}} {% else %} diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index f9088e6..532b058 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -30,6 +30,6 @@ module Spectator::DSL macro receive(method_name, _source_file = __FILE__, _source_line = __LINE__) %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - ::Spectator::GenericMethodStub(Nil).new({{method_name.symbolize}}, %source) + ::Spectator::GenericMethodStub(Nil).new({{method_name.symbolize}}, %source, ->{ nil }) end end diff --git a/src/spectator/generic_method_stub.cr b/src/spectator/generic_method_stub.cr index e9682fd..78042b1 100644 --- a/src/spectator/generic_method_stub.cr +++ b/src/spectator/generic_method_stub.cr @@ -1,13 +1,19 @@ +require "./method_call" require "./method_stub" require "./source" module Spectator - class GenericMethodStub(ReturnType, *ArgumentTypes) < MethodStub - def callable?(name : Symbol, *args) : Bool + class GenericMethodStub(ReturnType) < MethodStub + def initialize(name : Symbol, source : Source, @proc : -> ReturnType) + super(name, source) + end + + def callable?(call : MethodCall) : Bool super end - def call(*args) + def call(call : MethodCall) : ReturnType + @proc.call end end end diff --git a/src/spectator/method_call.cr b/src/spectator/method_call.cr new file mode 100644 index 0000000..da94054 --- /dev/null +++ b/src/spectator/method_call.cr @@ -0,0 +1,12 @@ +module Spectator + class MethodCall(T, NT) + getter name : Symbol + + getter args : T + + getter options : NT + + def initialize(@name : Symbol, @args : T, @options : NT) + end + end +end diff --git a/src/spectator/method_stub.cr b/src/spectator/method_stub.cr index 7a710a0..856ed1e 100644 --- a/src/spectator/method_stub.cr +++ b/src/spectator/method_stub.cr @@ -5,11 +5,8 @@ module Spectator def initialize(@name : Symbol, @source : Source) end - def callable?(name : Symbol, *args) : Bool - name == @name - end - - def call(*args) + def callable?(call : MethodCall) : Bool + @name == call.name end end end From 211050650ef02a56bebe324e7d9a396781dd6915 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Oct 2019 21:08:49 -0600 Subject: [PATCH 077/205] Remove internal double class Stub handling method should be called from nested methods. --- src/spectator/double.cr | 20 +++++++++----------- src/spectator/dsl/mocks.cr | 10 ++-------- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/src/spectator/double.cr b/src/spectator/double.cr index 4301370..210ea98 100644 --- a/src/spectator/double.cr +++ b/src/spectator/double.cr @@ -29,24 +29,22 @@ module Spectator call = ::Spectator::MethodCall.new({{name.symbolize}}, args, options) stub = @stubs.find(&.callable?(call)) if stub - stub.as(::Spectator::GenericMethodStub(typeof(@internal.{{name}}(*args, **options)))).call(call) + stub.as(::Spectator::GenericMethodStub(typeof(%method(*args, **options)))).call(call) else - @internal.{{name}}(*args, **options) + %method(*args, **options) end end def {{name}}(*args, **options, &block){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} - @internal.{{name}}(*args, **options, &block) + %method(*args, **options, &block) end - private class Internal - def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} - {% if body && !body.is_a?(Nop) %} - {{body.body}} - {% else %} - raise "Stubbed method called without being allowed" - {% end %} - end + def %method({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} + {% if body && !body.is_a?(Nop) %} + {{body.body}} + {% else %} + raise "Stubbed method called without being allowed" + {% end %} end end diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 532b058..fdd4ed2 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -8,16 +8,10 @@ module Spectator::DSL Double{{name.id}}.new(self) {% else %} class Double{{name.id}} < ::Spectator::Double - private class Internal - def initialize(@test : {{@type.id}}) - end - - forward_missing_to @test + def initialize(@spectator_test : {{@type.id}}) end - def initialize(test : {{@type.id}}) - @internal = Internal.new(test) - end + forward_missing_to @spectator_test {{block.body}} end From 27bc976ae3017cf276a2712d6fa31657d46960db Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Oct 2019 21:30:20 -0600 Subject: [PATCH 078/205] Try handling different method signatures and blocks --- src/spectator/double.cr | 44 ++++++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/src/spectator/double.cr b/src/spectator/double.cr index 210ea98..27de724 100644 --- a/src/spectator/double.cr +++ b/src/spectator/double.cr @@ -25,19 +25,41 @@ module Spectator end %} - def {{name}}(*args, **options){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} - call = ::Spectator::MethodCall.new({{name.symbolize}}, args, options) - stub = @stubs.find(&.callable?(call)) - if stub - stub.as(::Spectator::GenericMethodStub(typeof(%method(*args, **options)))).call(call) - else - %method(*args, **options) + {% if name.ends_with?('=') && name.id != "[]=" %} + def {{name}}(arg) + call = ::Spectator::MethodCall.new({{name.symbolize}}, {arg}, NamedTuple.new) + stub = @stubs.find(&.callable?(call)) + if stub + stub.as(::Spectator::GenericMethodStub(typeof(%method(arg)))).call(call) + else + %method(arg) + end + end + {% else %} + def {{name}}(*args, **options){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} + call = ::Spectator::MethodCall.new({{name.symbolize}}, args, options) + stub = @stubs.find(&.callable?(call)) + if stub + stub.as(::Spectator::GenericMethodStub(typeof(%method(*args, **options)))).call(call) + else + %method(*args, **options) + end end - end - def {{name}}(*args, **options, &block){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} - %method(*args, **options, &block) - end + {% if name != "[]=" %} + def {{name}}(*args, **options){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} + call = ::Spectator::MethodCall.new({{name.symbolize}}, args, options) + stub = @stubs.find(&.callable?(call)) + if stub + stub.as(::Spectator::GenericMethodStub(typeof(%method(*args, **options)))).call(call) + else + %method(*args, **options) do |*yield_args| + yield *yield_args + end + end + end + {% end %} + {% end %} def %method({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} {% if body && !body.is_a?(Nop) %} From aecdb514b31777078d71578fcdc784ceb9fb4997 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 12 Oct 2019 22:35:07 -0600 Subject: [PATCH 079/205] Implement basic and_return --- src/spectator/double.cr | 8 +++++++- src/spectator/generic_method_stub.cr | 4 ++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/spectator/double.cr b/src/spectator/double.cr index 27de724..9507b1d 100644 --- a/src/spectator/double.cr +++ b/src/spectator/double.cr @@ -51,7 +51,7 @@ module Spectator call = ::Spectator::MethodCall.new({{name.symbolize}}, args, options) stub = @stubs.find(&.callable?(call)) if stub - stub.as(::Spectator::GenericMethodStub(typeof(%method(*args, **options)))).call(call) + stub.as(::Spectator::GenericMethodStub(typeof(%method(*args, **options) { |*yield_args| yield *yield_args }))).call(call) else %method(*args, **options) do |*yield_args| yield *yield_args @@ -66,6 +66,12 @@ module Spectator {{body.body}} {% else %} raise "Stubbed method called without being allowed" + # This code shouldn't be reached, but makes the compiler happy to have a matching type. + {% if definition.is_a?(TypeDeclaration) %} + %x = uninitialized {{definition.type}} + {% else %} + nil + {% end %} {% end %} end end diff --git a/src/spectator/generic_method_stub.cr b/src/spectator/generic_method_stub.cr index 78042b1..263d843 100644 --- a/src/spectator/generic_method_stub.cr +++ b/src/spectator/generic_method_stub.cr @@ -15,5 +15,9 @@ module Spectator def call(call : MethodCall) : ReturnType @proc.call end + + def and_return(value : T) forall T + GenericMethodStub(T).new(@name, @source, -> { value }) + end end end From 3befc8001b93af517b17f013cc24ae4df827123a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 13 Oct 2019 11:21:23 -0600 Subject: [PATCH 080/205] Basics of stubbing is done Still need to handle some features of yielding. Also needs error handling. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c66f40..9c57285 100644 --- a/README.md +++ b/README.md @@ -312,7 +312,7 @@ Items not marked as completed may have partial implementations. - [ ] Mocks and Doubles - [ ] Mocks (Stub real types) - `mock TYPE { }` - [X] Doubles (Stand-ins for real types) - `double NAME { }` - - [ ] Method stubs - `allow().to receive()`, `allow().to receive().and_return()` + - [X] Method stubs - `allow().to receive()`, `allow().to receive().and_return()` - [ ] Spies - `expect().to receive()` - [ ] Message expectations - `expect().to receive().at_least()` - [ ] Argument expectations - `expect().to receive().with()` From b09898d131bc73ab9f97760e53ae66645f2fae97 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 13 Oct 2019 11:36:31 -0600 Subject: [PATCH 081/205] Rename @stubs to @spectator_stubs --- src/spectator/double.cr | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/spectator/double.cr b/src/spectator/double.cr index 9507b1d..5ac71cb 100644 --- a/src/spectator/double.cr +++ b/src/spectator/double.cr @@ -2,7 +2,7 @@ require "./method_stub" module Spectator abstract class Double - @stubs = Deque(MethodStub).new + @spectator_stubs = Deque(MethodStub).new private macro stub(definition, &block) {% @@ -28,7 +28,7 @@ module Spectator {% if name.ends_with?('=') && name.id != "[]=" %} def {{name}}(arg) call = ::Spectator::MethodCall.new({{name.symbolize}}, {arg}, NamedTuple.new) - stub = @stubs.find(&.callable?(call)) + stub = @spectator_stubs.find(&.callable?(call)) if stub stub.as(::Spectator::GenericMethodStub(typeof(%method(arg)))).call(call) else @@ -38,7 +38,7 @@ module Spectator {% else %} def {{name}}(*args, **options){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} call = ::Spectator::MethodCall.new({{name.symbolize}}, args, options) - stub = @stubs.find(&.callable?(call)) + stub = @spectator_stubs.find(&.callable?(call)) if stub stub.as(::Spectator::GenericMethodStub(typeof(%method(*args, **options)))).call(call) else @@ -49,7 +49,7 @@ module Spectator {% if name != "[]=" %} def {{name}}(*args, **options){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} call = ::Spectator::MethodCall.new({{name.symbolize}}, args, options) - stub = @stubs.find(&.callable?(call)) + stub = @spectator_stubs.find(&.callable?(call)) if stub stub.as(::Spectator::GenericMethodStub(typeof(%method(*args, **options) { |*yield_args| yield *yield_args }))).call(call) else @@ -77,7 +77,7 @@ module Spectator end protected def spectator_define_stub(stub : MethodStub) : Nil - @stubs << stub + @spectator_stubs << stub end end end From 8cbe2edf300f6676e665fe1d74c9110ed242eabe Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 13 Oct 2019 17:36:55 -0600 Subject: [PATCH 082/205] Formatting --- src/spectator/generic_method_stub.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/generic_method_stub.cr b/src/spectator/generic_method_stub.cr index 263d843..0c752ab 100644 --- a/src/spectator/generic_method_stub.cr +++ b/src/spectator/generic_method_stub.cr @@ -17,7 +17,7 @@ module Spectator end def and_return(value : T) forall T - GenericMethodStub(T).new(@name, @source, -> { value }) + GenericMethodStub(T).new(@name, @source, ->{ value }) end end end From d422376aaf8c4ac438f3063945e9778e58b56836 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 13 Oct 2019 18:41:10 -0600 Subject: [PATCH 083/205] Initial work on have_received matcher --- src/spectator/double.cr | 14 ++++++++--- src/spectator/dsl/matchers.cr | 5 ++++ src/spectator/generic_method_call.cr | 13 ++++++++++ src/spectator/matchers/receive_matcher.cr | 30 +++++++++++++++++++++++ src/spectator/method_call.cr | 8 ++---- 5 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 src/spectator/generic_method_call.cr create mode 100644 src/spectator/matchers/receive_matcher.cr diff --git a/src/spectator/double.cr b/src/spectator/double.cr index 5ac71cb..1674045 100644 --- a/src/spectator/double.cr +++ b/src/spectator/double.cr @@ -1,8 +1,10 @@ -require "./method_stub" +require "./generic_method_call" +require "./generic_method_stub" module Spectator abstract class Double @spectator_stubs = Deque(MethodStub).new + @spectator_stub_calls = Deque(MethodCall).new private macro stub(definition, &block) {% @@ -27,7 +29,7 @@ module Spectator {% if name.ends_with?('=') && name.id != "[]=" %} def {{name}}(arg) - call = ::Spectator::MethodCall.new({{name.symbolize}}, {arg}, NamedTuple.new) + call = ::Spectator::GenericMethodCall.new({{name.symbolize}}, {arg}, NamedTuple.new) stub = @spectator_stubs.find(&.callable?(call)) if stub stub.as(::Spectator::GenericMethodStub(typeof(%method(arg)))).call(call) @@ -37,7 +39,7 @@ module Spectator end {% else %} def {{name}}(*args, **options){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} - call = ::Spectator::MethodCall.new({{name.symbolize}}, args, options) + call = ::Spectator::GenericMethodCall.new({{name.symbolize}}, args, options) stub = @spectator_stubs.find(&.callable?(call)) if stub stub.as(::Spectator::GenericMethodStub(typeof(%method(*args, **options)))).call(call) @@ -48,7 +50,7 @@ module Spectator {% if name != "[]=" %} def {{name}}(*args, **options){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} - call = ::Spectator::MethodCall.new({{name.symbolize}}, args, options) + call = ::Spectator::GenericMethodCall.new({{name.symbolize}}, args, options) stub = @spectator_stubs.find(&.callable?(call)) if stub stub.as(::Spectator::GenericMethodStub(typeof(%method(*args, **options) { |*yield_args| yield *yield_args }))).call(call) @@ -79,5 +81,9 @@ module Spectator protected def spectator_define_stub(stub : MethodStub) : Nil @spectator_stubs << stub end + + protected def spectator_stub_calls(method : Symbol) : Array(MethodCall) + @spectator_stub_calls.select { |call| call.name == method } + end end end diff --git a/src/spectator/dsl/matchers.cr b/src/spectator/dsl/matchers.cr index 5c55a06..0e779c3 100644 --- a/src/spectator/dsl/matchers.cr +++ b/src/spectator/dsl/matchers.cr @@ -683,6 +683,11 @@ module Spectator expect {{block}}.to raise_error({{type}}, {{message}}) end + macro have_received(method) + %test_value = ::Spectator::TestValue.new(({{method.id.symbolize}}), {{method.id.stringify}}) + ::Spectator::Matchers::ReceiveMatcher.new(%test_value) + end + # Used to create predicate matchers. # Any missing method that starts with 'be_' or 'have_' will be handled. # All other method names will be ignored and raise a compile-time error. diff --git a/src/spectator/generic_method_call.cr b/src/spectator/generic_method_call.cr new file mode 100644 index 0000000..2d4d31d --- /dev/null +++ b/src/spectator/generic_method_call.cr @@ -0,0 +1,13 @@ +require "./method_call" + +module Spectator + class GenericMethodCall(T, NT) < MethodCall + getter args : T + + getter options : NT + + def initialize(name : Symbol, @args : T, @options : NT) + super(name) + end + end +end diff --git a/src/spectator/matchers/receive_matcher.cr b/src/spectator/matchers/receive_matcher.cr new file mode 100644 index 0000000..f6f88cf --- /dev/null +++ b/src/spectator/matchers/receive_matcher.cr @@ -0,0 +1,30 @@ +require "../double" +require "./standard_matcher" + +module Spectator::Matchers + struct ReceiveMatcher < StandardMatcher + def initialize(@expected : TestExpression(Symbol)) + end + + def description : String + "received message #{@expected.label}" + end + + def match?(actual : TestExpression(T)) : Bool forall T + double = actual.value.as(Double) + calls = double.spectator_stub_calls(@expected.value) + !calls.empty? + end + + def failure_message(actual : TestExpression(T)) : String forall T + "#{actual.value} did not receive #{@expected.label}" + end + + def values(actual : TestExpression(T)) forall T + { + expected: "1 time with any arguments", + received: "0 times with any arguments" + } + end + end +end diff --git a/src/spectator/method_call.cr b/src/spectator/method_call.cr index da94054..719b608 100644 --- a/src/spectator/method_call.cr +++ b/src/spectator/method_call.cr @@ -1,12 +1,8 @@ module Spectator - class MethodCall(T, NT) + abstract class MethodCall getter name : Symbol - getter args : T - - getter options : NT - - def initialize(@name : Symbol, @args : T, @options : NT) + def initialize(@name : Symbol) end end end From 76fa7e294f7e6248177d2508646198ea6527a3b7 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 13 Oct 2019 18:49:22 -0600 Subject: [PATCH 084/205] Friendlier descriptions of objects --- src/spectator/double.cr | 9 +++++++++ src/spectator/dsl/mocks.cr | 1 + src/spectator/generic_method_call.cr | 11 +++++++++++ src/spectator/matchers/receive_matcher.cr | 2 +- 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/spectator/double.cr b/src/spectator/double.cr index 1674045..6a88c60 100644 --- a/src/spectator/double.cr +++ b/src/spectator/double.cr @@ -6,6 +6,9 @@ module Spectator @spectator_stubs = Deque(MethodStub).new @spectator_stub_calls = Deque(MethodCall).new + def initialize(@spectator_double_name : Symbol) + end + private macro stub(definition, &block) {% name = nil @@ -78,6 +81,12 @@ module Spectator end end + def to_s(io) + io << "Double(" + io << @spectator_double_name + io << ')' + end + protected def spectator_define_stub(stub : MethodStub) : Nil @spectator_stubs << stub end diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index fdd4ed2..afcb681 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -9,6 +9,7 @@ module Spectator::DSL {% else %} class Double{{name.id}} < ::Spectator::Double def initialize(@spectator_test : {{@type.id}}) + super({{name.id.symbolize}}) end forward_missing_to @spectator_test diff --git a/src/spectator/generic_method_call.cr b/src/spectator/generic_method_call.cr index 2d4d31d..1afac3a 100644 --- a/src/spectator/generic_method_call.cr +++ b/src/spectator/generic_method_call.cr @@ -9,5 +9,16 @@ module Spectator def initialize(name : Symbol, @args : T, @options : NT) super(name) end + + def to_s(io) + io << name + return if @args.empty? && @options.empty? + + io << '(' + io << @args + io << ", " if @args.any? + io << @options + io << ')' + end end end diff --git a/src/spectator/matchers/receive_matcher.cr b/src/spectator/matchers/receive_matcher.cr index f6f88cf..89b627c 100644 --- a/src/spectator/matchers/receive_matcher.cr +++ b/src/spectator/matchers/receive_matcher.cr @@ -17,7 +17,7 @@ module Spectator::Matchers end def failure_message(actual : TestExpression(T)) : String forall T - "#{actual.value} did not receive #{@expected.label}" + "#{actual.label} did not receive #{@expected.label}" end def values(actual : TestExpression(T)) forall T From 3dd691a990142293ca612ef036ccd37c335a9f23 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 13 Oct 2019 18:50:19 -0600 Subject: [PATCH 085/205] Record call to stub --- src/spectator/double.cr | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/spectator/double.cr b/src/spectator/double.cr index 6a88c60..4d3a12a 100644 --- a/src/spectator/double.cr +++ b/src/spectator/double.cr @@ -33,6 +33,7 @@ module Spectator {% if name.ends_with?('=') && name.id != "[]=" %} def {{name}}(arg) call = ::Spectator::GenericMethodCall.new({{name.symbolize}}, {arg}, NamedTuple.new) + @spectator_stub_calls << call stub = @spectator_stubs.find(&.callable?(call)) if stub stub.as(::Spectator::GenericMethodStub(typeof(%method(arg)))).call(call) @@ -43,6 +44,7 @@ module Spectator {% else %} def {{name}}(*args, **options){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} call = ::Spectator::GenericMethodCall.new({{name.symbolize}}, args, options) + @spectator_stub_calls << call stub = @spectator_stubs.find(&.callable?(call)) if stub stub.as(::Spectator::GenericMethodStub(typeof(%method(*args, **options)))).call(call) @@ -54,6 +56,7 @@ module Spectator {% if name != "[]=" %} def {{name}}(*args, **options){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} call = ::Spectator::GenericMethodCall.new({{name.symbolize}}, args, options) + @spectator_stub_calls << call stub = @spectator_stubs.find(&.callable?(call)) if stub stub.as(::Spectator::GenericMethodStub(typeof(%method(*args, **options) { |*yield_args| yield *yield_args }))).call(call) From e035afa85fda487861ff400e7054f761370d510a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 24 Oct 2019 20:47:48 -0600 Subject: [PATCH 086/205] Rework let and subject to allow super This addresses https://gitlab.com/arctic-fox/spectator/issues/32 --- src/spectator/dsl/values.cr | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/spectator/dsl/values.cr b/src/spectator/dsl/values.cr index 194e853..ea9fdf5 100644 --- a/src/spectator/dsl/values.cr +++ b/src/spectator/dsl/values.cr @@ -1,17 +1,17 @@ module Spectator module DSL macro let(name, &block) - def %value - {{block.body}} - end - @%wrapper : ::Spectator::ValueWrapper? + def {{name.id}} + {{block.body}} + end + def {{name.id}} if (wrapper = @%wrapper) - wrapper.as(::Spectator::TypedValueWrapper(typeof(%value))).value + wrapper.as(::Spectator::TypedValueWrapper(typeof(previous_def))).value else - %value.tap do |value| + previous_def.tap do |value| @%wrapper = ::Spectator::TypedValueWrapper.new(value) end end From 541dc661ca0de5908c5ff420cc0fd31dd92c2145 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 2 Nov 2019 09:45:34 -0600 Subject: [PATCH 087/205] Only accept exact parameters, don't use splats --- src/spectator/double.cr | 63 ++++++++++++---------------- src/spectator/generic_method_call.cr | 4 ++ 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/src/spectator/double.cr b/src/spectator/double.cr index 4d3a12a..424f850 100644 --- a/src/spectator/double.cr +++ b/src/spectator/double.cr @@ -16,9 +16,15 @@ module Spectator args = nil body = nil if definition.is_a?(Call) # stub foo { :bar } + named = false name = definition.name.id params = definition.args - args = params.map { |p| p.is_a?(TypeDeclaration) ? p.var : p.id } + args = params.map do |p| + n = p.is_a?(TypeDeclaration) ? p.var : p.id + r = named ? "#{n}: #{n}".id : n + named = true if n.starts_with?('*') + r + end body = definition.block.is_a?(Nop) ? block : definition.block elsif definition.is_a?(TypeDeclaration) # stub foo : Symbol name = definition.var @@ -30,51 +36,34 @@ module Spectator end %} - {% if name.ends_with?('=') && name.id != "[]=" %} - def {{name}}(arg) - call = ::Spectator::GenericMethodCall.new({{name.symbolize}}, {arg}, NamedTuple.new) - @spectator_stub_calls << call - stub = @spectator_stubs.find(&.callable?(call)) - if stub - stub.as(::Spectator::GenericMethodStub(typeof(%method(arg)))).call(call) - else - %method(arg) - end - end - {% else %} - def {{name}}(*args, **options){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} - call = ::Spectator::GenericMethodCall.new({{name.symbolize}}, args, options) - @spectator_stub_calls << call - stub = @spectator_stubs.find(&.callable?(call)) - if stub - stub.as(::Spectator::GenericMethodStub(typeof(%method(*args, **options)))).call(call) - else - %method(*args, **options) - end + def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} + %call = ::Spectator::GenericMethodCall.create({{name.symbolize}}{% unless args.empty? %}, {{args.splat}}{% end %}) + @spectator_stub_calls << %call + if (%stub = @spectator_stubs.find(&.callable?(%call))) + %stub.as(::Spectator::GenericMethodStub(typeof(%method({{args.splat}})))).call(%call) + else + %method({{args.splat}}) end + end - {% if name != "[]=" %} - def {{name}}(*args, **options){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} - call = ::Spectator::GenericMethodCall.new({{name.symbolize}}, args, options) - @spectator_stub_calls << call - stub = @spectator_stubs.find(&.callable?(call)) - if stub - stub.as(::Spectator::GenericMethodStub(typeof(%method(*args, **options) { |*yield_args| yield *yield_args }))).call(call) - else - %method(*args, **options) do |*yield_args| - yield *yield_args - end - end + def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} + %call = ::Spectator::GenericMethodCall.create({{name.symbolize}}{% unless args.empty? %}, {{args.splat}}{% end %}) + @spectator_stub_calls << %call + if (%stub = @spectator_stubs.find(&.callable?(%call))) + %stub.as(::Spectator::GenericMethodStub(typeof(%method({{args.splat}}) { |*%yield_args| yield *%yield_args }))).call(%call) + else + %method({{args.splat}}) do |*%yield_args| + yield *%yield_args end - {% end %} - {% end %} + end + end def %method({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} {% if body && !body.is_a?(Nop) %} {{body.body}} {% else %} raise "Stubbed method called without being allowed" - # This code shouldn't be reached, but makes the compiler happy to have a matching type. + # This code shouldn't be reached, but makes the compiler happy to have a matching return type. {% if definition.is_a?(TypeDeclaration) %} %x = uninitialized {{definition.type}} {% else %} diff --git a/src/spectator/generic_method_call.cr b/src/spectator/generic_method_call.cr index 1afac3a..75050ce 100644 --- a/src/spectator/generic_method_call.cr +++ b/src/spectator/generic_method_call.cr @@ -10,6 +10,10 @@ module Spectator super(name) end + def self.create(name : Symbol, *args, **options) + GenericMethodCall.new(name, args, options) + end + def to_s(io) io << name return if @args.empty? && @options.empty? From 42aaae7908b3f077fd76fa2ebf5cbed0610ece06 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 2 Nov 2019 19:58:47 -0600 Subject: [PATCH 088/205] Some initial work on mocks --- src/spectator/dsl/mocks.cr | 25 ++++++++++++ src/spectator/stubs.cr | 82 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 src/spectator/stubs.cr diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index afcb681..d45ec2d 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -1,6 +1,7 @@ require "../double" require "../generic_method_stub" require "../open_mock" +require "../stubs" module Spectator::DSL macro double(name, &block) @@ -19,6 +20,30 @@ module Spectator::DSL {% end %} end + macro mock(name, &block) + {% if block.is_a?(Nop) %} + {{name}}.new.tap do |%inst| + %inst.spectator_test = self + end + {% else %} + {% resolved = name.resolve + type = if resolved < Reference + :class + elsif resolved < Value + :struct + else + :module + end + %} + {{type.id}} ::{{resolved.id}} + include ::Spectator::Stubs + + {{block.body}} + end + {% end %} + {% debug %} + end + def allow(double : ::Spectator::Double) OpenMock.new(double) end diff --git a/src/spectator/stubs.cr b/src/spectator/stubs.cr new file mode 100644 index 0000000..acb68ff --- /dev/null +++ b/src/spectator/stubs.cr @@ -0,0 +1,82 @@ +module Spectator + module Stubs + private macro stub(definition, &block) + {% + name = nil + params = nil + args = nil + body = nil + if definition.is_a?(Call) # stub foo { :bar } + name = definition.name.id + params = definition.args + args = params.map { |p| p.is_a?(TypeDeclaration) ? p.var : p.id } + body = definition.block.is_a?(Nop) ? block : definition.block + elsif definition.is_a?(TypeDeclaration) # stub foo : Symbol + name = definition.var + params = [] of MacroId + args = [] of MacroId + body = block + else + raise "Unrecognized stub format" + end + %} + def {{name}}{% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} + %method + end + + {% if name.ends_with?('=') && name.id != "[]=" %} + def {{name}}(arg) + call = ::Spectator::GenericMethodCall.new({{name.symbolize}}, {arg}, NamedTuple.new) + @spectator_stub_calls << call + stub = @spectator_stubs.find(&.callable?(call)) + if stub + stub.as(::Spectator::GenericMethodStub(typeof(%method(arg)))).call(call) + else + %method(arg) + end + end + {% else %} + def {{name}}(*args, **options){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} + call = ::Spectator::GenericMethodCall.new({{name.symbolize}}, args, options) + @spectator_stub_calls << call + stub = @spectator_stubs.find(&.callable?(call)) + if stub + stub.as(::Spectator::GenericMethodStub(typeof(%method(*args, **options)))).call(call) + else + %method(*args, **options) + end + end + + {% if name != "[]=" %} + def {{name}}(*args, **options){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} + call = ::Spectator::GenericMethodCall.new({{name.symbolize}}, args, options) + @spectator_stub_calls << call + stub = @spectator_stubs.find(&.callable?(call)) + if stub + stub.as(::Spectator::GenericMethodStub(typeof(%method(*args, **options) { |*yield_args| yield *yield_args }))).call(call) + else + %method(*args, **options) do |*yield_args| + yield *yield_args + end + end + end + {% end %} + {% end %} + + def %method({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} + {% if body && !body.is_a?(Nop) %} + {{body.body}} + {% else %} + raise "Stubbed method called without being allowed" + # This code shouldn't be reached, but makes the compiler happy to have a matching type. + {% if definition.is_a?(TypeDeclaration) %} + %x = uninitialized {{definition.type}} + {% else %} + nil + {% end %} + {% end %} + end + {% debug %} + end + end +end From c95e228bdef9eb4f511ddb15eb231d701995daca Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 3 Nov 2019 09:44:32 -0700 Subject: [PATCH 089/205] Move mocks to their own module --- src/spectator/dsl/matchers.cr | 56 +++++++++---------- src/spectator/dsl/mocks.cr | 12 ++-- src/spectator/includes.cr | 3 +- src/spectator/matchers/receive_matcher.cr | 4 +- src/spectator/mocks.cr | 7 +++ src/spectator/{ => mocks}/double.cr | 10 ++-- .../{ => mocks}/generic_method_call.cr | 2 +- .../{ => mocks}/generic_method_stub.cr | 4 +- src/spectator/{ => mocks}/method_call.cr | 2 +- src/spectator/{ => mocks}/method_stub.cr | 4 +- src/spectator/{ => mocks}/mock.cr | 2 +- src/spectator/{ => mocks}/open_mock.cr | 2 +- 12 files changed, 56 insertions(+), 52 deletions(-) create mode 100644 src/spectator/mocks.cr rename src/spectator/{ => mocks}/double.cr (83%) rename src/spectator/{ => mocks}/generic_method_call.cr (95%) rename src/spectator/{ => mocks}/generic_method_stub.cr (91%) rename src/spectator/{ => mocks}/method_call.cr (81%) rename src/spectator/{ => mocks}/method_stub.cr (80%) rename src/spectator/{ => mocks}/mock.cr (98%) rename src/spectator/{ => mocks}/open_mock.cr (86%) diff --git a/src/spectator/dsl/matchers.cr b/src/spectator/dsl/matchers.cr index 0e779c3..572aea6 100644 --- a/src/spectator/dsl/matchers.cr +++ b/src/spectator/dsl/matchers.cr @@ -703,33 +703,33 @@ module Spectator # # Is equivalent to: # expect("foobar".has_back_references?).to_not be_true # ``` - macro method_missing(call) - {% if call.name.starts_with?("be_") %} - # Remove `be_` prefix. - {% method_name = call.name[3..-1] %} - {% matcher = "PredicateMatcher" %} - {% elsif call.name.starts_with?("have_") %} - # Remove `have_` prefix. - {% method_name = call.name[5..-1] %} - {% matcher = "HavePredicateMatcher" %} - {% else %} - {% raise "Undefined local variable or method '#{call}'" %} - {% end %} - - descriptor = { {{method_name}}: Tuple.new({{call.args.splat}}) } - label = String::Builder.new({{method_name.stringify}}) - {% unless call.args.empty? %} - label << '(' - {% for arg, index in call.args %} - label << {{arg}} - {% if index < call.args.size - 1 %} - label << ", " - {% end %} - {% end %} - label << ')' - {% end %} - test_value = ::Spectator::TestValue.new(descriptor, label.to_s) - ::Spectator::Matchers::{{matcher.id}}.new(test_value) - end + # macro method_missing(call) + # {% if call.name.starts_with?("be_") %} + # # Remove `be_` prefix. + # {% method_name = call.name[3..-1] %} + # {% matcher = "PredicateMatcher" %} + # {% elsif call.name.starts_with?("have_") %} + # # Remove `have_` prefix. + # {% method_name = call.name[5..-1] %} + # {% matcher = "HavePredicateMatcher" %} + # {% else %} + # {% raise "Undefined local variable or method '#{call}'" %} + # {% end %} + # + # descriptor = { {{method_name}}: Tuple.new({{call.args.splat}}) } + # label = String::Builder.new({{method_name.stringify}}) + # {% unless call.args.empty? %} + # label << '(' + # {% for arg, index in call.args %} + # label << {{arg}} + # {% if index < call.args.size - 1 %} + # label << ", " + # {% end %} + # {% end %} + # label << ')' + # {% end %} + # test_value = ::Spectator::TestValue.new(descriptor, label.to_s) + # ::Spectator::Matchers::{{matcher.id}}.new(test_value) + # end end end diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index afcb681..be05616 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -1,13 +1,11 @@ -require "../double" -require "../generic_method_stub" -require "../open_mock" +require "../mocks" module Spectator::DSL macro double(name, &block) {% if block.is_a?(Nop) %} Double{{name.id}}.new(self) {% else %} - class Double{{name.id}} < ::Spectator::Double + class Double{{name.id}} < ::Spectator::Mocks::Double def initialize(@spectator_test : {{@type.id}}) super({{name.id.symbolize}}) end @@ -19,12 +17,12 @@ module Spectator::DSL {% end %} end - def allow(double : ::Spectator::Double) - OpenMock.new(double) + def allow(double : ::Spectator::Mocks::Double) + Mocks::OpenMock.new(double) end macro receive(method_name, _source_file = __FILE__, _source_line = __LINE__) %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - ::Spectator::GenericMethodStub(Nil).new({{method_name.symbolize}}, %source, ->{ nil }) + ::Spectator::Mocks::GenericMethodStub(Nil).new({{method_name.symbolize}}, %source, ->{ nil }) end end diff --git a/src/spectator/includes.cr b/src/spectator/includes.cr index d103cfa..4425f99 100644 --- a/src/spectator/includes.cr +++ b/src/spectator/includes.cr @@ -29,8 +29,7 @@ require "./example_group" require "./nested_example_group" require "./root_example_group" -require "./mock" -require "./double" +require "./mocks" require "./config" require "./config_builder" diff --git a/src/spectator/matchers/receive_matcher.cr b/src/spectator/matchers/receive_matcher.cr index 89b627c..7bb8756 100644 --- a/src/spectator/matchers/receive_matcher.cr +++ b/src/spectator/matchers/receive_matcher.cr @@ -1,4 +1,4 @@ -require "../double" +require "../mocks/double" require "./standard_matcher" module Spectator::Matchers @@ -11,7 +11,7 @@ module Spectator::Matchers end def match?(actual : TestExpression(T)) : Bool forall T - double = actual.value.as(Double) + double = actual.value.as(Mocks::Double) calls = double.spectator_stub_calls(@expected.value) !calls.empty? end diff --git a/src/spectator/mocks.cr b/src/spectator/mocks.cr new file mode 100644 index 0000000..f50bf9e --- /dev/null +++ b/src/spectator/mocks.cr @@ -0,0 +1,7 @@ +require "./mocks/*" + +module Spectator + # Functionality for mocking existing types. + module Mocks + end +end diff --git a/src/spectator/double.cr b/src/spectator/mocks/double.cr similarity index 83% rename from src/spectator/double.cr rename to src/spectator/mocks/double.cr index 424f850..83c3679 100644 --- a/src/spectator/double.cr +++ b/src/spectator/mocks/double.cr @@ -1,7 +1,7 @@ require "./generic_method_call" require "./generic_method_stub" -module Spectator +module Spectator::Mocks abstract class Double @spectator_stubs = Deque(MethodStub).new @spectator_stub_calls = Deque(MethodCall).new @@ -37,20 +37,20 @@ module Spectator %} def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} - %call = ::Spectator::GenericMethodCall.create({{name.symbolize}}{% unless args.empty? %}, {{args.splat}}{% end %}) + %call = ::Spectator::Mocks::GenericMethodCall.create({{name.symbolize}}{% unless args.empty? %}, {{args.splat}}{% end %}) @spectator_stub_calls << %call if (%stub = @spectator_stubs.find(&.callable?(%call))) - %stub.as(::Spectator::GenericMethodStub(typeof(%method({{args.splat}})))).call(%call) + %stub.as(::Spectator::Mocks::GenericMethodStub(typeof(%method({{args.splat}})))).call(%call) else %method({{args.splat}}) end end def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} - %call = ::Spectator::GenericMethodCall.create({{name.symbolize}}{% unless args.empty? %}, {{args.splat}}{% end %}) + %call = ::Spectator::Mocks::GenericMethodCall.create({{name.symbolize}}{% unless args.empty? %}, {{args.splat}}{% end %}) @spectator_stub_calls << %call if (%stub = @spectator_stubs.find(&.callable?(%call))) - %stub.as(::Spectator::GenericMethodStub(typeof(%method({{args.splat}}) { |*%yield_args| yield *%yield_args }))).call(%call) + %stub.as(::Spectator::Mocks::GenericMethodStub(typeof(%method({{args.splat}}) { |*%yield_args| yield *%yield_args }))).call(%call) else %method({{args.splat}}) do |*%yield_args| yield *%yield_args diff --git a/src/spectator/generic_method_call.cr b/src/spectator/mocks/generic_method_call.cr similarity index 95% rename from src/spectator/generic_method_call.cr rename to src/spectator/mocks/generic_method_call.cr index 75050ce..f413537 100644 --- a/src/spectator/generic_method_call.cr +++ b/src/spectator/mocks/generic_method_call.cr @@ -1,6 +1,6 @@ require "./method_call" -module Spectator +module Spectator::Mocks class GenericMethodCall(T, NT) < MethodCall getter args : T diff --git a/src/spectator/generic_method_stub.cr b/src/spectator/mocks/generic_method_stub.cr similarity index 91% rename from src/spectator/generic_method_stub.cr rename to src/spectator/mocks/generic_method_stub.cr index 0c752ab..c3349ae 100644 --- a/src/spectator/generic_method_stub.cr +++ b/src/spectator/mocks/generic_method_stub.cr @@ -1,8 +1,8 @@ +require "../source" require "./method_call" require "./method_stub" -require "./source" -module Spectator +module Spectator::Mocks class GenericMethodStub(ReturnType) < MethodStub def initialize(name : Symbol, source : Source, @proc : -> ReturnType) super(name, source) diff --git a/src/spectator/method_call.cr b/src/spectator/mocks/method_call.cr similarity index 81% rename from src/spectator/method_call.cr rename to src/spectator/mocks/method_call.cr index 719b608..7c85862 100644 --- a/src/spectator/method_call.cr +++ b/src/spectator/mocks/method_call.cr @@ -1,4 +1,4 @@ -module Spectator +module Spectator::Mocks abstract class MethodCall getter name : Symbol diff --git a/src/spectator/method_stub.cr b/src/spectator/mocks/method_stub.cr similarity index 80% rename from src/spectator/method_stub.cr rename to src/spectator/mocks/method_stub.cr index 856ed1e..3bc3790 100644 --- a/src/spectator/method_stub.cr +++ b/src/spectator/mocks/method_stub.cr @@ -1,6 +1,6 @@ -require "./source" +require "../source" -module Spectator +module Spectator::Mocks abstract class MethodStub def initialize(@name : Symbol, @source : Source) end diff --git a/src/spectator/mock.cr b/src/spectator/mocks/mock.cr similarity index 98% rename from src/spectator/mock.cr rename to src/spectator/mocks/mock.cr index 76df55b..e46b6a9 100644 --- a/src/spectator/mock.cr +++ b/src/spectator/mocks/mock.cr @@ -1,4 +1,4 @@ -module Spectator +module Spectator::Mocks module Mock macro included {% for meth in @type.methods %} diff --git a/src/spectator/open_mock.cr b/src/spectator/mocks/open_mock.cr similarity index 86% rename from src/spectator/open_mock.cr rename to src/spectator/mocks/open_mock.cr index 64068ce..b4549f9 100644 --- a/src/spectator/open_mock.cr +++ b/src/spectator/mocks/open_mock.cr @@ -1,4 +1,4 @@ -module Spectator +module Spectator::Mocks struct OpenMock def initialize(@mock : Double) end From 5b143cb72cd09e6fe4399edc1ad1eda2dcf293ed Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 3 Nov 2019 10:02:53 -0700 Subject: [PATCH 090/205] Change structure around --- src/spectator/dsl/mocks.cr | 2 +- src/spectator/mocks/arguments.cr | 26 ++++++++++++++++++++++ src/spectator/mocks/double.cr | 10 +++++---- src/spectator/mocks/generic_method_call.cr | 22 +++--------------- src/spectator/mocks/generic_method_stub.cr | 21 +++-------------- src/spectator/mocks/proc_method_stub.cr | 14 ++++++++++++ src/spectator/mocks/value_method_stub.cr | 18 +++++++++++++++ 7 files changed, 71 insertions(+), 42 deletions(-) create mode 100644 src/spectator/mocks/arguments.cr create mode 100644 src/spectator/mocks/proc_method_stub.cr create mode 100644 src/spectator/mocks/value_method_stub.cr diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index be05616..4ccafe9 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -23,6 +23,6 @@ module Spectator::DSL macro receive(method_name, _source_file = __FILE__, _source_line = __LINE__) %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - ::Spectator::Mocks::GenericMethodStub(Nil).new({{method_name.symbolize}}, %source, ->{ nil }) + ::Spectator::Mocks::ValueMethodStub.new({{method_name.symbolize}}, %source, nil) end end diff --git a/src/spectator/mocks/arguments.cr b/src/spectator/mocks/arguments.cr new file mode 100644 index 0000000..7d70a69 --- /dev/null +++ b/src/spectator/mocks/arguments.cr @@ -0,0 +1,26 @@ +module Spectator::Mocks + class Arguments(T, NT) + protected getter args + protected getter opts + + def initialize(@args : T, @opts : NT) + end + + def self.create(*args, **opts) + Arguments.new(args, opts) + end + + def pass_to(dispatcher) + dispatcher.call(*@args, **@opts) + end + + def ===(other : Arguments(U, NU)) : Bool forall U, NU + return false unless @args === other.args + return false unless @opts.size === other.opts.size + + @opts.keys.all? do |key| + other.opts.has_key?(key) && @opts[key] === other.opts[key] + end + end + end +end diff --git a/src/spectator/mocks/double.cr b/src/spectator/mocks/double.cr index 83c3679..5af252c 100644 --- a/src/spectator/mocks/double.cr +++ b/src/spectator/mocks/double.cr @@ -37,20 +37,22 @@ module Spectator::Mocks %} def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} - %call = ::Spectator::Mocks::GenericMethodCall.create({{name.symbolize}}{% unless args.empty? %}, {{args.splat}}{% end %}) + %args = ::Spectator::Mocks::Arguments.create({{args.splat}}) + %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) @spectator_stub_calls << %call if (%stub = @spectator_stubs.find(&.callable?(%call))) - %stub.as(::Spectator::Mocks::GenericMethodStub(typeof(%method({{args.splat}})))).call(%call) + %stub.as(::Spectator::Mocks::GenericMethodStub(typeof(%method({{args.splat}})))).call(%args) else %method({{args.splat}}) end end def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} - %call = ::Spectator::Mocks::GenericMethodCall.create({{name.symbolize}}{% unless args.empty? %}, {{args.splat}}{% end %}) + %args = ::Spectator::Mocks::Arguments.create({{args.splat}}) + %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) @spectator_stub_calls << %call if (%stub = @spectator_stubs.find(&.callable?(%call))) - %stub.as(::Spectator::Mocks::GenericMethodStub(typeof(%method({{args.splat}}) { |*%yield_args| yield *%yield_args }))).call(%call) + %stub.as(::Spectator::Mocks::GenericMethodStub(typeof(%method({{args.splat}}) { |*%yield_args| yield *%yield_args }))).call(%args) else %method({{args.splat}}) do |*%yield_args| yield *%yield_args diff --git a/src/spectator/mocks/generic_method_call.cr b/src/spectator/mocks/generic_method_call.cr index f413537..639e825 100644 --- a/src/spectator/mocks/generic_method_call.cr +++ b/src/spectator/mocks/generic_method_call.cr @@ -1,28 +1,12 @@ +require "./arguments" require "./method_call" module Spectator::Mocks class GenericMethodCall(T, NT) < MethodCall - getter args : T + getter args - getter options : NT - - def initialize(name : Symbol, @args : T, @options : NT) + def initialize(name : Symbol, @args : Arguments(T, NT)) super(name) end - - def self.create(name : Symbol, *args, **options) - GenericMethodCall.new(name, args, options) - end - - def to_s(io) - io << name - return if @args.empty? && @options.empty? - - io << '(' - io << @args - io << ", " if @args.any? - io << @options - io << ')' - end end end diff --git a/src/spectator/mocks/generic_method_stub.cr b/src/spectator/mocks/generic_method_stub.cr index c3349ae..e5f21ba 100644 --- a/src/spectator/mocks/generic_method_stub.cr +++ b/src/spectator/mocks/generic_method_stub.cr @@ -1,23 +1,8 @@ -require "../source" -require "./method_call" +require "./arguments" require "./method_stub" module Spectator::Mocks - class GenericMethodStub(ReturnType) < MethodStub - def initialize(name : Symbol, source : Source, @proc : -> ReturnType) - super(name, source) - end - - def callable?(call : MethodCall) : Bool - super - end - - def call(call : MethodCall) : ReturnType - @proc.call - end - - def and_return(value : T) forall T - GenericMethodStub(T).new(@name, @source, ->{ value }) - end + abstract class GenericMethodStub(ReturnType) < MethodStub + abstract def call(args : Arguments(T, NT)) : ReturnType forall T, NT end end diff --git a/src/spectator/mocks/proc_method_stub.cr b/src/spectator/mocks/proc_method_stub.cr new file mode 100644 index 0000000..e1b1956 --- /dev/null +++ b/src/spectator/mocks/proc_method_stub.cr @@ -0,0 +1,14 @@ +require "./arguments" +require "./generic_method_stub" + +module Spectator::Mocks + class ProcMethodStub(ReturnType) < GenericMethodStub(ReturnType) + def initialize(name, source, @proc : -> ReturnType) + super(name, source) + end + + def call(_args : Arguments(T, NT)) : ReturnType forall T, NT + @proc.call + end + end +end diff --git a/src/spectator/mocks/value_method_stub.cr b/src/spectator/mocks/value_method_stub.cr new file mode 100644 index 0000000..3b02d33 --- /dev/null +++ b/src/spectator/mocks/value_method_stub.cr @@ -0,0 +1,18 @@ +require "./arguments" +require "./generic_method_stub" + +module Spectator::Mocks + class ValueMethodStub(ReturnType) < GenericMethodStub(ReturnType) + def initialize(name, source, @value : ReturnType) + super(name, source) + end + + def call(_args : Arguments(T, NT)) : ReturnType forall T, NT + @value + end + + def and_return(value : T) forall T + ValueMethodStub.new(@name, @source, value) + end + end +end From 3931f420fef61e8c5e428b35b6702bd1abc86e6e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 3 Nov 2019 10:03:09 -0700 Subject: [PATCH 091/205] Formatting --- src/spectator/matchers/receive_matcher.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/matchers/receive_matcher.cr b/src/spectator/matchers/receive_matcher.cr index 7bb8756..8ca3ae8 100644 --- a/src/spectator/matchers/receive_matcher.cr +++ b/src/spectator/matchers/receive_matcher.cr @@ -23,7 +23,7 @@ module Spectator::Matchers def values(actual : TestExpression(T)) forall T { expected: "1 time with any arguments", - received: "0 times with any arguments" + received: "0 times with any arguments", } end end From 20b80cc85a507a54051bf6fa3f7ee14ed8879840 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 3 Nov 2019 10:07:32 -0700 Subject: [PATCH 092/205] Formatting --- src/spectator/dsl/mocks.cr | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 920d29f..5d1e927 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -24,14 +24,13 @@ module Spectator::DSL end {% else %} {% resolved = name.resolve - type = if resolved < Reference - :class - elsif resolved < Value - :struct - else - :module - end - %} + type = if resolved < Reference + :class + elsif resolved < Value + :struct + else + :module + end %} {{type.id}} ::{{resolved.id}} include ::Spectator::Mocks::Stubs From af9104dfe4192f79dfdd491cd12e4caeb512f102 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 3 Nov 2019 11:18:46 -0700 Subject: [PATCH 093/205] Store arguments in method stub Needed for matching arguments (setting constraints). --- src/spectator/mocks/arguments.cr | 24 +------------------ src/spectator/mocks/double.cr | 4 ++-- src/spectator/mocks/generic_arguments.cr | 28 ++++++++++++++++++++++ src/spectator/mocks/generic_method_call.cr | 4 ++-- src/spectator/mocks/generic_method_stub.cr | 12 +++++++++- src/spectator/mocks/method_stub.cr | 3 ++- src/spectator/mocks/proc_method_stub.cr | 6 ++--- src/spectator/mocks/value_method_stub.cr | 6 ++--- 8 files changed, 52 insertions(+), 35 deletions(-) create mode 100644 src/spectator/mocks/generic_arguments.cr diff --git a/src/spectator/mocks/arguments.cr b/src/spectator/mocks/arguments.cr index 7d70a69..e909235 100644 --- a/src/spectator/mocks/arguments.cr +++ b/src/spectator/mocks/arguments.cr @@ -1,26 +1,4 @@ module Spectator::Mocks - class Arguments(T, NT) - protected getter args - protected getter opts - - def initialize(@args : T, @opts : NT) - end - - def self.create(*args, **opts) - Arguments.new(args, opts) - end - - def pass_to(dispatcher) - dispatcher.call(*@args, **@opts) - end - - def ===(other : Arguments(U, NU)) : Bool forall U, NU - return false unless @args === other.args - return false unless @opts.size === other.opts.size - - @opts.keys.all? do |key| - other.opts.has_key?(key) && @opts[key] === other.opts[key] - end - end + abstract class Arguments end end diff --git a/src/spectator/mocks/double.cr b/src/spectator/mocks/double.cr index 5af252c..806e3fc 100644 --- a/src/spectator/mocks/double.cr +++ b/src/spectator/mocks/double.cr @@ -37,7 +37,7 @@ module Spectator::Mocks %} def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} - %args = ::Spectator::Mocks::Arguments.create({{args.splat}}) + %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) @spectator_stub_calls << %call if (%stub = @spectator_stubs.find(&.callable?(%call))) @@ -48,7 +48,7 @@ module Spectator::Mocks end def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} - %args = ::Spectator::Mocks::Arguments.create({{args.splat}}) + %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) @spectator_stub_calls << %call if (%stub = @spectator_stubs.find(&.callable?(%call))) diff --git a/src/spectator/mocks/generic_arguments.cr b/src/spectator/mocks/generic_arguments.cr new file mode 100644 index 0000000..246c78d --- /dev/null +++ b/src/spectator/mocks/generic_arguments.cr @@ -0,0 +1,28 @@ +require "./arguments" + +module Spectator::Mocks + class GenericArguments(T, NT) < Arguments + protected getter args + protected getter opts + + def initialize(@args : T, @opts : NT) + end + + def self.create(*args, **opts) + GenericArguments.new(args, opts) + end + + def pass_to(dispatcher) + dispatcher.call(*@args, **@opts) + end + + def ===(other : Arguments(U, NU)) : Bool forall U, NU + return false unless @args === other.args + return false unless @opts.size === other.opts.size + + @opts.keys.all? do |key| + other.opts.has_key?(key) && @opts[key] === other.opts[key] + end + end + end +end diff --git a/src/spectator/mocks/generic_method_call.cr b/src/spectator/mocks/generic_method_call.cr index 639e825..628e40f 100644 --- a/src/spectator/mocks/generic_method_call.cr +++ b/src/spectator/mocks/generic_method_call.cr @@ -1,11 +1,11 @@ -require "./arguments" +require "./generic_arguments" require "./method_call" module Spectator::Mocks class GenericMethodCall(T, NT) < MethodCall getter args - def initialize(name : Symbol, @args : Arguments(T, NT)) + def initialize(name : Symbol, @args : GenericArguments(T, NT)) super(name) end end diff --git a/src/spectator/mocks/generic_method_stub.cr b/src/spectator/mocks/generic_method_stub.cr index e5f21ba..e29f4b5 100644 --- a/src/spectator/mocks/generic_method_stub.cr +++ b/src/spectator/mocks/generic_method_stub.cr @@ -1,8 +1,18 @@ require "./arguments" +require "./generic_arguments" +require "./method_call" require "./method_stub" module Spectator::Mocks abstract class GenericMethodStub(ReturnType) < MethodStub - abstract def call(args : Arguments(T, NT)) : ReturnType forall T, NT + def initialize(name, source, @args : Arguments? = nil) + super(name, source) + end + + def callable?(call : GenericMethodCall(T, NT)) : Bool forall T, NT + super && (!@args || @args === call.args) + end + + abstract def call(args : GenericArguments(T, NT)) : ReturnType forall T, NT end end diff --git a/src/spectator/mocks/method_stub.cr b/src/spectator/mocks/method_stub.cr index 3bc3790..acdc3d5 100644 --- a/src/spectator/mocks/method_stub.cr +++ b/src/spectator/mocks/method_stub.cr @@ -1,11 +1,12 @@ require "../source" +require "./generic_method_call" module Spectator::Mocks abstract class MethodStub def initialize(@name : Symbol, @source : Source) end - def callable?(call : MethodCall) : Bool + def callable?(call : GenericMethodCall(T, NT)) : Bool forall T, NT @name == call.name end end diff --git a/src/spectator/mocks/proc_method_stub.cr b/src/spectator/mocks/proc_method_stub.cr index e1b1956..f3141ed 100644 --- a/src/spectator/mocks/proc_method_stub.cr +++ b/src/spectator/mocks/proc_method_stub.cr @@ -3,11 +3,11 @@ require "./generic_method_stub" module Spectator::Mocks class ProcMethodStub(ReturnType) < GenericMethodStub(ReturnType) - def initialize(name, source, @proc : -> ReturnType) - super(name, source) + def initialize(name, source, @proc : -> ReturnType, args = nil) + super(name, source, args) end - def call(_args : Arguments(T, NT)) : ReturnType forall T, NT + def call(_args : GenericArguments(T, NT)) : ReturnType forall T, NT @proc.call end end diff --git a/src/spectator/mocks/value_method_stub.cr b/src/spectator/mocks/value_method_stub.cr index 3b02d33..0fa5b77 100644 --- a/src/spectator/mocks/value_method_stub.cr +++ b/src/spectator/mocks/value_method_stub.cr @@ -3,11 +3,11 @@ require "./generic_method_stub" module Spectator::Mocks class ValueMethodStub(ReturnType) < GenericMethodStub(ReturnType) - def initialize(name, source, @value : ReturnType) - super(name, source) + def initialize(name, source, @value : ReturnType, args = nil) + super(name, source, args) end - def call(_args : Arguments(T, NT)) : ReturnType forall T, NT + def call(_args : GenericArguments(T, NT)) : ReturnType forall T, NT @value end From 477271d297f782ef0e9f138145dc866d7ebb1726 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 3 Nov 2019 11:23:04 -0700 Subject: [PATCH 094/205] Use NilMethodStub by default Provides one place that #with can be defined and enforces ordering. --- src/spectator/dsl/mocks.cr | 2 +- src/spectator/mocks/nil_method_stub.cr | 19 +++++++++++++++++++ src/spectator/mocks/value_method_stub.cr | 6 +----- 3 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 src/spectator/mocks/nil_method_stub.cr diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 5d1e927..6e29045 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -46,6 +46,6 @@ module Spectator::DSL macro receive(method_name, _source_file = __FILE__, _source_line = __LINE__) %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - ::Spectator::Mocks::ValueMethodStub.new({{method_name.symbolize}}, %source, nil) + ::Spectator::Mocks::NilMethodStub.new({{method_name.symbolize}}, %source) end end diff --git a/src/spectator/mocks/nil_method_stub.cr b/src/spectator/mocks/nil_method_stub.cr new file mode 100644 index 0000000..68274d2 --- /dev/null +++ b/src/spectator/mocks/nil_method_stub.cr @@ -0,0 +1,19 @@ +require "./generic_arguments" +require "./generic_method_stub" +require "./value_method_stub" + +module Spectator::Mocks + class NilMethodStub < GenericMethodStub(Nil) + def initialize(name, source, args = nil) + super(name, source, args) + end + + def call(_args : GenericArguments(T, NT)) : ReturnType forall T, NT + nil + end + + def and_return(value : T) forall T + ValueMethodStub.new(@name, @source, value) + end + end +end diff --git a/src/spectator/mocks/value_method_stub.cr b/src/spectator/mocks/value_method_stub.cr index 0fa5b77..fbf7421 100644 --- a/src/spectator/mocks/value_method_stub.cr +++ b/src/spectator/mocks/value_method_stub.cr @@ -1,4 +1,4 @@ -require "./arguments" +require "./generic_arguments" require "./generic_method_stub" module Spectator::Mocks @@ -10,9 +10,5 @@ module Spectator::Mocks def call(_args : GenericArguments(T, NT)) : ReturnType forall T, NT @value end - - def and_return(value : T) forall T - ValueMethodStub.new(@name, @source, value) - end end end From 17566707105ec965529a371ad8958b4f5185ece3 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 3 Nov 2019 11:42:07 -0700 Subject: [PATCH 095/205] Add to_s method --- src/spectator/mocks/generic_arguments.cr | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/spectator/mocks/generic_arguments.cr b/src/spectator/mocks/generic_arguments.cr index 246c78d..ea7493a 100644 --- a/src/spectator/mocks/generic_arguments.cr +++ b/src/spectator/mocks/generic_arguments.cr @@ -24,5 +24,19 @@ module Spectator::Mocks other.opts.has_key?(key) && @opts[key] === other.opts[key] end end + + def to_s(io) + @args.each_with_index do |arg, i| + arg.inspect(io) + io << ", " if i < @args.size - 1 + end + io << ", " unless @args.empty? || @opts.empty? + @opts.each_with_index do |key, value, i| + io << key + io << ": " + value.inspect(io) + io << ", " if 1 < @opts.size - 1 + end + end end end From e6c1a6b2d26a7c9b46fcdc85219babd892986650 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 3 Nov 2019 11:42:31 -0700 Subject: [PATCH 096/205] Fix arg passing and implement arg matching --- src/spectator/mocks/generic_arguments.cr | 2 +- src/spectator/mocks/nil_method_stub.cr | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/spectator/mocks/generic_arguments.cr b/src/spectator/mocks/generic_arguments.cr index ea7493a..0da404c 100644 --- a/src/spectator/mocks/generic_arguments.cr +++ b/src/spectator/mocks/generic_arguments.cr @@ -16,7 +16,7 @@ module Spectator::Mocks dispatcher.call(*@args, **@opts) end - def ===(other : Arguments(U, NU)) : Bool forall U, NU + def ===(other : GenericArguments(U, NU)) : Bool forall U, NU return false unless @args === other.args return false unless @opts.size === other.opts.size diff --git a/src/spectator/mocks/nil_method_stub.cr b/src/spectator/mocks/nil_method_stub.cr index 68274d2..9bbdaa0 100644 --- a/src/spectator/mocks/nil_method_stub.cr +++ b/src/spectator/mocks/nil_method_stub.cr @@ -13,7 +13,12 @@ module Spectator::Mocks end def and_return(value : T) forall T - ValueMethodStub.new(@name, @source, value) + ValueMethodStub.new(@name, @source, value, @args) + end + + def with(*args : *T, **opts : **NT) forall T, NT + args = GenericArguments.new(args, opts) + NilMethodStub.new(@name, @source, args) end end end From d1b54ad48fe73cdee8441d6287cb734068c5cae5 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 3 Nov 2019 11:56:04 -0700 Subject: [PATCH 097/205] Better error for when stubs don't match return type --- src/spectator/mocks/double.cr | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/spectator/mocks/double.cr b/src/spectator/mocks/double.cr index 806e3fc..ccba68b 100644 --- a/src/spectator/mocks/double.cr +++ b/src/spectator/mocks/double.cr @@ -41,7 +41,11 @@ module Spectator::Mocks %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) @spectator_stub_calls << %call if (%stub = @spectator_stubs.find(&.callable?(%call))) - %stub.as(::Spectator::Mocks::GenericMethodStub(typeof(%method({{args.splat}})))).call(%args) + if (%cast = %stub.as?(::Spectator::Mocks::GenericMethodStub(typeof(%method({{args.splat}}))))) + %cast.call(%args) + else + raise "The return type of stub #{%stub} doesn't match the expected type #{typeof(%method({{args.splat}}))}" + end else %method({{args.splat}}) end @@ -52,7 +56,11 @@ module Spectator::Mocks %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) @spectator_stub_calls << %call if (%stub = @spectator_stubs.find(&.callable?(%call))) - %stub.as(::Spectator::Mocks::GenericMethodStub(typeof(%method({{args.splat}}) { |*%yield_args| yield *%yield_args }))).call(%args) + if (%cast = %stub.as?(::Spectator::Mocks::GenericMethodStub(typeof(%method({{args.splat}}) { |*%yield_args| yield *%yield_args })))) + %cast.call(%args) + else + raise "The return type of stub #{%stub} doesn't match the expected type #{typeof(%method({{args.splat}}) { |*%ya| yield *%ya })}" + end else %method({{args.splat}}) do |*%yield_args| yield *%yield_args From 929459944044306283e1f9db601a69ee4feec2c7 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 3 Nov 2019 11:59:12 -0700 Subject: [PATCH 098/205] Basic to_s for stubs --- src/spectator/mocks/generic_method_stub.cr | 9 +++++++++ src/spectator/mocks/method_stub.cr | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/src/spectator/mocks/generic_method_stub.cr b/src/spectator/mocks/generic_method_stub.cr index e29f4b5..f5e650e 100644 --- a/src/spectator/mocks/generic_method_stub.cr +++ b/src/spectator/mocks/generic_method_stub.cr @@ -14,5 +14,14 @@ module Spectator::Mocks end abstract def call(args : GenericArguments(T, NT)) : ReturnType forall T, NT + + def to_s(io) + super(io) + if @args + io << '(' + io << @args + io << ')' + end + end end end diff --git a/src/spectator/mocks/method_stub.cr b/src/spectator/mocks/method_stub.cr index acdc3d5..98bc268 100644 --- a/src/spectator/mocks/method_stub.cr +++ b/src/spectator/mocks/method_stub.cr @@ -9,5 +9,10 @@ module Spectator::Mocks def callable?(call : GenericMethodCall(T, NT)) : Bool forall T, NT @name == call.name end + + def to_s(io) + io << '#' + io << @name + end end end From f1a7018718589cfa9b3c6c40663d537168805c25 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 3 Nov 2019 12:05:26 -0700 Subject: [PATCH 099/205] Remove unnecessary initializer --- src/spectator/mocks/nil_method_stub.cr | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/spectator/mocks/nil_method_stub.cr b/src/spectator/mocks/nil_method_stub.cr index 9bbdaa0..d8421c3 100644 --- a/src/spectator/mocks/nil_method_stub.cr +++ b/src/spectator/mocks/nil_method_stub.cr @@ -4,10 +4,6 @@ require "./value_method_stub" module Spectator::Mocks class NilMethodStub < GenericMethodStub(Nil) - def initialize(name, source, args = nil) - super(name, source, args) - end - def call(_args : GenericArguments(T, NT)) : ReturnType forall T, NT nil end From 2a484d58161681ac3f4c4b3afede9ee66765f7e9 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 3 Nov 2019 12:07:21 -0700 Subject: [PATCH 100/205] Fix return type --- src/spectator/mocks/nil_method_stub.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/mocks/nil_method_stub.cr b/src/spectator/mocks/nil_method_stub.cr index d8421c3..798d378 100644 --- a/src/spectator/mocks/nil_method_stub.cr +++ b/src/spectator/mocks/nil_method_stub.cr @@ -4,7 +4,7 @@ require "./value_method_stub" module Spectator::Mocks class NilMethodStub < GenericMethodStub(Nil) - def call(_args : GenericArguments(T, NT)) : ReturnType forall T, NT + def call(_args : GenericArguments(T, NT)) : Nil forall T, NT nil end From 59884f253f93fda7a71d64186b60e554818198f1 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 3 Nov 2019 13:04:44 -0700 Subject: [PATCH 101/205] Better handling of casting with covariance and contravariance --- src/spectator/mocks/double.cr | 12 ++---------- src/spectator/mocks/generic_method_stub.cr | 2 -- src/spectator/mocks/method_stub.cr | 2 ++ src/spectator/mocks/nil_method_stub.cr | 8 ++++++-- src/spectator/mocks/proc_method_stub.cr | 9 +++++++-- src/spectator/mocks/value_method_stub.cr | 9 +++++++-- 6 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/spectator/mocks/double.cr b/src/spectator/mocks/double.cr index ccba68b..a0ca3f4 100644 --- a/src/spectator/mocks/double.cr +++ b/src/spectator/mocks/double.cr @@ -41,11 +41,7 @@ module Spectator::Mocks %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) @spectator_stub_calls << %call if (%stub = @spectator_stubs.find(&.callable?(%call))) - if (%cast = %stub.as?(::Spectator::Mocks::GenericMethodStub(typeof(%method({{args.splat}}))))) - %cast.call(%args) - else - raise "The return type of stub #{%stub} doesn't match the expected type #{typeof(%method({{args.splat}}))}" - end + %stub.call(%args, typeof(%method({{args.splat}}))) else %method({{args.splat}}) end @@ -56,11 +52,7 @@ module Spectator::Mocks %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) @spectator_stub_calls << %call if (%stub = @spectator_stubs.find(&.callable?(%call))) - if (%cast = %stub.as?(::Spectator::Mocks::GenericMethodStub(typeof(%method({{args.splat}}) { |*%yield_args| yield *%yield_args })))) - %cast.call(%args) - else - raise "The return type of stub #{%stub} doesn't match the expected type #{typeof(%method({{args.splat}}) { |*%ya| yield *%ya })}" - end + %stub.call(%args, typeof(%method({{args.splat}}) { |*%ya| yield *%ya })) else %method({{args.splat}}) do |*%yield_args| yield *%yield_args diff --git a/src/spectator/mocks/generic_method_stub.cr b/src/spectator/mocks/generic_method_stub.cr index f5e650e..0ac09ed 100644 --- a/src/spectator/mocks/generic_method_stub.cr +++ b/src/spectator/mocks/generic_method_stub.cr @@ -13,8 +13,6 @@ module Spectator::Mocks super && (!@args || @args === call.args) end - abstract def call(args : GenericArguments(T, NT)) : ReturnType forall T, NT - def to_s(io) super(io) if @args diff --git a/src/spectator/mocks/method_stub.cr b/src/spectator/mocks/method_stub.cr index 98bc268..01020ac 100644 --- a/src/spectator/mocks/method_stub.cr +++ b/src/spectator/mocks/method_stub.cr @@ -10,6 +10,8 @@ module Spectator::Mocks @name == call.name end + abstract def call(args : GenericArguments(T, NT), rt : RT.class) forall T, NT, RT + def to_s(io) io << '#' io << @name diff --git a/src/spectator/mocks/nil_method_stub.cr b/src/spectator/mocks/nil_method_stub.cr index 798d378..9a8954c 100644 --- a/src/spectator/mocks/nil_method_stub.cr +++ b/src/spectator/mocks/nil_method_stub.cr @@ -4,8 +4,12 @@ require "./value_method_stub" module Spectator::Mocks class NilMethodStub < GenericMethodStub(Nil) - def call(_args : GenericArguments(T, NT)) : Nil forall T, NT - nil + def call(_args : GenericArguments(T, NT), rt : RT.class) forall T, NT, RT + if (cast = nil.as?(RT)) + cast + else + raise "The return type of stub #{to_s} : Nil doesn't match the expected type #{RT}" + end end def and_return(value : T) forall T diff --git a/src/spectator/mocks/proc_method_stub.cr b/src/spectator/mocks/proc_method_stub.cr index f3141ed..1960a04 100644 --- a/src/spectator/mocks/proc_method_stub.cr +++ b/src/spectator/mocks/proc_method_stub.cr @@ -7,8 +7,13 @@ module Spectator::Mocks super(name, source, args) end - def call(_args : GenericArguments(T, NT)) : ReturnType forall T, NT - @proc.call + def call(_args : GenericArguments(T, NT), rt : RT.class) forall T, NT, RT + result = @proc.call + if (cast = result.as?(RT)) + cast + else + raise "The return type of stub #{to_s} : #{ReturnType} doesn't match the expected type #{RT}" + end end end end diff --git a/src/spectator/mocks/value_method_stub.cr b/src/spectator/mocks/value_method_stub.cr index fbf7421..f1e989a 100644 --- a/src/spectator/mocks/value_method_stub.cr +++ b/src/spectator/mocks/value_method_stub.cr @@ -7,8 +7,13 @@ module Spectator::Mocks super(name, source, args) end - def call(_args : GenericArguments(T, NT)) : ReturnType forall T, NT - @value + def call(_args : GenericArguments(T, NT), rt : RT.class) forall T, NT, RT + result = @value + if (cast = result.as?(RT)) + cast + else + raise "The return type of stub #{to_s} : #{ReturnType} doesn't match the expected type #{RT}" + end end end end From b107511c9e311959cf6bc939958e218f0718e4c0 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 3 Nov 2019 13:13:54 -0700 Subject: [PATCH 102/205] Include stub source in to_s --- src/spectator/mocks/generic_method_stub.cr | 4 ++++ src/spectator/mocks/nil_method_stub.cr | 2 +- src/spectator/mocks/proc_method_stub.cr | 2 +- src/spectator/mocks/value_method_stub.cr | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/spectator/mocks/generic_method_stub.cr b/src/spectator/mocks/generic_method_stub.cr index 0ac09ed..f695646 100644 --- a/src/spectator/mocks/generic_method_stub.cr +++ b/src/spectator/mocks/generic_method_stub.cr @@ -20,6 +20,10 @@ module Spectator::Mocks io << @args io << ')' end + io << " : " + io << ReturnType + io << " at " + io << @source end end end diff --git a/src/spectator/mocks/nil_method_stub.cr b/src/spectator/mocks/nil_method_stub.cr index 9a8954c..b0d087e 100644 --- a/src/spectator/mocks/nil_method_stub.cr +++ b/src/spectator/mocks/nil_method_stub.cr @@ -8,7 +8,7 @@ module Spectator::Mocks if (cast = nil.as?(RT)) cast else - raise "The return type of stub #{to_s} : Nil doesn't match the expected type #{RT}" + raise "The return type of stub #{self} doesn't match the expected type #{RT}" end end diff --git a/src/spectator/mocks/proc_method_stub.cr b/src/spectator/mocks/proc_method_stub.cr index 1960a04..98f9ca3 100644 --- a/src/spectator/mocks/proc_method_stub.cr +++ b/src/spectator/mocks/proc_method_stub.cr @@ -12,7 +12,7 @@ module Spectator::Mocks if (cast = result.as?(RT)) cast else - raise "The return type of stub #{to_s} : #{ReturnType} doesn't match the expected type #{RT}" + raise "The return type of stub #{self} doesn't match the expected type #{RT}" end end end diff --git a/src/spectator/mocks/value_method_stub.cr b/src/spectator/mocks/value_method_stub.cr index f1e989a..aef48d9 100644 --- a/src/spectator/mocks/value_method_stub.cr +++ b/src/spectator/mocks/value_method_stub.cr @@ -12,7 +12,7 @@ module Spectator::Mocks if (cast = result.as?(RT)) cast else - raise "The return type of stub #{to_s} : #{ReturnType} doesn't match the expected type #{RT}" + raise "The return type of stub #{self} doesn't match the expected type #{RT}" end end end From 2048267eef7feacfca4f3720a0c4f902ce95d2ea Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 3 Nov 2019 13:22:00 -0700 Subject: [PATCH 103/205] Allow specifying receive count Updated checks and failure output to support this. --- src/spectator/matchers/receive_matcher.cr | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/spectator/matchers/receive_matcher.cr b/src/spectator/matchers/receive_matcher.cr index 8ca3ae8..0915821 100644 --- a/src/spectator/matchers/receive_matcher.cr +++ b/src/spectator/matchers/receive_matcher.cr @@ -3,7 +3,7 @@ require "./standard_matcher" module Spectator::Matchers struct ReceiveMatcher < StandardMatcher - def initialize(@expected : TestExpression(Symbol)) + def initialize(@expected : TestExpression(Symbol), @count : Int32? = nil) end def description : String @@ -13,7 +13,11 @@ module Spectator::Matchers def match?(actual : TestExpression(T)) : Bool forall T double = actual.value.as(Mocks::Double) calls = double.spectator_stub_calls(@expected.value) - !calls.empty? + if (count = @count) + count == calls.size + else + !calls.empty? + end end def failure_message(actual : TestExpression(T)) : String forall T @@ -21,9 +25,11 @@ module Spectator::Matchers end def values(actual : TestExpression(T)) forall T + double = actual.value.as(Mocks::Double) + calls = double.spectator_stub_calls(@expected.value) { - expected: "1 time with any arguments", - received: "0 times with any arguments", + expected: "#{@count ? "#{@count} time(s)" : "At least once"} with any arguments", + received: "#{calls.size} time(s) with any arguments", } end end From dabd3a7658330845f5c7eb6a125ed2b312006b7d Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 3 Nov 2019 13:59:38 -0700 Subject: [PATCH 104/205] Use ranges to specify call count --- src/spectator/matchers/receive_matcher.cr | 39 ++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/spectator/matchers/receive_matcher.cr b/src/spectator/matchers/receive_matcher.cr index 0915821..8d3a544 100644 --- a/src/spectator/matchers/receive_matcher.cr +++ b/src/spectator/matchers/receive_matcher.cr @@ -3,7 +3,9 @@ require "./standard_matcher" module Spectator::Matchers struct ReceiveMatcher < StandardMatcher - def initialize(@expected : TestExpression(Symbol), @count : Int32? = nil) + alias Range = ::Range(Int32, Int32) | ::Range(Nil, Int32) | ::Range(Int32, Nil) + + def initialize(@expected : TestExpression(Symbol), @range : Range? = nil) end def description : String @@ -13,8 +15,8 @@ module Spectator::Matchers def match?(actual : TestExpression(T)) : Bool forall T double = actual.value.as(Mocks::Double) calls = double.spectator_stub_calls(@expected.value) - if (count = @count) - count == calls.size + if (range = @range) + range.includes?(calls.size) else !calls.empty? end @@ -27,10 +29,39 @@ module Spectator::Matchers def values(actual : TestExpression(T)) forall T double = actual.value.as(Mocks::Double) calls = double.spectator_stub_calls(@expected.value) + range = @range { - expected: "#{@count ? "#{@count} time(s)" : "At least once"} with any arguments", + expected: "#{range ? "#{humanize_range(range)} time(s)" : "At least once"} with any arguments", received: "#{calls.size} time(s) with any arguments", } end + + def once + ReceiveMatcher.new(@expected, (1..1)) + end + + def twice + ReceiveMatcher.new(@expected, (2..2)) + end + + def humanize_range(range : Range) + if (min = range.begin) + if (max = range.end) + if min == max + min + else + "#{min} to #{max}" + end + else + "At least #{min}" + end + else + if (max = range.end) + "At most #{max}" + else + raise "Unexpected endless range" + end + end + end end end From 46aff9e11c8a29ff318f7e3ccf6e760239d9c82d Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 3 Nov 2019 14:00:40 -0700 Subject: [PATCH 105/205] Add utility methods for setting count --- src/spectator/matchers/receive_matcher.cr | 37 +++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/spectator/matchers/receive_matcher.cr b/src/spectator/matchers/receive_matcher.cr index 8d3a544..4698289 100644 --- a/src/spectator/matchers/receive_matcher.cr +++ b/src/spectator/matchers/receive_matcher.cr @@ -44,6 +44,34 @@ module Spectator::Matchers ReceiveMatcher.new(@expected, (2..2)) end + def exactly(count) + Count.new(@expected, (count..count)) + end + + def at_least(count) + Count.new(@expected, (count..)) + end + + def at_most(count) + Count.new(@expected, (..count)) + end + + def at_least_once + at_least(1).times + end + + def at_least_twice + at_least(2).times + end + + def at_most_once + at_most(1).times + end + + def at_most_twice + at_most(2).times + end + def humanize_range(range : Range) if (min = range.begin) if (max = range.end) @@ -63,5 +91,14 @@ module Spectator::Matchers end end end + + private struct Count + def initialize(@expected : TestExpression(Symbol), @range : Range) + end + + def times + ReceiveMatcher.new(@expected, @range) + end + end end end From 4acf6aaa587bc142727b3866b15c15d51fc7156d Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 3 Nov 2019 14:28:57 -0700 Subject: [PATCH 106/205] Add range to description and failure message --- src/spectator/matchers/receive_matcher.cr | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/spectator/matchers/receive_matcher.cr b/src/spectator/matchers/receive_matcher.cr index 4698289..bdca036 100644 --- a/src/spectator/matchers/receive_matcher.cr +++ b/src/spectator/matchers/receive_matcher.cr @@ -9,7 +9,8 @@ module Spectator::Matchers end def description : String - "received message #{@expected.label}" + range = @range + "received message #{@expected.label} #{range ? "#{humanize_range(range)} time(s)" : "at least once"} with any arguments" end def match?(actual : TestExpression(T)) : Bool forall T @@ -23,7 +24,8 @@ module Spectator::Matchers end def failure_message(actual : TestExpression(T)) : String forall T - "#{actual.label} did not receive #{@expected.label}" + range = @range + "#{actual.label} did not receive #{@expected.label} #{range ? "#{humanize_range(range)} time(s)" : "at least once"}" end def values(actual : TestExpression(T)) forall T From 82e01e44deabffaf0b10fcb634f0f61aafa39c6a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 3 Nov 2019 14:48:08 -0700 Subject: [PATCH 107/205] Copy-paste and tweak to implement argument expectations --- README.md | 4 ++-- src/spectator/matchers/receive_matcher.cr | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9c57285..1585016 100644 --- a/README.md +++ b/README.md @@ -314,8 +314,8 @@ Items not marked as completed may have partial implementations. - [X] Doubles (Stand-ins for real types) - `double NAME { }` - [X] Method stubs - `allow().to receive()`, `allow().to receive().and_return()` - [ ] Spies - `expect().to receive()` - - [ ] Message expectations - `expect().to receive().at_least()` - - [ ] Argument expectations - `expect().to receive().with()` + - [X] Message expectations - `expect().to receive().at_least()` + - [X] Argument expectations - `expect().to receive().with()` - [ ] Message ordering - `expect().to receive().ordered` - [ ] Null doubles - [ ] Runner diff --git a/src/spectator/matchers/receive_matcher.cr b/src/spectator/matchers/receive_matcher.cr index bdca036..ae294bb 100644 --- a/src/spectator/matchers/receive_matcher.cr +++ b/src/spectator/matchers/receive_matcher.cr @@ -38,6 +38,11 @@ module Spectator::Matchers } end + def with(*args, **opts) + args = Mocks::GenericArguments.new(args, opts) + ReceiveArgumentsMatcher.new(@expected, args, @range) + end + def once ReceiveMatcher.new(@expected, (1..1)) end From e3b9cef2215c1b74c968f16bea0ff88c3b418324 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 3 Nov 2019 14:49:10 -0700 Subject: [PATCH 108/205] Remove type restriction Not sure if this will blow up the type resolution, but seems to fix argument comparison from recorded calls. --- src/spectator/mocks/generic_arguments.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/mocks/generic_arguments.cr b/src/spectator/mocks/generic_arguments.cr index 0da404c..7ac7db5 100644 --- a/src/spectator/mocks/generic_arguments.cr +++ b/src/spectator/mocks/generic_arguments.cr @@ -16,7 +16,7 @@ module Spectator::Mocks dispatcher.call(*@args, **@opts) end - def ===(other : GenericArguments(U, NU)) : Bool forall U, NU + def ===(other) : Bool return false unless @args === other.args return false unless @opts.size === other.opts.size From db092747465a563dee2af2a31f33dee7644692ba Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 3 Nov 2019 14:50:51 -0700 Subject: [PATCH 109/205] Forgot to check-in argument matcher --- .../matchers/receive_arguments_matcher.cr | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 src/spectator/matchers/receive_arguments_matcher.cr diff --git a/src/spectator/matchers/receive_arguments_matcher.cr b/src/spectator/matchers/receive_arguments_matcher.cr new file mode 100644 index 0000000..483b836 --- /dev/null +++ b/src/spectator/matchers/receive_arguments_matcher.cr @@ -0,0 +1,106 @@ +require "../mocks" +require "./standard_matcher" + +module Spectator::Matchers + struct ReceiveArgumentsMatcher(T, NT) < StandardMatcher + alias Range = ::Range(Int32, Int32) | ::Range(Nil, Int32) | ::Range(Int32, Nil) + + def initialize(@expected : TestExpression(Symbol), @args : Mocks::GenericArguments(T, NT), @range : Range? = nil) + end + + def description : String + range = @range + "received message #{@expected.label} #{range ? "#{humanize_range(range)} time(s)" : "At least once"} with #{@args}" + end + + def match?(actual : TestExpression(T)) : Bool forall T + double = actual.value.as(Mocks::Double) + calls = double.spectator_stub_calls(@expected.value).select { |call| @args === call.args } + if (range = @range) + range.includes?(calls.size) + else + !calls.empty? + end + end + + def failure_message(actual : TestExpression(T)) : String forall T + range = @range + "#{actual.label} did not receive #{@expected.label} #{range ? "#{humanize_range(range)} time(s)" : "at least once"} with #{@args}" + end + + def values(actual : TestExpression(T)) forall T + double = actual.value.as(Mocks::Double) + calls = double.spectator_stub_calls(@expected.value) + range = @range + { + expected: "#{range ? "#{humanize_range(range)} time(s)" : "At least once"} with #{@args}", + received: "#{calls.size} time(s)", + } + end + + def once + ReceiveArgumentsMatcher.new(@expected, @args, (1..1)) + end + + def twice + ReceiveArgumentsMatcher.new(@expected, @args, (2..2)) + end + + def exactly(count) + Count.new(@expected, (count..count)) + end + + def at_least(count) + Count.new(@expected, (count..)) + end + + def at_most(count) + Count.new(@expected, (..count)) + end + + def at_least_once + at_least(1).times + end + + def at_least_twice + at_least(2).times + end + + def at_most_once + at_most(1).times + end + + def at_most_twice + at_most(2).times + end + + def humanize_range(range : Range) + if (min = range.begin) + if (max = range.end) + if min == max + min + else + "#{min} to #{max}" + end + else + "At least #{min}" + end + else + if (max = range.end) + "At most #{max}" + else + raise "Unexpected endless range" + end + end + end + + private struct Count + def initialize(@expected : TestExpression(Symbol), @args : Mocks::Arguments, @range : Range) + end + + def times + ReceiveArgumentsMatcher.new(@expected, @args, @range) + end + end + end +end From cf8e028bd96953496d5b72ec862b1dffa35601f5 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 3 Nov 2019 15:07:25 -0700 Subject: [PATCH 110/205] Some updates to mocks Not working correctly for some cases, specifically String. The default stub can seriously mess up Crystal internals. It looks like the stubs and spy information will need to be kept outside the instance. --- src/spectator/dsl/mocks.cr | 4 +- src/spectator/mocks/stubs.cr | 85 ++++++++++++------------------------ 2 files changed, 32 insertions(+), 57 deletions(-) diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 6e29045..452fd65 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -34,10 +34,12 @@ module Spectator::DSL {{type.id}} ::{{resolved.id}} include ::Spectator::Mocks::Stubs + @spectator_stubs = Deque(::Spectator::Mocks::MethodStub).new + @spectator_stub_calls = Deque(::Spectator::Mocks::MethodCall).new + {{block.body}} end {% end %} - {% debug %} end def allow(double : ::Spectator::Mocks::Double) diff --git a/src/spectator/mocks/stubs.cr b/src/spectator/mocks/stubs.cr index acb68ff..2ae8e4e 100644 --- a/src/spectator/mocks/stubs.cr +++ b/src/spectator/mocks/stubs.cr @@ -1,4 +1,4 @@ -module Spectator +module Spectator::Mocks module Stubs private macro stub(definition, &block) {% @@ -7,9 +7,15 @@ module Spectator args = nil body = nil if definition.is_a?(Call) # stub foo { :bar } + named = false name = definition.name.id params = definition.args - args = params.map { |p| p.is_a?(TypeDeclaration) ? p.var : p.id } + args = params.map do |p| + n = p.is_a?(TypeDeclaration) ? p.var : p.id + r = named ? "#{n}: #{n}".id : n + named = true if n.starts_with?('*') + r + end body = definition.block.is_a?(Nop) ? block : definition.block elsif definition.is_a?(TypeDeclaration) # stub foo : Symbol name = definition.var @@ -20,63 +26,30 @@ module Spectator raise "Unrecognized stub format" end %} - def {{name}}{% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} - %method - end - {% if name.ends_with?('=') && name.id != "[]=" %} - def {{name}}(arg) - call = ::Spectator::GenericMethodCall.new({{name.symbolize}}, {arg}, NamedTuple.new) - @spectator_stub_calls << call - stub = @spectator_stubs.find(&.callable?(call)) - if stub - stub.as(::Spectator::GenericMethodStub(typeof(%method(arg)))).call(call) - else - %method(arg) - end + def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} + %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) + %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) + @spectator_stub_calls << %call + if (%stub = @spectator_stubs.find(&.callable?(%call))) + %stub.call(%args, typeof(previous_def({{args.splat}}))) + else + previous_def({{args.splat}}) + end + end + + def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} + %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) + %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) + @spectator_stub_calls << %call + if (%stub = @spectator_stubs.find(&.callable?(%call))) + %stub.call(%args, typeof(previous_def({{args.splat}}) { |*%ya| yield *%ya })) + else + previous_def({{args.splat}}) do |*%yield_args| + yield *%yield_args + end end - {% else %} - def {{name}}(*args, **options){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} - call = ::Spectator::GenericMethodCall.new({{name.symbolize}}, args, options) - @spectator_stub_calls << call - stub = @spectator_stubs.find(&.callable?(call)) - if stub - stub.as(::Spectator::GenericMethodStub(typeof(%method(*args, **options)))).call(call) - else - %method(*args, **options) - end - end - - {% if name != "[]=" %} - def {{name}}(*args, **options){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} - call = ::Spectator::GenericMethodCall.new({{name.symbolize}}, args, options) - @spectator_stub_calls << call - stub = @spectator_stubs.find(&.callable?(call)) - if stub - stub.as(::Spectator::GenericMethodStub(typeof(%method(*args, **options) { |*yield_args| yield *yield_args }))).call(call) - else - %method(*args, **options) do |*yield_args| - yield *yield_args - end - end - end - {% end %} - {% end %} - - def %method({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} - {% if body && !body.is_a?(Nop) %} - {{body.body}} - {% else %} - raise "Stubbed method called without being allowed" - # This code shouldn't be reached, but makes the compiler happy to have a matching type. - {% if definition.is_a?(TypeDeclaration) %} - %x = uninitialized {{definition.type}} - {% else %} - nil - {% end %} - {% end %} end - {% debug %} end end end From 875333cffe79610ce2a2ec293796812cf2a4721f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 6 Nov 2019 21:02:24 -0700 Subject: [PATCH 111/205] Initial mock registry code --- src/spectator/mocks/registry.cr | 45 +++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/spectator/mocks/registry.cr diff --git a/src/spectator/mocks/registry.cr b/src/spectator/mocks/registry.cr new file mode 100644 index 0000000..82d8939 --- /dev/null +++ b/src/spectator/mocks/registry.cr @@ -0,0 +1,45 @@ +module Spectator::Mocks + module Registry + extend self + + alias Key = Tuple(UInt64, UInt64) + + private struct Entry + getter stubs = Deque(MethodStub).new + getter calls = Deque(MethodCall).new + end + + @entries = {} of Key => Entry + + def reset + @entries.clear + end + + def register(object) + key = unique_key(object) + @entries[key] = Entry.new + end + + def stub(object, stub : MethodStub) + key = unique_key(object) + @entries[key].stubs << stub + rescue KeyError + raise "Cannot stub unregistered mock" + end + + def record_call(object, call : MethodCall) + key = unique_key(object) + @entries[key].calls << call + rescue KeyError + raise "Cannot record call for unregistered mock" + end + + private def unique_key(reference : Reference) + {reference.class.hash, reference.object_id} + end + + private def unique_key(value : Value) + {value.class.hash, value.hash} + end + end +end From 8c2f8d973ba12eacb8c338a553a4c9310e459e62 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Nov 2019 09:03:07 -0700 Subject: [PATCH 112/205] Don't embed mock info in type This can cause problems. Need to use a registry approach. --- src/spectator/dsl/mocks.cr | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 452fd65..5b195bd 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -18,25 +18,18 @@ module Spectator::DSL end macro mock(name, &block) - {% if block.is_a?(Nop) %} - {{name}}.new.tap do |%inst| - %inst.spectator_test = self - end - {% else %} - {% resolved = name.resolve - type = if resolved < Reference - :class - elsif resolved < Value - :struct - else - :module - end %} + {% resolved = name.resolve + type = if resolved < Reference + :class + elsif resolved < Value + :struct + else + :module + end %} + {% begin %} {{type.id}} ::{{resolved.id}} include ::Spectator::Mocks::Stubs - @spectator_stubs = Deque(::Spectator::Mocks::MethodStub).new - @spectator_stub_calls = Deque(::Spectator::Mocks::MethodCall).new - {{block.body}} end {% end %} From 0698ed655dfece8b0be3a6058df27307079dd7b1 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Nov 2019 09:03:36 -0700 Subject: [PATCH 113/205] Switch to using Registry for mocks and fix various issues --- src/spectator/mocks/registry.cr | 23 +++++++++++++++-------- src/spectator/mocks/stubs.cr | 8 ++++---- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/spectator/mocks/registry.cr b/src/spectator/mocks/registry.cr index 82d8939..f3504f5 100644 --- a/src/spectator/mocks/registry.cr +++ b/src/spectator/mocks/registry.cr @@ -9,27 +9,34 @@ module Spectator::Mocks getter calls = Deque(MethodCall).new end - @entries = {} of Key => Entry + @@entries = {} of Key => Entry - def reset + def reset : Nil @entries.clear end - def register(object) + def register(object) : Nil key = unique_key(object) - @entries[key] = Entry.new + @@entries[key] = Entry.new end - def stub(object, stub : MethodStub) + def add_stub(object, stub : MethodStub) : Nil key = unique_key(object) - @entries[key].stubs << stub + @@entries[key].stubs << stub rescue KeyError raise "Cannot stub unregistered mock" end - def record_call(object, call : MethodCall) + def find_stub(object, call : GenericMethodCall(T, NT)) forall T, NT key = unique_key(object) - @entries[key].calls << call + @@entries[key].stubs.find(&.callable?(call)) + rescue KeyError + raise "Cannot stub unregistered mock" + end + + def record_call(object, call : MethodCall) : Nil + key = unique_key(object) + @@entries[key].calls << call rescue KeyError raise "Cannot record call for unregistered mock" end diff --git a/src/spectator/mocks/stubs.cr b/src/spectator/mocks/stubs.cr index 2ae8e4e..da00356 100644 --- a/src/spectator/mocks/stubs.cr +++ b/src/spectator/mocks/stubs.cr @@ -30,8 +30,8 @@ module Spectator::Mocks def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) - @spectator_stub_calls << %call - if (%stub = @spectator_stubs.find(&.callable?(%call))) + ::Spectator::Mocks::Registry.record_call(self, %call) + if (%stub = ::Spectator::Mocks::Registry.find_stub(self, %call)) %stub.call(%args, typeof(previous_def({{args.splat}}))) else previous_def({{args.splat}}) @@ -41,8 +41,8 @@ module Spectator::Mocks def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) - @spectator_stub_calls << %call - if (%stub = @spectator_stubs.find(&.callable?(%call))) + ::Spectator::Mocks::Registry.record_call(self, %call) + if (%stub = ::Spectator::Mocks::Registry.find_stub(self, %call)) %stub.call(%args, typeof(previous_def({{args.splat}}) { |*%ya| yield *%ya })) else previous_def({{args.splat}}) do |*%yield_args| From 8262bb031660cdcf14a9cb62937057b44d29418e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Nov 2019 09:05:17 -0700 Subject: [PATCH 114/205] Rename OpenMock to Allow --- src/spectator/dsl/mocks.cr | 2 +- src/spectator/mocks/{open_mock.cr => allow.cr} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/spectator/mocks/{open_mock.cr => allow.cr} (90%) diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 5b195bd..0be5893 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -36,7 +36,7 @@ module Spectator::DSL end def allow(double : ::Spectator::Mocks::Double) - Mocks::OpenMock.new(double) + Mocks::Allow.new(double) end macro receive(method_name, _source_file = __FILE__, _source_line = __LINE__) diff --git a/src/spectator/mocks/open_mock.cr b/src/spectator/mocks/allow.cr similarity index 90% rename from src/spectator/mocks/open_mock.cr rename to src/spectator/mocks/allow.cr index b4549f9..f6a70fc 100644 --- a/src/spectator/mocks/open_mock.cr +++ b/src/spectator/mocks/allow.cr @@ -1,5 +1,5 @@ module Spectator::Mocks - struct OpenMock + struct Allow def initialize(@mock : Double) end From aa33acd14ee6567d663d078a1ba28cff54eed4d4 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Nov 2019 09:07:54 -0700 Subject: [PATCH 115/205] Anything can be passed to allow() --- src/spectator/dsl/mocks.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 0be5893..4b4de61 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -35,8 +35,8 @@ module Spectator::DSL {% end %} end - def allow(double : ::Spectator::Mocks::Double) - Mocks::Allow.new(double) + def allow(thing : T) forall T + Mocks::Allow.new(thing) end macro receive(method_name, _source_file = __FILE__, _source_line = __LINE__) From fc0b46caca11afe98fea25b03ca01f475f54231f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Nov 2019 09:08:09 -0700 Subject: [PATCH 116/205] Use Registry for storing stubs --- src/spectator/mocks/allow.cr | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/spectator/mocks/allow.cr b/src/spectator/mocks/allow.cr index f6a70fc..00ecc99 100644 --- a/src/spectator/mocks/allow.cr +++ b/src/spectator/mocks/allow.cr @@ -1,10 +1,12 @@ +require "./registry" + module Spectator::Mocks - struct Allow - def initialize(@mock : Double) + struct Allow(T) + def initialize(@mock : T) end def to(stub : MethodStub) : Nil - @mock.spectator_define_stub(stub) + Registry.add_stub(@mock, stub) end end end From 5a072301af1626ff9d22d977b605c97c628bb116 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Nov 2019 09:15:41 -0700 Subject: [PATCH 117/205] Don't require registration --- src/spectator/mocks/registry.cr | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/spectator/mocks/registry.cr b/src/spectator/mocks/registry.cr index f3504f5..3672d30 100644 --- a/src/spectator/mocks/registry.cr +++ b/src/spectator/mocks/registry.cr @@ -15,30 +15,25 @@ module Spectator::Mocks @entries.clear end - def register(object) : Nil - key = unique_key(object) - @@entries[key] = Entry.new - end - def add_stub(object, stub : MethodStub) : Nil - key = unique_key(object) - @@entries[key].stubs << stub - rescue KeyError - raise "Cannot stub unregistered mock" + fetch(object).stubs << stub end def find_stub(object, call : GenericMethodCall(T, NT)) forall T, NT - key = unique_key(object) - @@entries[key].stubs.find(&.callable?(call)) - rescue KeyError - raise "Cannot stub unregistered mock" + fetch(object).stubs.find(&.callable?(call)) end def record_call(object, call : MethodCall) : Nil + fetch(object).calls << call + end + + private def fetch(object) key = unique_key(object) - @@entries[key].calls << call - rescue KeyError - raise "Cannot record call for unregistered mock" + if @@entries.has_key?(key) + @@entries[key] + else + @@entries[key] = Entry.new + end end private def unique_key(reference : Reference) From 9e8286f892fe19f9de4d4405f5015a89f497f887 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Nov 2019 09:18:34 -0700 Subject: [PATCH 118/205] Move double info to registry --- src/spectator/mocks/double.cr | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/spectator/mocks/double.cr b/src/spectator/mocks/double.cr index a0ca3f4..a91ad5e 100644 --- a/src/spectator/mocks/double.cr +++ b/src/spectator/mocks/double.cr @@ -3,9 +3,6 @@ require "./generic_method_stub" module Spectator::Mocks abstract class Double - @spectator_stubs = Deque(MethodStub).new - @spectator_stub_calls = Deque(MethodCall).new - def initialize(@spectator_double_name : Symbol) end @@ -39,8 +36,8 @@ module Spectator::Mocks def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) - @spectator_stub_calls << %call - if (%stub = @spectator_stubs.find(&.callable?(%call))) + ::Spectator::Mocks::Registry.record_call(self, %call) + if (%stub = ::Spectator::Mocks::Registry.find_stub(self, %call)) %stub.call(%args, typeof(%method({{args.splat}}))) else %method({{args.splat}}) @@ -50,8 +47,8 @@ module Spectator::Mocks def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) - @spectator_stub_calls << %call - if (%stub = @spectator_stubs.find(&.callable?(%call))) + ::Spectator::Mocks::Registry.record_call(self, %call) + if (%stub = ::Spectator::Mocks::Registry.find_stub(self, %call)) %stub.call(%args, typeof(%method({{args.splat}}) { |*%ya| yield *%ya })) else %method({{args.splat}}) do |*%yield_args| @@ -80,13 +77,5 @@ module Spectator::Mocks io << @spectator_double_name io << ')' end - - protected def spectator_define_stub(stub : MethodStub) : Nil - @spectator_stubs << stub - end - - protected def spectator_stub_calls(method : Symbol) : Array(MethodCall) - @spectator_stub_calls.select { |call| call.name == method } - end end end From 48363951c27faabf764486166884b5cee819dd50 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Nov 2019 09:23:01 -0700 Subject: [PATCH 119/205] Update receive matchers to use registry --- src/spectator/matchers/receive_arguments_matcher.cr | 6 ++---- src/spectator/matchers/receive_matcher.cr | 6 ++---- src/spectator/mocks/registry.cr | 4 ++++ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/spectator/matchers/receive_arguments_matcher.cr b/src/spectator/matchers/receive_arguments_matcher.cr index 483b836..0110d53 100644 --- a/src/spectator/matchers/receive_arguments_matcher.cr +++ b/src/spectator/matchers/receive_arguments_matcher.cr @@ -14,8 +14,7 @@ module Spectator::Matchers end def match?(actual : TestExpression(T)) : Bool forall T - double = actual.value.as(Mocks::Double) - calls = double.spectator_stub_calls(@expected.value).select { |call| @args === call.args } + calls = Mocks::Registry.calls_for(actual.value, @expected.value).select { |call| @args === call.args } if (range = @range) range.includes?(calls.size) else @@ -29,8 +28,7 @@ module Spectator::Matchers end def values(actual : TestExpression(T)) forall T - double = actual.value.as(Mocks::Double) - calls = double.spectator_stub_calls(@expected.value) + calls = Mocks::Registry.calls_for(actual.value, @expected.value).select { |call| @args === call.args } range = @range { expected: "#{range ? "#{humanize_range(range)} time(s)" : "At least once"} with #{@args}", diff --git a/src/spectator/matchers/receive_matcher.cr b/src/spectator/matchers/receive_matcher.cr index ae294bb..489f453 100644 --- a/src/spectator/matchers/receive_matcher.cr +++ b/src/spectator/matchers/receive_matcher.cr @@ -14,8 +14,7 @@ module Spectator::Matchers end def match?(actual : TestExpression(T)) : Bool forall T - double = actual.value.as(Mocks::Double) - calls = double.spectator_stub_calls(@expected.value) + calls = Mocks::Registry.calls_for(actual.value, @expected.value) if (range = @range) range.includes?(calls.size) else @@ -29,8 +28,7 @@ module Spectator::Matchers end def values(actual : TestExpression(T)) forall T - double = actual.value.as(Mocks::Double) - calls = double.spectator_stub_calls(@expected.value) + calls = Mocks::Registry.calls_for(actual.value, @expected.value) range = @range { expected: "#{range ? "#{humanize_range(range)} time(s)" : "At least once"} with any arguments", diff --git a/src/spectator/mocks/registry.cr b/src/spectator/mocks/registry.cr index 3672d30..a662cb5 100644 --- a/src/spectator/mocks/registry.cr +++ b/src/spectator/mocks/registry.cr @@ -27,6 +27,10 @@ module Spectator::Mocks fetch(object).calls << call end + def calls_for(object, method_name : Symbol) + fetch(object).calls.select { |call| call.name == method_name } + end + private def fetch(object) key = unique_key(object) if @@entries.has_key?(key) From 66dc6bf09821716a00d934a4f739f860efd750d4 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Nov 2019 09:38:11 -0700 Subject: [PATCH 120/205] Uncomment predicate matcher --- src/spectator/dsl/matchers.cr | 56 +++++++++++++++++------------------ 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/spectator/dsl/matchers.cr b/src/spectator/dsl/matchers.cr index 572aea6..0e779c3 100644 --- a/src/spectator/dsl/matchers.cr +++ b/src/spectator/dsl/matchers.cr @@ -703,33 +703,33 @@ module Spectator # # Is equivalent to: # expect("foobar".has_back_references?).to_not be_true # ``` - # macro method_missing(call) - # {% if call.name.starts_with?("be_") %} - # # Remove `be_` prefix. - # {% method_name = call.name[3..-1] %} - # {% matcher = "PredicateMatcher" %} - # {% elsif call.name.starts_with?("have_") %} - # # Remove `have_` prefix. - # {% method_name = call.name[5..-1] %} - # {% matcher = "HavePredicateMatcher" %} - # {% else %} - # {% raise "Undefined local variable or method '#{call}'" %} - # {% end %} - # - # descriptor = { {{method_name}}: Tuple.new({{call.args.splat}}) } - # label = String::Builder.new({{method_name.stringify}}) - # {% unless call.args.empty? %} - # label << '(' - # {% for arg, index in call.args %} - # label << {{arg}} - # {% if index < call.args.size - 1 %} - # label << ", " - # {% end %} - # {% end %} - # label << ')' - # {% end %} - # test_value = ::Spectator::TestValue.new(descriptor, label.to_s) - # ::Spectator::Matchers::{{matcher.id}}.new(test_value) - # end + macro method_missing(call) + {% if call.name.starts_with?("be_") %} + # Remove `be_` prefix. + {% method_name = call.name[3..-1] %} + {% matcher = "PredicateMatcher" %} + {% elsif call.name.starts_with?("have_") %} + # Remove `have_` prefix. + {% method_name = call.name[5..-1] %} + {% matcher = "HavePredicateMatcher" %} + {% else %} + {% raise "Undefined local variable or method '#{call}'" %} + {% end %} + + descriptor = { {{method_name}}: Tuple.new({{call.args.splat}}) } + label = String::Builder.new({{method_name.stringify}}) + {% unless call.args.empty? %} + label << '(' + {% for arg, index in call.args %} + label << {{arg}} + {% if index < call.args.size - 1 %} + label << ", " + {% end %} + {% end %} + label << ')' + {% end %} + test_value = ::Spectator::TestValue.new(descriptor, label.to_s) + ::Spectator::Matchers::{{matcher.id}}.new(test_value) + end end end From c03808dfce9d5660dfec93fea570459ad6b9d81c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Nov 2019 09:45:57 -0700 Subject: [PATCH 121/205] Handle super vs previous_def --- src/spectator/mocks/stubs.cr | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/spectator/mocks/stubs.cr b/src/spectator/mocks/stubs.cr index da00356..919371a 100644 --- a/src/spectator/mocks/stubs.cr +++ b/src/spectator/mocks/stubs.cr @@ -25,6 +25,12 @@ module Spectator::Mocks else raise "Unrecognized stub format" end + + original = if @type.methods.find { |m| m.name.id == name } + :previous_def + else + :super + end.id %} def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} @@ -32,9 +38,9 @@ module Spectator::Mocks %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) ::Spectator::Mocks::Registry.record_call(self, %call) if (%stub = ::Spectator::Mocks::Registry.find_stub(self, %call)) - %stub.call(%args, typeof(previous_def({{args.splat}}))) + %stub.call(%args, typeof({{original}}({{args.splat}}))) else - previous_def({{args.splat}}) + {{original}}({{args.splat}}) end end @@ -43,9 +49,9 @@ module Spectator::Mocks %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) ::Spectator::Mocks::Registry.record_call(self, %call) if (%stub = ::Spectator::Mocks::Registry.find_stub(self, %call)) - %stub.call(%args, typeof(previous_def({{args.splat}}) { |*%ya| yield *%ya })) + %stub.call(%args, typeof({{original}}({{args.splat}}) { |*%ya| yield *%ya })) else - previous_def({{args.splat}}) do |*%yield_args| + {{original}}({{args.splat}}) do |*%yield_args| yield *%yield_args end end From 74eb4fc11a1abdf7f7115ca4e76e2d5488afd340 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Nov 2019 10:39:02 -0700 Subject: [PATCH 122/205] Reset stubs after each test --- src/spectator/mocks/registry.cr | 2 +- src/spectator/runnable_example.cr | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/spectator/mocks/registry.cr b/src/spectator/mocks/registry.cr index a662cb5..b6ca7f8 100644 --- a/src/spectator/mocks/registry.cr +++ b/src/spectator/mocks/registry.cr @@ -12,7 +12,7 @@ module Spectator::Mocks @@entries = {} of Key => Entry def reset : Nil - @entries.clear + @@entries.clear end def add_stub(object, stub : MethodStub) : Nil diff --git a/src/spectator/runnable_example.cr b/src/spectator/runnable_example.cr index d981689..507279a 100644 --- a/src/spectator/runnable_example.cr +++ b/src/spectator/runnable_example.cr @@ -9,6 +9,7 @@ module Spectator def run_impl : Result result = capture_result expectations = Harness.current.expectations + Mocks::Registry.reset translate_result(result, expectations) end From e9f7e65ac956a7600d8f22989e6c1faf6200feb8 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Nov 2019 10:49:29 -0700 Subject: [PATCH 123/205] Add mock registry prep --- src/spectator/mocks.cr | 6 ++++++ src/spectator/mocks/registry.cr | 4 ++++ src/spectator/runnable_example.cr | 9 +++++---- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/spectator/mocks.cr b/src/spectator/mocks.cr index f50bf9e..d36eeff 100644 --- a/src/spectator/mocks.cr +++ b/src/spectator/mocks.cr @@ -3,5 +3,11 @@ require "./mocks/*" module Spectator # Functionality for mocking existing types. module Mocks + def self.run(context : TestContext) + Registry.prepare(context) + yield + ensure + Registry.reset + end end end diff --git a/src/spectator/mocks/registry.cr b/src/spectator/mocks/registry.cr index b6ca7f8..a655eee 100644 --- a/src/spectator/mocks/registry.cr +++ b/src/spectator/mocks/registry.cr @@ -11,6 +11,10 @@ module Spectator::Mocks @@entries = {} of Key => Entry + def prepare(context : TestContext) : Nil + # TODO + end + def reset : Nil @@entries.clear end diff --git a/src/spectator/runnable_example.cr b/src/spectator/runnable_example.cr index 507279a..c5c0b5b 100644 --- a/src/spectator/runnable_example.cr +++ b/src/spectator/runnable_example.cr @@ -7,10 +7,11 @@ module Spectator # 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 - Mocks::Registry.reset - translate_result(result, expectations) + Mocks.run(group.context) do + result = capture_result + expectations = Harness.current.expectations + translate_result(result, expectations) + end end # Runs all hooks and the example code. From 17695d35cf012c0b50f4bcfd3ac4a0eb4f479aee Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Nov 2019 21:30:59 -0700 Subject: [PATCH 124/205] Fix cast to nil when nil is expected Resolves `TypeCastError: The return type of stub #... : Nil at ...:## doesn't match the expected type Nil` --- src/spectator/mocks/double.cr | 4 ++-- src/spectator/mocks/method_stub.cr | 9 +++++++++ src/spectator/mocks/nil_method_stub.cr | 8 ++------ src/spectator/mocks/proc_method_stub.cr | 7 +------ src/spectator/mocks/stubs.cr | 4 ++-- src/spectator/mocks/value_method_stub.cr | 7 +------ 6 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/spectator/mocks/double.cr b/src/spectator/mocks/double.cr index a91ad5e..bceaeff 100644 --- a/src/spectator/mocks/double.cr +++ b/src/spectator/mocks/double.cr @@ -38,7 +38,7 @@ module Spectator::Mocks %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) ::Spectator::Mocks::Registry.record_call(self, %call) if (%stub = ::Spectator::Mocks::Registry.find_stub(self, %call)) - %stub.call(%args, typeof(%method({{args.splat}}))) + %stub.call!(%args, typeof(%method({{args.splat}}))) else %method({{args.splat}}) end @@ -49,7 +49,7 @@ module Spectator::Mocks %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) ::Spectator::Mocks::Registry.record_call(self, %call) if (%stub = ::Spectator::Mocks::Registry.find_stub(self, %call)) - %stub.call(%args, typeof(%method({{args.splat}}) { |*%ya| yield *%ya })) + %stub.call!(%args, typeof(%method({{args.splat}}) { |*%ya| yield *%ya })) else %method({{args.splat}}) do |*%yield_args| yield *%yield_args diff --git a/src/spectator/mocks/method_stub.cr b/src/spectator/mocks/method_stub.cr index 01020ac..49794fd 100644 --- a/src/spectator/mocks/method_stub.cr +++ b/src/spectator/mocks/method_stub.cr @@ -12,6 +12,15 @@ module Spectator::Mocks abstract def call(args : GenericArguments(T, NT), rt : RT.class) forall T, NT, RT + def call!(args : GenericArguments(T, NT), rt : RT.class) : RT forall T, NT, RT + value = call(args, rt) + if value.is_a?(RT) + value.as(RT) + else + raise TypeCastError.new("The return type of stub #{self} doesn't match the expected type #{RT}") + end + end + def to_s(io) io << '#' io << @name diff --git a/src/spectator/mocks/nil_method_stub.cr b/src/spectator/mocks/nil_method_stub.cr index b0d087e..64e10bb 100644 --- a/src/spectator/mocks/nil_method_stub.cr +++ b/src/spectator/mocks/nil_method_stub.cr @@ -4,12 +4,8 @@ require "./value_method_stub" module Spectator::Mocks class NilMethodStub < GenericMethodStub(Nil) - def call(_args : GenericArguments(T, NT), rt : RT.class) forall T, NT, RT - if (cast = nil.as?(RT)) - cast - else - raise "The return type of stub #{self} doesn't match the expected type #{RT}" - end + def call(_args : GenericArguments(T, NT), _rt : RT.class) forall T, NT, RT + nil end def and_return(value : T) forall T diff --git a/src/spectator/mocks/proc_method_stub.cr b/src/spectator/mocks/proc_method_stub.cr index 98f9ca3..a2fa2dd 100644 --- a/src/spectator/mocks/proc_method_stub.cr +++ b/src/spectator/mocks/proc_method_stub.cr @@ -8,12 +8,7 @@ module Spectator::Mocks end def call(_args : GenericArguments(T, NT), rt : RT.class) forall T, NT, RT - result = @proc.call - if (cast = result.as?(RT)) - cast - else - raise "The return type of stub #{self} doesn't match the expected type #{RT}" - end + @proc.call end end end diff --git a/src/spectator/mocks/stubs.cr b/src/spectator/mocks/stubs.cr index 919371a..f4f633f 100644 --- a/src/spectator/mocks/stubs.cr +++ b/src/spectator/mocks/stubs.cr @@ -38,7 +38,7 @@ module Spectator::Mocks %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) ::Spectator::Mocks::Registry.record_call(self, %call) if (%stub = ::Spectator::Mocks::Registry.find_stub(self, %call)) - %stub.call(%args, typeof({{original}}({{args.splat}}))) + %stub.call!(%args, typeof({{original}}({{args.splat}}))) else {{original}}({{args.splat}}) end @@ -49,7 +49,7 @@ module Spectator::Mocks %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) ::Spectator::Mocks::Registry.record_call(self, %call) if (%stub = ::Spectator::Mocks::Registry.find_stub(self, %call)) - %stub.call(%args, typeof({{original}}({{args.splat}}) { |*%ya| yield *%ya })) + %stub.call!(%args, typeof({{original}}({{args.splat}}) { |*%ya| yield *%ya })) else {{original}}({{args.splat}}) do |*%yield_args| yield *%yield_args diff --git a/src/spectator/mocks/value_method_stub.cr b/src/spectator/mocks/value_method_stub.cr index aef48d9..5141441 100644 --- a/src/spectator/mocks/value_method_stub.cr +++ b/src/spectator/mocks/value_method_stub.cr @@ -8,12 +8,7 @@ module Spectator::Mocks end def call(_args : GenericArguments(T, NT), rt : RT.class) forall T, NT, RT - result = @value - if (cast = result.as?(RT)) - cast - else - raise "The return type of stub #{self} doesn't match the expected type #{RT}" - end + @value end end end From 0471794814e8cbf223b89e0d66e8c4f546b5039e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Nov 2019 22:13:02 -0700 Subject: [PATCH 125/205] Store stubs in reverse order --- src/spectator/mocks/registry.cr | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/spectator/mocks/registry.cr b/src/spectator/mocks/registry.cr index a655eee..7e165ae 100644 --- a/src/spectator/mocks/registry.cr +++ b/src/spectator/mocks/registry.cr @@ -20,7 +20,9 @@ module Spectator::Mocks end def add_stub(object, stub : MethodStub) : Nil - fetch(object).stubs << stub + # Stubs are added in reverse order, + # so that later-defined stubs override previously defined ones. + fetch(object).stubs.unshift(stub) end def find_stub(object, call : GenericMethodCall(T, NT)) forall T, NT From e0d12e9d0d5b7d5463638010028e1e4e381ba19a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Nov 2019 22:32:49 -0700 Subject: [PATCH 126/205] Use class name instead of hash of type Hash of type could collide, though unlikely. --- src/spectator/mocks/registry.cr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/spectator/mocks/registry.cr b/src/spectator/mocks/registry.cr index 7e165ae..dbfaf49 100644 --- a/src/spectator/mocks/registry.cr +++ b/src/spectator/mocks/registry.cr @@ -2,7 +2,7 @@ module Spectator::Mocks module Registry extend self - alias Key = Tuple(UInt64, UInt64) + alias Key = Tuple(String, UInt64) private struct Entry getter stubs = Deque(MethodStub).new @@ -47,11 +47,11 @@ module Spectator::Mocks end private def unique_key(reference : Reference) - {reference.class.hash, reference.object_id} + {reference.class.name, reference.object_id} end private def unique_key(value : Value) - {value.class.hash, value.hash} + {value.class.name, value.hash} end end end From e4aae1f60aeef0a8c6b1d317cf35a77e34d10ce4 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Nov 2019 23:05:22 -0700 Subject: [PATCH 127/205] Change Registry to a class --- src/spectator/mocks/registry.cr | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/spectator/mocks/registry.cr b/src/spectator/mocks/registry.cr index dbfaf49..b0b1f75 100644 --- a/src/spectator/mocks/registry.cr +++ b/src/spectator/mocks/registry.cr @@ -1,7 +1,5 @@ module Spectator::Mocks - module Registry - extend self - + class Registry alias Key = Tuple(String, UInt64) private struct Entry @@ -9,14 +7,14 @@ module Spectator::Mocks getter calls = Deque(MethodCall).new end - @@entries = {} of Key => Entry + @entries = {} of Key => Entry def prepare(context : TestContext) : Nil # TODO end def reset : Nil - @@entries.clear + @entries.clear end def add_stub(object, stub : MethodStub) : Nil @@ -39,10 +37,10 @@ module Spectator::Mocks private def fetch(object) key = unique_key(object) - if @@entries.has_key?(key) - @@entries[key] + if @entries.has_key?(key) + @entries[key] else - @@entries[key] = Entry.new + @entries[key] = Entry.new end end From 11ea7bf2ceb92e73eeaf86f30fc93da1216cfb24 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 9 Nov 2019 23:22:21 -0700 Subject: [PATCH 128/205] Move mock containment to harness --- src/spectator/harness.cr | 11 ++++++----- src/spectator/matchers/receive_arguments_matcher.cr | 4 ++-- src/spectator/matchers/receive_matcher.cr | 4 ++-- src/spectator/mocks/allow.cr | 2 +- src/spectator/mocks/double.cr | 8 ++++---- src/spectator/mocks/stubs.cr | 8 ++++---- src/spectator/runnable_example.cr | 8 +++----- 7 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index ebbf5db..88873e3 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -1,3 +1,5 @@ +require "./mocks/registry" + 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`. @@ -25,7 +27,8 @@ module Spectator # 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) + @@current = harness = new(example) + harness.mocks.prepare(example.group.context) example.run ensure @@current = nil @@ -34,6 +37,8 @@ module Spectator # Retrieves the current running example. getter example : Example + getter mocks = Mocks::Registry.new + # Retrieves the group for the current running example. def group example.group @@ -51,10 +56,6 @@ module Spectator @reporter.expectations end - def double(id) - example.group.double(id, example.sample_values) - end - # Creates a new harness. # The example the harness is for should be passed in. private def initialize(@example) diff --git a/src/spectator/matchers/receive_arguments_matcher.cr b/src/spectator/matchers/receive_arguments_matcher.cr index 0110d53..bc729db 100644 --- a/src/spectator/matchers/receive_arguments_matcher.cr +++ b/src/spectator/matchers/receive_arguments_matcher.cr @@ -14,7 +14,7 @@ module Spectator::Matchers end def match?(actual : TestExpression(T)) : Bool forall T - calls = Mocks::Registry.calls_for(actual.value, @expected.value).select { |call| @args === call.args } + calls = Harness.current.mocks.calls_for(actual.value, @expected.value).select { |call| @args === call.args } if (range = @range) range.includes?(calls.size) else @@ -28,7 +28,7 @@ module Spectator::Matchers end def values(actual : TestExpression(T)) forall T - calls = Mocks::Registry.calls_for(actual.value, @expected.value).select { |call| @args === call.args } + calls = Harness.current.mocks.calls_for(actual.value, @expected.value).select { |call| @args === call.args } range = @range { expected: "#{range ? "#{humanize_range(range)} time(s)" : "At least once"} with #{@args}", diff --git a/src/spectator/matchers/receive_matcher.cr b/src/spectator/matchers/receive_matcher.cr index 489f453..376047a 100644 --- a/src/spectator/matchers/receive_matcher.cr +++ b/src/spectator/matchers/receive_matcher.cr @@ -14,7 +14,7 @@ module Spectator::Matchers end def match?(actual : TestExpression(T)) : Bool forall T - calls = Mocks::Registry.calls_for(actual.value, @expected.value) + calls = Harness.current.mocks.calls_for(actual.value, @expected.value) if (range = @range) range.includes?(calls.size) else @@ -28,7 +28,7 @@ module Spectator::Matchers end def values(actual : TestExpression(T)) forall T - calls = Mocks::Registry.calls_for(actual.value, @expected.value) + calls = Harness.current.mocks.calls_for(actual.value, @expected.value) range = @range { expected: "#{range ? "#{humanize_range(range)} time(s)" : "At least once"} with any arguments", diff --git a/src/spectator/mocks/allow.cr b/src/spectator/mocks/allow.cr index 00ecc99..1479e69 100644 --- a/src/spectator/mocks/allow.cr +++ b/src/spectator/mocks/allow.cr @@ -6,7 +6,7 @@ module Spectator::Mocks end def to(stub : MethodStub) : Nil - Registry.add_stub(@mock, stub) + Harness.current.mocks.add_stub(@mock, stub) end end end diff --git a/src/spectator/mocks/double.cr b/src/spectator/mocks/double.cr index bceaeff..e714042 100644 --- a/src/spectator/mocks/double.cr +++ b/src/spectator/mocks/double.cr @@ -36,8 +36,8 @@ module Spectator::Mocks def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) - ::Spectator::Mocks::Registry.record_call(self, %call) - if (%stub = ::Spectator::Mocks::Registry.find_stub(self, %call)) + ::Spectator::Harness.current.mocks.record_call(self, %call) + if (%stub = ::Spectator::Harness.current.mocks.find_stub(self, %call)) %stub.call!(%args, typeof(%method({{args.splat}}))) else %method({{args.splat}}) @@ -47,8 +47,8 @@ module Spectator::Mocks def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) - ::Spectator::Mocks::Registry.record_call(self, %call) - if (%stub = ::Spectator::Mocks::Registry.find_stub(self, %call)) + ::Spectator::Harness.current.mocks.record_call(self, %call) + if (%stub = ::Spectator::Harness.current.mocks.find_stub(self, %call)) %stub.call!(%args, typeof(%method({{args.splat}}) { |*%ya| yield *%ya })) else %method({{args.splat}}) do |*%yield_args| diff --git a/src/spectator/mocks/stubs.cr b/src/spectator/mocks/stubs.cr index f4f633f..ed7e1b1 100644 --- a/src/spectator/mocks/stubs.cr +++ b/src/spectator/mocks/stubs.cr @@ -36,8 +36,8 @@ module Spectator::Mocks def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) - ::Spectator::Mocks::Registry.record_call(self, %call) - if (%stub = ::Spectator::Mocks::Registry.find_stub(self, %call)) + ::Spectator::Harness.current.mocks.record_call(self, %call) + if (%stub = ::Spectator::Harness.current.mocks.find_stub(self, %call)) %stub.call!(%args, typeof({{original}}({{args.splat}}))) else {{original}}({{args.splat}}) @@ -47,8 +47,8 @@ module Spectator::Mocks def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) - ::Spectator::Mocks::Registry.record_call(self, %call) - if (%stub = ::Spectator::Mocks::Registry.find_stub(self, %call)) + ::Spectator::Harness.current.mocks.record_call(self, %call) + if (%stub = ::Spectator::Harness.current.mocks.find_stub(self, %call)) %stub.call!(%args, typeof({{original}}({{args.splat}}) { |*%ya| yield *%ya })) else {{original}}({{args.splat}}) do |*%yield_args| diff --git a/src/spectator/runnable_example.cr b/src/spectator/runnable_example.cr index c5c0b5b..d981689 100644 --- a/src/spectator/runnable_example.cr +++ b/src/spectator/runnable_example.cr @@ -7,11 +7,9 @@ module Spectator # Runs the example, hooks, and captures the result # and translates to a usable result. def run_impl : Result - Mocks.run(group.context) do - result = capture_result - expectations = Harness.current.expectations - translate_result(result, expectations) - end + result = capture_result + expectations = Harness.current.expectations + translate_result(result, expectations) end # Runs all hooks and the example code. From f816a6477018290fe98bce18f7deb0d4158be561 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 10 Nov 2019 07:27:59 -0700 Subject: [PATCH 129/205] Resolve issue with harness trying to be used outside of test --- src/spectator/mocks/stubs.cr | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/spectator/mocks/stubs.cr b/src/spectator/mocks/stubs.cr index ed7e1b1..e2310cd 100644 --- a/src/spectator/mocks/stubs.cr +++ b/src/spectator/mocks/stubs.cr @@ -34,27 +34,29 @@ module Spectator::Mocks %} def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} - %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) - %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) - ::Spectator::Harness.current.mocks.record_call(self, %call) - if (%stub = ::Spectator::Harness.current.mocks.find_stub(self, %call)) - %stub.call!(%args, typeof({{original}}({{args.splat}}))) - else - {{original}}({{args.splat}}) + if (%harness = ::Spectator::Harness.current?) + %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) + %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) + %harness.mocks.record_call(self, %call) + if (%stub = ::Spectator::Harness.current.mocks.find_stub(self, %call)) + return %stub.call!(%args, typeof({{original}}({{args.splat}}))) + end end + {{original}}({{args.splat}}) end def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} - %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) - %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) - ::Spectator::Harness.current.mocks.record_call(self, %call) - if (%stub = ::Spectator::Harness.current.mocks.find_stub(self, %call)) - %stub.call!(%args, typeof({{original}}({{args.splat}}) { |*%ya| yield *%ya })) - else - {{original}}({{args.splat}}) do |*%yield_args| - yield *%yield_args + if (%harness = ::Spectator::Harness.current?) + %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) + %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) + %harness.mocks.record_call(self, %call) + if (%stub = ::Spectator::Harness.current.mocks.find_stub(self, %call)) + return %stub.call!(%args, typeof({{original}}({{args.splat}}) { |*%ya| yield *%ya })) end end + {{original}}({{args.splat}}) do |*%yield_args| + yield *%yield_args + end end end end From a2b72eaa36395b3f0e59653fa9a184e2fe18c1c2 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 10 Nov 2019 09:46:23 -0700 Subject: [PATCH 130/205] Implement allow_any_instance_of --- src/spectator/dsl/mocks.cr | 4 +++ src/spectator/mocks/allow_any_instance.cr | 9 +++++++ src/spectator/mocks/registry.cr | 30 +++++++++++++++++++---- 3 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 src/spectator/mocks/allow_any_instance.cr diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 4b4de61..7321ac7 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -39,6 +39,10 @@ module Spectator::DSL Mocks::Allow.new(thing) end + def allow_any_instance_of(type : T.class) forall T + Mocks::AllowAnyInstance(T).new + end + macro receive(method_name, _source_file = __FILE__, _source_line = __LINE__) %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) ::Spectator::Mocks::NilMethodStub.new({{method_name.symbolize}}, %source) diff --git a/src/spectator/mocks/allow_any_instance.cr b/src/spectator/mocks/allow_any_instance.cr new file mode 100644 index 0000000..6cc892a --- /dev/null +++ b/src/spectator/mocks/allow_any_instance.cr @@ -0,0 +1,9 @@ +require "./registry" + +module Spectator::Mocks + struct AllowAnyInstance(T) + def to(stub : MethodStub) : Nil + Harness.current.mocks.add_type_stub(T, stub) + end + end +end diff --git a/src/spectator/mocks/registry.cr b/src/spectator/mocks/registry.cr index b0b1f75..d19119f 100644 --- a/src/spectator/mocks/registry.cr +++ b/src/spectator/mocks/registry.cr @@ -7,6 +7,7 @@ module Spectator::Mocks getter calls = Deque(MethodCall).new end + @all_instances = {} of String => Entry @entries = {} of Key => Entry def prepare(context : TestContext) : Nil @@ -20,22 +21,32 @@ module Spectator::Mocks def add_stub(object, stub : MethodStub) : Nil # Stubs are added in reverse order, # so that later-defined stubs override previously defined ones. - fetch(object).stubs.unshift(stub) + fetch_instance(object).stubs.unshift(stub) + end + + def add_type_stub(type, stub : MethodStub) : Nil + fetch_type(type).stubs.unshift(stub) end def find_stub(object, call : GenericMethodCall(T, NT)) forall T, NT - fetch(object).stubs.find(&.callable?(call)) + fetch_instance(object).stubs.find(&.callable?(call)) || + fetch_type(object.class).stubs.find(&.callable?(call)) end def record_call(object, call : MethodCall) : Nil - fetch(object).calls << call + fetch_instance(object).calls << call + fetch_type(object.class).calls << call end def calls_for(object, method_name : Symbol) - fetch(object).calls.select { |call| call.name == method_name } + fetch_instance(object).calls.select { |call| call.name == method_name } end - private def fetch(object) + def calls_for_type(type, method_name : Symbol) + fetch_type(type).calls.select { |call| call.name == method_name } + end + + private def fetch_instance(object) key = unique_key(object) if @entries.has_key?(key) @entries[key] @@ -44,6 +55,15 @@ module Spectator::Mocks end end + private def fetch_type(type) + key = type.name + if @all_instances.has_key?(key) + @all_instances[key] + else + @all_instances[key] = Entry.new + end + end + private def unique_key(reference : Reference) {reference.class.name, reference.object_id} end From 14876a8e9a707570868e27d14192f3ab894cdd1b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 10 Nov 2019 12:35:03 -0700 Subject: [PATCH 131/205] Add default stubs to TestContext Pass default stubs into mocks registry on example startup. --- src/spectator/harness.cr | 6 +++--- src/spectator/mocks/registry.cr | 8 ++++++-- src/spectator/spec_builder.cr | 5 ++--- src/spectator/spec_builder/example_group_builder.cr | 7 +++++++ .../spec_builder/nested_example_group_builder.cr | 2 +- src/spectator/spec_builder/root_example_group_builder.cr | 2 +- .../spec_builder/sample_example_group_builder.cr | 4 ++-- src/spectator/test_context.cr | 8 +++++++- 8 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 88873e3..70ad610 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -27,8 +27,7 @@ module Spectator # 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 = harness = new(example) - harness.mocks.prepare(example.group.context) + @@current = new(example) example.run ensure @@current = nil @@ -37,7 +36,7 @@ module Spectator # Retrieves the current running example. getter example : Example - getter mocks = Mocks::Registry.new + getter mocks : Mocks::Registry # Retrieves the group for the current running example. def group @@ -60,6 +59,7 @@ module Spectator # 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.stubs) end end end diff --git a/src/spectator/mocks/registry.cr b/src/spectator/mocks/registry.cr index d19119f..31c956b 100644 --- a/src/spectator/mocks/registry.cr +++ b/src/spectator/mocks/registry.cr @@ -10,8 +10,12 @@ module Spectator::Mocks @all_instances = {} of String => Entry @entries = {} of Key => Entry - def prepare(context : TestContext) : Nil - # TODO + def initialize(default_stubs) + @all_instances = default_stubs.map do |k, v| + entry = Entry.new + entry.stubs.concat(v) + {k, entry} + end.to_h end def reset : Nil diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 07ddf8c..9e199c2 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -96,9 +96,8 @@ module Spectator @@stack.current.add_post_condition(block) end - def add_double(id : Symbol, double_type : Double.class) : Nil - double_factory = DoubleFactory.new(double_type) - current_group.add_double(id, double_factory) + def add_default_stub(*args) : Nil + @@stack.current.add_default_stub(*args) end # Builds the entire spec and returns it as a test suite. diff --git a/src/spectator/spec_builder/example_group_builder.cr b/src/spectator/spec_builder/example_group_builder.cr index f9ed5bf..90034b4 100644 --- a/src/spectator/spec_builder/example_group_builder.cr +++ b/src/spectator/spec_builder/example_group_builder.cr @@ -14,6 +14,7 @@ module Spectator::SpecBuilder @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 @@ -47,6 +48,12 @@ module Spectator::SpecBuilder @post_conditions << hook end + def add_default_stub(type : T.class, stub : Mocks::MethodStub) forall T + key = type.name + @default_stubs[key] = Dequeue.new unless @default_stubs.has_key?(key) + @default_stubs[key] << stub + end + private def build_hooks ExampleHooks.new( @before_all_hooks.to_a, diff --git a/src/spectator/spec_builder/nested_example_group_builder.cr b/src/spectator/spec_builder/nested_example_group_builder.cr index dc08cff..6d7bb77 100644 --- a/src/spectator/spec_builder/nested_example_group_builder.cr +++ b/src/spectator/spec_builder/nested_example_group_builder.cr @@ -7,7 +7,7 @@ module Spectator::SpecBuilder end def build(parent_group) - context = TestContext.new(parent_group.context, build_hooks, build_conditions, parent_group.context.values) + 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) diff --git a/src/spectator/spec_builder/root_example_group_builder.cr b/src/spectator/spec_builder/root_example_group_builder.cr index 02f4914..3dbbcf7 100644 --- a/src/spectator/spec_builder/root_example_group_builder.cr +++ b/src/spectator/spec_builder/root_example_group_builder.cr @@ -4,7 +4,7 @@ require "./example_group_builder" module Spectator::SpecBuilder class RootExampleGroupBuilder < ExampleGroupBuilder def build - context = TestContext.new(nil, build_hooks, build_conditions, TestValues.empty) + 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) diff --git a/src/spectator/spec_builder/sample_example_group_builder.cr b/src/spectator/spec_builder/sample_example_group_builder.cr index eacd596..955ac7a 100644 --- a/src/spectator/spec_builder/sample_example_group_builder.cr +++ b/src/spectator/spec_builder/sample_example_group_builder.cr @@ -9,7 +9,7 @@ module Spectator::SpecBuilder 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) + 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) @@ -19,7 +19,7 @@ module Spectator::SpecBuilder 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) + 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) diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr index 8d16fee..fd735e2 100644 --- a/src/spectator/test_context.cr +++ b/src/spectator/test_context.cr @@ -5,7 +5,13 @@ module Spectator class TestContext getter values - def initialize(@parent : TestContext?, @hooks : ExampleHooks, @conditions : ExampleConditions, @values : TestValues) + 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 From eb1a99b3abe711e5aca1cc6875f4b05e6cd1eabd Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 10 Nov 2019 12:49:24 -0700 Subject: [PATCH 132/205] Fix deque creation --- src/spectator/spec_builder/example_group_builder.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/spec_builder/example_group_builder.cr b/src/spectator/spec_builder/example_group_builder.cr index 90034b4..e0871c8 100644 --- a/src/spectator/spec_builder/example_group_builder.cr +++ b/src/spectator/spec_builder/example_group_builder.cr @@ -50,7 +50,7 @@ module Spectator::SpecBuilder def add_default_stub(type : T.class, stub : Mocks::MethodStub) forall T key = type.name - @default_stubs[key] = Dequeue.new unless @default_stubs.has_key?(key) + @default_stubs[key] = Deque(Mocks::MethodStub).new unless @default_stubs.has_key?(key) @default_stubs[key] << stub end From 4fea5ea435b7855a28a09a35fa68009bd109afdc Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 10 Nov 2019 12:49:36 -0700 Subject: [PATCH 133/205] Add ability to define default stub for mocks --- src/spectator/mocks/stubs.cr | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/spectator/mocks/stubs.cr b/src/spectator/mocks/stubs.cr index e2310cd..092da34 100644 --- a/src/spectator/mocks/stubs.cr +++ b/src/spectator/mocks/stubs.cr @@ -1,6 +1,6 @@ module Spectator::Mocks module Stubs - private macro stub(definition, &block) + private macro stub(definition, _file = __FILE__, _line = __LINE__, &block) {% name = nil params = nil @@ -33,6 +33,15 @@ module Spectator::Mocks end.id %} + {% if body && !body.is_a?(Nop) %} + %source = ::Spectator::Source.new({{_file}}, {{_line}}) + %proc = ->{ + {{body.body}} + } + %ds = ::Spectator::Mocks::ProcMethodStub.new({{name.symbolize}}, %source, %proc) + ::Spectator::SpecBuilder.add_default_stub({{@type.name}}, %ds) + {% end %} + def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} if (%harness = ::Spectator::Harness.current?) %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) From 2bc4f71edb877191c143b0177cccf90257d688a6 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 10 Nov 2019 13:04:28 -0700 Subject: [PATCH 134/205] Add stubs in reverse order --- src/spectator/spec_builder/example_group_builder.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/spec_builder/example_group_builder.cr b/src/spectator/spec_builder/example_group_builder.cr index e0871c8..950f99d 100644 --- a/src/spectator/spec_builder/example_group_builder.cr +++ b/src/spectator/spec_builder/example_group_builder.cr @@ -51,7 +51,7 @@ module Spectator::SpecBuilder 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] << stub + @default_stubs[key].unshift(stub) end private def build_hooks From d2e27d1e6e3d88be588dea1a691173f788ff62c6 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 10 Nov 2019 13:08:43 -0700 Subject: [PATCH 135/205] Pass test context to registry --- src/spectator/harness.cr | 2 +- src/spectator/mocks/registry.cr | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 70ad610..537bbf2 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -59,7 +59,7 @@ module Spectator # 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.stubs) + @mocks = Mocks::Registry.new(@example.group.context) end end end diff --git a/src/spectator/mocks/registry.cr b/src/spectator/mocks/registry.cr index 31c956b..2791bc5 100644 --- a/src/spectator/mocks/registry.cr +++ b/src/spectator/mocks/registry.cr @@ -10,8 +10,8 @@ module Spectator::Mocks @all_instances = {} of String => Entry @entries = {} of Key => Entry - def initialize(default_stubs) - @all_instances = default_stubs.map do |k, v| + def initialize(context : TestContext) + @all_instances = context.stubs.map do |k, v| entry = Entry.new entry.stubs.concat(v) {k, entry} From 6c0693f2a40d51f428c28d68ecadeebbdc3a6578 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 10 Nov 2019 13:22:18 -0700 Subject: [PATCH 136/205] Add default stubs from parent contexts --- src/spectator/mocks/registry.cr | 19 ++++++++++++++----- src/spectator/test_context.cr | 10 ++++++---- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/spectator/mocks/registry.cr b/src/spectator/mocks/registry.cr index 2791bc5..c76c1ba 100644 --- a/src/spectator/mocks/registry.cr +++ b/src/spectator/mocks/registry.cr @@ -11,11 +11,20 @@ module Spectator::Mocks @entries = {} of Key => Entry def initialize(context : TestContext) - @all_instances = context.stubs.map do |k, v| - entry = Entry.new - entry.stubs.concat(v) - {k, entry} - end.to_h + 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 diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr index fd735e2..bf2c612 100644 --- a/src/spectator/test_context.cr +++ b/src/spectator/test_context.cr @@ -3,15 +3,17 @@ 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))) + @hooks : ExampleHooks, + @conditions : ExampleConditions, + @values : TestValues, + @stubs : Hash(String, Deque(Mocks::MethodStub))) @before_all_hooks_run = false @after_all_hooks_run = false end From 96aae8f6d5c6c1f975246a729af832e5f308ea3a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 10 Nov 2019 13:49:10 -0700 Subject: [PATCH 137/205] Use harness variable --- src/spectator/mocks/stubs.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/mocks/stubs.cr b/src/spectator/mocks/stubs.cr index 092da34..a1c1423 100644 --- a/src/spectator/mocks/stubs.cr +++ b/src/spectator/mocks/stubs.cr @@ -47,7 +47,7 @@ module Spectator::Mocks %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) %harness.mocks.record_call(self, %call) - if (%stub = ::Spectator::Harness.current.mocks.find_stub(self, %call)) + if (%stub = %harness.mocks.find_stub(self, %call)) return %stub.call!(%args, typeof({{original}}({{args.splat}}))) end end @@ -59,7 +59,7 @@ module Spectator::Mocks %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) %harness.mocks.record_call(self, %call) - if (%stub = ::Spectator::Harness.current.mocks.find_stub(self, %call)) + if (%stub = %harness.mocks.find_stub(self, %call)) return %stub.call!(%args, typeof({{original}}({{args.splat}}) { |*%ya| yield *%ya })) end end From fd2c6d3d8c2b3bb04ccd07e911e10fd37e56c0fd Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 11 Nov 2019 20:59:12 -0700 Subject: [PATCH 138/205] Remove reference to test Disallows group context values being used within double definitions. This had an adverse effect where context values could be called on the double (dbl.foo). --- src/spectator/dsl/mocks.cr | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 7321ac7..19624d7 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -3,15 +3,13 @@ require "../mocks" module Spectator::DSL macro double(name, &block) {% if block.is_a?(Nop) %} - Double{{name.id}}.new(self) + Double{{name.id}}.new {% else %} class Double{{name.id}} < ::Spectator::Mocks::Double - def initialize(@spectator_test : {{@type.id}}) + def initialize super({{name.id.symbolize}}) end - forward_missing_to @spectator_test - {{block.body}} end {% end %} From 18b3879dce1630d700b163700636a20997953fa2 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 11 Nov 2019 20:59:33 -0700 Subject: [PATCH 139/205] Treat symbols and non-symbols the same --- src/spectator/dsl/mocks.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 19624d7..19c9b21 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -43,6 +43,6 @@ module Spectator::DSL macro receive(method_name, _source_file = __FILE__, _source_line = __LINE__) %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - ::Spectator::Mocks::NilMethodStub.new({{method_name.symbolize}}, %source) + ::Spectator::Mocks::NilMethodStub.new({{method_name.id.symbolize}}, %source) end end From 925af7908b792713d849426ef82edb6c0704b769 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 11 Nov 2019 22:08:36 -0700 Subject: [PATCH 140/205] Raise UnexpectedMessageError --- src/spectator/mocks/double.cr | 3 ++- src/spectator/mocks/unexpected_message_error.cr | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 src/spectator/mocks/unexpected_message_error.cr diff --git a/src/spectator/mocks/double.cr b/src/spectator/mocks/double.cr index e714042..436acb4 100644 --- a/src/spectator/mocks/double.cr +++ b/src/spectator/mocks/double.cr @@ -1,5 +1,6 @@ require "./generic_method_call" require "./generic_method_stub" +require "./unexpected_message_error" module Spectator::Mocks abstract class Double @@ -61,7 +62,7 @@ module Spectator::Mocks {% if body && !body.is_a?(Nop) %} {{body.body}} {% else %} - raise "Stubbed method called without being allowed" + raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{name}}") # This code shouldn't be reached, but makes the compiler happy to have a matching return type. {% if definition.is_a?(TypeDeclaration) %} %x = uninitialized {{definition.type}} diff --git a/src/spectator/mocks/unexpected_message_error.cr b/src/spectator/mocks/unexpected_message_error.cr new file mode 100644 index 0000000..9207846 --- /dev/null +++ b/src/spectator/mocks/unexpected_message_error.cr @@ -0,0 +1,4 @@ +module Spectator::Mocks + class UnexpectedMessageError < Exception + end +end From 73e3f8fd6672de0249747698fc8780780fd2d164 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 11 Nov 2019 22:11:58 -0700 Subject: [PATCH 141/205] Raise on non-defined methods --- src/spectator/mocks/double.cr | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/spectator/mocks/double.cr b/src/spectator/mocks/double.cr index 436acb4..ac20348 100644 --- a/src/spectator/mocks/double.cr +++ b/src/spectator/mocks/double.cr @@ -73,6 +73,10 @@ module Spectator::Mocks end end + macro method_missing(call) + raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{call.name}}") + end + def to_s(io) io << "Double(" io << @spectator_double_name From 9c161a07ffc56360093c4729c7190678b4cfd46a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 11 Nov 2019 22:17:37 -0700 Subject: [PATCH 142/205] Support for canned responses in double creation --- src/spectator/dsl/mocks.cr | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 19c9b21..b6c2dd2 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -1,9 +1,13 @@ require "../mocks" module Spectator::DSL - macro double(name, &block) + macro double(name, **stubs, &block) {% if block.is_a?(Nop) %} - Double{{name.id}}.new + Double{{name.id}}.new.tap do |%double| + {% for name, value in stubs %} + allow(%double).to receive({{name.id}}).and_return({{value}}) + {% end %} + end {% else %} class Double{{name.id}} < ::Spectator::Mocks::Double def initialize From 20f68e956d3d878360a39c89170192e0dfbdb0f3 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 11 Nov 2019 22:27:53 -0700 Subject: [PATCH 143/205] Add receive_messages macro to stub multiple methods --- src/spectator/dsl/mocks.cr | 9 +++++++++ src/spectator/mocks/allow.cr | 6 ++++++ src/spectator/mocks/allow_any_instance.cr | 6 ++++++ 3 files changed, 21 insertions(+) diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index b6c2dd2..0bd6320 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -49,4 +49,13 @@ module Spectator::DSL %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) ::Spectator::Mocks::NilMethodStub.new({{method_name.id.symbolize}}, %source) 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 diff --git a/src/spectator/mocks/allow.cr b/src/spectator/mocks/allow.cr index 1479e69..fb6b86f 100644 --- a/src/spectator/mocks/allow.cr +++ b/src/spectator/mocks/allow.cr @@ -8,5 +8,11 @@ module Spectator::Mocks def to(stub : MethodStub) : Nil Harness.current.mocks.add_stub(@mock, stub) end + + def to(stubs : Enumerable(MethodStub)) : Nil + stubs.each do |stub| + Harness.current.mocks.add_stub(@mock, stub) + end + end end end diff --git a/src/spectator/mocks/allow_any_instance.cr b/src/spectator/mocks/allow_any_instance.cr index 6cc892a..b652940 100644 --- a/src/spectator/mocks/allow_any_instance.cr +++ b/src/spectator/mocks/allow_any_instance.cr @@ -5,5 +5,11 @@ module Spectator::Mocks def to(stub : MethodStub) : Nil Harness.current.mocks.add_type_stub(T, stub) end + + def to(stubs : Enumerable(MethodStub)) : Nil + stubs.each do |stub| + Harness.current.mocks.add_type_stub(T, stub) + end + end end end From 233e9573de977c3d3a8089aba10138e96f4c0b60 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 11 Nov 2019 22:36:29 -0700 Subject: [PATCH 144/205] Placeholders for delayed message expectation --- src/spectator/expectations/expectation_partial.cr | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/spectator/expectations/expectation_partial.cr b/src/spectator/expectations/expectation_partial.cr index ed2e33c..4bb7813 100644 --- a/src/spectator/expectations/expectation_partial.cr +++ b/src/spectator/expectations/expectation_partial.cr @@ -24,6 +24,10 @@ module Spectator::Expectations report(match_data) end + def to(stub : Mocks::MethodStub) : Nil + raise NotImplementedError.new("`expect(double).to receive(message)` syntax not implemented yet") + end + # Asserts that some criteria defined by the matcher is not satisfied. # This is effectively the opposite of `#to`. def to_not(matcher) : Nil @@ -31,6 +35,10 @@ module Spectator::Expectations report(match_data) end + def to_not(stub : Mocks::MethodStub) : Nil + raise NotImplementedError.new("`expect(double).to_not receive(message)` syntax not implemented yet") + end + # ditto @[AlwaysInline] def not_to(matcher) : Nil From c3cba6962c2f9ee5665d814726f15e0244d0884d Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 11 Nov 2019 23:30:30 -0700 Subject: [PATCH 145/205] Get stubs on class methods (self.) working --- src/spectator/mocks/stubs.cr | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/spectator/mocks/stubs.cr b/src/spectator/mocks/stubs.cr index a1c1423..2c2cf1f 100644 --- a/src/spectator/mocks/stubs.cr +++ b/src/spectator/mocks/stubs.cr @@ -2,11 +2,13 @@ module Spectator::Mocks module Stubs private macro stub(definition, _file = __FILE__, _line = __LINE__, &block) {% + receiver = nil name = nil params = nil args = nil body = nil if definition.is_a?(Call) # stub foo { :bar } + receiver = definition.receiver.id named = false name = definition.name.id params = definition.args @@ -31,6 +33,12 @@ module Spectator::Mocks else :super end.id + receiver = if receiver == :self.id + original = :previous_def.id + "self." + else + "" + end.id %} {% if body && !body.is_a?(Nop) %} @@ -42,7 +50,7 @@ module Spectator::Mocks ::Spectator::SpecBuilder.add_default_stub({{@type.name}}, %ds) {% end %} - def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} + def {{receiver}}{{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} if (%harness = ::Spectator::Harness.current?) %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) @@ -54,7 +62,7 @@ module Spectator::Mocks {{original}}({{args.splat}}) end - def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} + def {{receiver}}{{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} if (%harness = ::Spectator::Harness.current?) %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) From 48ea53c2f50f0ec6197c8eff2db0f0f6b0e38f3c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 11 Nov 2019 23:30:36 -0700 Subject: [PATCH 146/205] Formatting --- src/spectator/mocks/registry.cr | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/spectator/mocks/registry.cr b/src/spectator/mocks/registry.cr index c76c1ba..bae45f9 100644 --- a/src/spectator/mocks/registry.cr +++ b/src/spectator/mocks/registry.cr @@ -15,12 +15,12 @@ module Spectator::Mocks 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 + @all_instances[k].stubs + else + entry = Entry.new + @all_instances[k] = entry + entry.stubs + end stubs.concat(v) end current_context = current_context.parent? From 94e210c3065ce2f8ea9dfab7eb845e25120cfe71 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 11 Nov 2019 23:40:08 -0700 Subject: [PATCH 147/205] Implement null doubles --- src/spectator/dsl/mocks.cr | 30 ++++++++++++++++++++++++++++-- src/spectator/mocks/double.cr | 4 +++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 0bd6320..3982756 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -10,8 +10,34 @@ module Spectator::DSL end {% else %} class Double{{name.id}} < ::Spectator::Mocks::Double - def initialize - super({{name.id.symbolize}}) + def initialize(null = false) + super({{name.id.symbolize}}, null) + end + + def as_null_object + Double{{name.id}}.new(true) + end + + {{block.body}} + end + {% end %} + end + + macro null_double(name, **stubs, &block) + {% if block.is_a?(Nop) %} + Double{{name.id}}.new(true).tap do |%double| + {% for name, value in stubs %} + allow(%double).to receive({{name.id}}).and_return({{value}}) + {% end %} + end + {% else %} + class Double{{name.id}} < ::Spectator::Mocks::Double + def initialize(null = true) + super({{name.id.symbolize}}, null) + end + + def as_null_object + Double{{name.id}}.new(true) end {{block.body}} diff --git a/src/spectator/mocks/double.cr b/src/spectator/mocks/double.cr index ac20348..d546b37 100644 --- a/src/spectator/mocks/double.cr +++ b/src/spectator/mocks/double.cr @@ -4,7 +4,7 @@ require "./unexpected_message_error" module Spectator::Mocks abstract class Double - def initialize(@spectator_double_name : Symbol) + def initialize(@spectator_double_name : Symbol, @null = false) end private macro stub(definition, &block) @@ -74,6 +74,8 @@ module Spectator::Mocks end macro method_missing(call) + return self if @null + raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{call.name}}") end From f50e71606e443367c71e05da9ce50808ba96f424 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 12 Nov 2019 21:13:44 -0700 Subject: [PATCH 148/205] Make derived double names safer --- src/spectator/dsl/mocks.cr | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 3982756..7e4497c 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -2,16 +2,17 @@ require "../mocks" module Spectator::DSL macro double(name, **stubs, &block) + {% safe_name = name.id.symbolize.gsub(/\W/, "_").id %} {% if block.is_a?(Nop) %} - Double{{name.id}}.new.tap do |%double| + Double{{safe_name}}.new.tap do |%double| {% for name, value in stubs %} allow(%double).to receive({{name.id}}).and_return({{value}}) {% end %} end {% else %} - class Double{{name.id}} < ::Spectator::Mocks::Double + class Double{{safe_name}} < ::Spectator::Mocks::Double def initialize(null = false) - super({{name.id.symbolize}}, null) + super({{name.id.stringify}}, null) end def as_null_object @@ -24,16 +25,17 @@ module Spectator::DSL end macro null_double(name, **stubs, &block) + {% safe_name = name.id.symbolize.gsub(/\W/, "_").id %} {% if block.is_a?(Nop) %} - Double{{name.id}}.new(true).tap do |%double| + Double{{safe_name}}.new(true).tap do |%double| {% for name, value in stubs %} allow(%double).to receive({{name.id}}).and_return({{value}}) {% end %} end {% else %} - class Double{{name.id}} < ::Spectator::Mocks::Double + class Double{{safe_name}} < ::Spectator::Mocks::Double def initialize(null = true) - super({{name.id.symbolize}}, null) + super({{name.id.stringify}}, null) end def as_null_object From 87a60cf92a274a92104aecf9f54ff52af20faf1b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 12 Nov 2019 21:21:30 -0700 Subject: [PATCH 149/205] Split out double macros --- src/spectator/dsl/mocks.cr | 90 ++++++++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 34 deletions(-) diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 7e4497c..1fea3d4 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -2,51 +2,73 @@ require "../mocks" module Spectator::DSL macro double(name, **stubs, &block) - {% safe_name = name.id.symbolize.gsub(/\W/, "_").id %} {% if block.is_a?(Nop) %} - Double{{safe_name}}.new.tap do |%double| - {% for name, value in stubs %} - allow(%double).to receive({{name.id}}).and_return({{value}}) - {% end %} - end + create_double({{name}}, {{stubs.double_splat}}) {% else %} - class Double{{safe_name}} < ::Spectator::Mocks::Double - def initialize(null = false) - super({{name.id.stringify}}, null) - end - - def as_null_object - Double{{name.id}}.new(true) - end - - {{block.body}} - end + define_double({{name}}, {{stubs.double_splat}}) {{block}} {% end %} end - macro null_double(name, **stubs, &block) + macro create_double(name, **stubs) {% safe_name = name.id.symbolize.gsub(/\W/, "_").id %} + Double{{safe_name}}.new.tap do |%double| + {% for name, value in stubs %} + allow(%double).to receive({{name.id}}).and_return({{value}}) + {% end %} + end + end + + macro define_double(name, **stubs, &block) + {% safe_name = name.id.symbolize.gsub(/\W/, "_").id %} + class Double{{safe_name}} < ::Spectator::Mocks::Double + def initialize(null = false) + super({{name.id.stringify}}, null) + end + + def as_null_object + Double{{safe_name}}.new(true) + end + + # TODO: Do something with **stubs? + + {{block.body}} + end + end + + macro null_double(name, **stubs, &block) {% if block.is_a?(Nop) %} - Double{{safe_name}}.new(true).tap do |%double| - {% for name, value in stubs %} - allow(%double).to receive({{name.id}}).and_return({{value}}) - {% end %} - end + create_null_double({{name}}, {{stubs.double_splat}}) {% else %} - class Double{{safe_name}} < ::Spectator::Mocks::Double - def initialize(null = true) - super({{name.id.stringify}}, null) - end - - def as_null_object - Double{{name.id}}.new(true) - end - - {{block.body}} - end + define_null_double({{name}}, {{stubs.double_splat}}) {{block}} {% end %} end + macro create_null_double(name, **stubs) + {% safe_name = name.id.symbolize.gsub(/\W/, "_").id %} + Double{{safe_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(name, **stubs, &block) + {% safe_name = name.id.symbolize.gsub(/\W/, "_").id %} + class Double{{safe_name}} < ::Spectator::Mocks::Double + def initialize(null = true) + super({{name.id.stringify}}, null) + end + + def as_null_object + Double{{safe_name}}.new(true) + end + + # TODO: Do something with **stubs? + + {{block.body}} + end + end + macro mock(name, &block) {% resolved = name.resolve type = if resolved < Reference From cb466b4ff5994a7b7c8f509f7cb4da34dbbde07a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 12 Nov 2019 21:44:35 -0700 Subject: [PATCH 150/205] Display better error when a double isn't found/defined --- src/spectator/dsl/mocks.cr | 52 +++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 1fea3d4..46e02ed 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -2,31 +2,39 @@ require "../mocks" module Spectator::DSL macro double(name, **stubs, &block) + {% + safe_name = name.id.symbolize.gsub(/\W/, "_").id + type_name = "Double#{safe_name}".id + %} + {% if block.is_a?(Nop) %} - create_double({{name}}, {{stubs.double_splat}}) + create_double({{type_name}}, {{name}}, {{stubs.double_splat}}) {% else %} - define_double({{name}}, {{stubs.double_splat}}) {{block}} + define_double({{type_name}}, {{name}}, {{stubs.double_splat}}) {{block}} {% end %} end - macro create_double(name, **stubs) - {% safe_name = name.id.symbolize.gsub(/\W/, "_").id %} - Double{{safe_name}}.new.tap do |%double| + macro create_double(type_name, name, **stubs) + {% + type = type_name.resolve? + raise "Could not find a double labeled #{name}" unless type + %} + + {{type_name}}.new.tap do |%double| {% for name, value in stubs %} allow(%double).to receive({{name.id}}).and_return({{value}}) {% end %} end end - macro define_double(name, **stubs, &block) - {% safe_name = name.id.symbolize.gsub(/\W/, "_").id %} - class Double{{safe_name}} < ::Spectator::Mocks::Double + macro define_double(type_name, name, **stubs, &block) + class {{type_name}} < ::Spectator::Mocks::Double def initialize(null = false) super({{name.id.stringify}}, null) end def as_null_object - Double{{safe_name}}.new(true) + {{type_name}}.new(true) end # TODO: Do something with **stubs? @@ -36,31 +44,39 @@ module Spectator::DSL end macro null_double(name, **stubs, &block) + {% + safe_name = name.id.symbolize.gsub(/\W/, "_").id + type_name = "Double#{safe_name}".id + %} + {% if block.is_a?(Nop) %} - create_null_double({{name}}, {{stubs.double_splat}}) + create_null_double({{type_name}}, {{name}}, {{stubs.double_splat}}) {% else %} - define_null_double({{name}}, {{stubs.double_splat}}) {{block}} + define_null_double({{type_name}}, {{name}}, {{stubs.double_splat}}) {{block}} {% end %} end - macro create_null_double(name, **stubs) - {% safe_name = name.id.symbolize.gsub(/\W/, "_").id %} - Double{{safe_name}}.new(true).tap do |%double| + macro create_null_double(type_name, name, **stubs) + {% + type = type_name.resolve? + raise "Could not find a double labeled #{name}" unless type + %} + + {{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(name, **stubs, &block) - {% safe_name = name.id.symbolize.gsub(/\W/, "_").id %} - class Double{{safe_name}} < ::Spectator::Mocks::Double + 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 - Double{{safe_name}}.new(true) + {{type_name}}.new(true) end # TODO: Do something with **stubs? From 38ec44c9ac50e4f33df6ba45344b9f91d664aebc Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 12 Nov 2019 21:46:33 -0700 Subject: [PATCH 151/205] Simplify type resolution code --- src/spectator/dsl/mocks.cr | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 46e02ed..2a83711 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -15,10 +15,7 @@ module Spectator::DSL end macro create_double(type_name, name, **stubs) - {% - type = type_name.resolve? - raise "Could not find a double labeled #{name}" unless type - %} + {% type_name.resolve? || raise("Could not find a double labeled #{name}") %} {{type_name}}.new.tap do |%double| {% for name, value in stubs %} @@ -57,10 +54,7 @@ module Spectator::DSL end macro create_null_double(type_name, name, **stubs) - {% - type = type_name.resolve? - raise "Could not find a double labeled #{name}" unless type - %} + {% type_name.resolve? || raise("Could not find a double labeled #{name}") %} {{type_name}}.new(true).tap do |%double| {% for name, value in stubs %} From b0562981f7a4c9126f26d236ce727420af563d8f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 12 Nov 2019 21:54:55 -0700 Subject: [PATCH 152/205] Change double name to string --- src/spectator/mocks/double.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/mocks/double.cr b/src/spectator/mocks/double.cr index d546b37..4af8cf1 100644 --- a/src/spectator/mocks/double.cr +++ b/src/spectator/mocks/double.cr @@ -4,7 +4,7 @@ require "./unexpected_message_error" module Spectator::Mocks abstract class Double - def initialize(@spectator_double_name : Symbol, @null = false) + def initialize(@spectator_double_name : String, @null = false) end private macro stub(definition, &block) From b9fe1b6a60907141699246740b837e1bf4edc6a4 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 12 Nov 2019 21:55:16 -0700 Subject: [PATCH 153/205] Initial code to support anonymous doubles --- src/spectator/dsl/mocks.cr | 48 +++++++++++++------- src/spectator/mocks/anonymous_double.cr | 16 +++++++ src/spectator/mocks/anonymous_null_double.cr | 10 ++++ 3 files changed, 58 insertions(+), 16 deletions(-) create mode 100644 src/spectator/mocks/anonymous_double.cr create mode 100644 src/spectator/mocks/anonymous_null_double.cr diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 2a83711..e04ef68 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -2,15 +2,19 @@ require "../mocks" module Spectator::DSL macro double(name, **stubs, &block) - {% - 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}}) + {% if name.is_a?(StringLiteral) %} + anonymous_double({{name}}, {{stubs.double_splat}}) {% else %} - define_double({{type_name}}, {{name}}, {{stubs.double_splat}}) {{block}} + {% + 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}}) + {% else %} + define_double({{type_name}}, {{name}}, {{stubs.double_splat}}) {{block}} + {% end %} {% end %} end @@ -40,16 +44,24 @@ module Spectator::DSL end end - macro null_double(name, **stubs, &block) - {% - safe_name = name.id.symbolize.gsub(/\W/, "_").id - type_name = "Double#{safe_name}".id - %} + def anonymous_double(name : String, **stubs) + Mocks::AnonymousDouble.new(name, stubs) + end - {% if block.is_a?(Nop) %} - create_null_double({{type_name}}, {{name}}, {{stubs.double_splat}}) + macro null_double(name, **stubs, &block) + {% if name.is_a?(StringLiteral) %} + anonymous_null_double({{name}}, {{stubs.double_splat}}) {% else %} - define_null_double({{type_name}}, {{name}}, {{stubs.double_splat}}) {{block}} + {% + 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 @@ -79,6 +91,10 @@ module Spectator::DSL end end + def anonymous_null_double(name : Strings, **stubs) + AnonymousNullDouble.new(name, stubs) + end + macro mock(name, &block) {% resolved = name.resolve type = if resolved < Reference diff --git a/src/spectator/mocks/anonymous_double.cr b/src/spectator/mocks/anonymous_double.cr new file mode 100644 index 0000000..663692c --- /dev/null +++ b/src/spectator/mocks/anonymous_double.cr @@ -0,0 +1,16 @@ +module Spectator::Mocks + class AnonymousDouble(T) + def initialize(@name : String, @values : T) + end + + def as_null_object + AnonymousNullDouble.new(@values) + end + + macro method_missing(call) + @values.fetch({{call.name.symbolize}}) do + raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{call.name}}") + end + end + end +end diff --git a/src/spectator/mocks/anonymous_null_double.cr b/src/spectator/mocks/anonymous_null_double.cr new file mode 100644 index 0000000..474729a --- /dev/null +++ b/src/spectator/mocks/anonymous_null_double.cr @@ -0,0 +1,10 @@ +module Spectator::Mocks + class AnonymousNullDouble(T) + def initialize(@name : String, @values : T) + end + + macro method_missing(call) + @values.fetch({{call.name.symbolize}}) { self } + end + end +end From e44f0229d6fdad93982a8001a0f2340684ae4f29 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 14 Nov 2019 17:12:27 -0700 Subject: [PATCH 154/205] Allow stubbing/overriding anonymous double methods --- src/spectator/mocks/anonymous_double.cr | 11 +++++++++-- src/spectator/mocks/anonymous_null_double.cr | 9 ++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/spectator/mocks/anonymous_double.cr b/src/spectator/mocks/anonymous_double.cr index 663692c..26c9f26 100644 --- a/src/spectator/mocks/anonymous_double.cr +++ b/src/spectator/mocks/anonymous_double.cr @@ -8,8 +8,15 @@ module Spectator::Mocks end macro method_missing(call) - @values.fetch({{call.name.symbolize}}) do - raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{call.name}}") + args = ::Spectator::Mocks::GenericArguments.create({{call.args.splat}}) + call = ::Spectator::Mocks::GenericMethodCall.new({{call.name.symbolize}}, args) + ::Spectator::Harness.current.mocks.record_call(self, call) + if (stub = ::Spectator::Harness.current.mocks.find_stub(self, call)) + stub.call!(args, typeof(@values.fetch({{call.name.symbolize}}) { raise })) + else + @values.fetch({{call.name.symbolize}}) do + raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{call.name}}") + end end end end diff --git a/src/spectator/mocks/anonymous_null_double.cr b/src/spectator/mocks/anonymous_null_double.cr index 474729a..3da8122 100644 --- a/src/spectator/mocks/anonymous_null_double.cr +++ b/src/spectator/mocks/anonymous_null_double.cr @@ -4,7 +4,14 @@ module Spectator::Mocks end macro method_missing(call) - @values.fetch({{call.name.symbolize}}) { self } + args = ::Spectator::Mocks::GenericArguments.create({{call.args.splat}}) + call = ::Spectator::Mocks::GenericMethodCall.new({{call.name.symbolize}}, args) + ::Spectator::Harness.current.mocks.record_call(self, call) + if (stub = ::Spectator::Harness.current.mocks.find_stub(self, call)) + stub.call!(args, typeof(@values.fetch({{call.name.symbolize}}) { self })) + else + @values.fetch({{call.name.symbolize}}) { self } + end end end end From acad88204affa6705a5bdc901b85bacbcfd9600e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 14 Nov 2019 17:55:56 -0700 Subject: [PATCH 155/205] Fix issues with creating anonymous null doubles --- src/spectator/dsl/mocks.cr | 2 +- src/spectator/mocks/anonymous_double.cr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index e04ef68..a398059 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -91,7 +91,7 @@ module Spectator::DSL end end - def anonymous_null_double(name : Strings, **stubs) + def anonymous_null_double(name : String, **stubs) AnonymousNullDouble.new(name, stubs) end diff --git a/src/spectator/mocks/anonymous_double.cr b/src/spectator/mocks/anonymous_double.cr index 26c9f26..d1aea1d 100644 --- a/src/spectator/mocks/anonymous_double.cr +++ b/src/spectator/mocks/anonymous_double.cr @@ -4,7 +4,7 @@ module Spectator::Mocks end def as_null_object - AnonymousNullDouble.new(@values) + AnonymousNullDouble.new(@name, @values) end macro method_missing(call) From b40abe805754c17eefcd488ff718f53648e7d3f7 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 14 Nov 2019 18:06:20 -0700 Subject: [PATCH 156/205] Fix GenericArguments#to_s --- src/spectator/mocks/generic_arguments.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/mocks/generic_arguments.cr b/src/spectator/mocks/generic_arguments.cr index 7ac7db5..18d4193 100644 --- a/src/spectator/mocks/generic_arguments.cr +++ b/src/spectator/mocks/generic_arguments.cr @@ -35,7 +35,7 @@ module Spectator::Mocks io << key io << ": " value.inspect(io) - io << ", " if 1 < @opts.size - 1 + io << ", " if i < @opts.size - 1 end end end From bd0396c001aed4628f0b4e6b1e9b07e3b5823eaf Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 14 Nov 2019 18:19:37 -0700 Subject: [PATCH 157/205] Add to_eventually and to_never operations This defines what they should look like. --- .../expectations/expectation_partial.cr | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/spectator/expectations/expectation_partial.cr b/src/spectator/expectations/expectation_partial.cr index ed2e33c..8aa3709 100644 --- a/src/spectator/expectations/expectation_partial.cr +++ b/src/spectator/expectations/expectation_partial.cr @@ -37,6 +37,24 @@ module Spectator::Expectations 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 + + # 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 + + # 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) From a80d018ff6d7f088c42c198947b0d8817502251a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 14 Nov 2019 18:23:45 -0700 Subject: [PATCH 158/205] Remove type annotation/requirement --- src/spectator/should.cr | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/spectator/should.cr b/src/spectator/should.cr index de75c85..bfa0941 100644 --- a/src/spectator/should.cr +++ b/src/spectator/should.cr @@ -16,7 +16,7 @@ class Object # ``` # require "spectator/should" # ``` - def should(matcher : ::Spectator::Matchers::Matcher) + 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. @@ -28,7 +28,7 @@ class Object # Works the same as `#should` except the condition is inverted. # When `#should` succeeds, this method will fail, and vice-versa. - def should_not(matcher : ::Spectator::Matchers::Matcher) + def should_not(matcher) actual = ::Spectator::TestValue.new(self) source = ::Spectator::Source.new(__FILE__, __LINE__) ::Spectator::Expectations::ExpectationPartial.new(actual, source).to_not(matcher) @@ -38,7 +38,7 @@ 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 : ::Spectator::Matchers::Matcher) + def should(matcher) actual = ::Spectator::TestBlock.new(self) source = ::Spectator::Source.new(__FILE__, __LINE__) ::Spectator::Expectations::ExpectationPartial.new(actual, source).to(matcher) @@ -46,7 +46,7 @@ struct Proc(*T, R) # Works the same as `#should` except the condition is inverted. # When `#should` succeeds, this method will fail, and vice-versa. - def should_not(matcher : ::Spectator::Matchers::Matcher) + def should_not(matcher) actual = ::Spectator::TestBlock.new(self) source = ::Spectator::Source.new(__FILE__, __LINE__) ::Spectator::Expectations::BlockExpectationPartial.new(actual, source).to_not(matcher) From 173f7bfa13f8207011361c7865865c0dc18f00c0 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 14 Nov 2019 18:26:24 -0700 Subject: [PATCH 159/205] Add should_eventually and should_never methods --- src/spectator/should.cr | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/spectator/should.cr b/src/spectator/should.cr index bfa0941..b8cda4a 100644 --- a/src/spectator/should.cr +++ b/src/spectator/should.cr @@ -33,6 +33,18 @@ class Object source = ::Spectator::Source.new(__FILE__, __LINE__) ::Spectator::Expectations::ExpectationPartial.new(actual, source).to_not(matcher) 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) } + 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) } + end end struct Proc(*T, R) From 2128184659eb998e87710b86dd37b773c514ddd6 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 14 Nov 2019 18:30:48 -0700 Subject: [PATCH 160/205] Implement defer logic in harness --- src/spectator/harness.cr | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 8fe0070..caa43a0 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -51,10 +51,22 @@ module Spectator @reporter.expectations end + # Marks a block of code to run later. + def defer(&block : ->) : Nil + @deferred << block + end + + # Runs all deferred blocks. + 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 + @deferred = Deque(->).new end end end From 6b3885af309c2da217e77e6daaeaba033c7be50e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 14 Nov 2019 18:41:26 -0700 Subject: [PATCH 161/205] Run deferred blocks --- src/spectator/runnable_example.cr | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/spectator/runnable_example.cr b/src/spectator/runnable_example.cr index d981689..9bbbbed 100644 --- a/src/spectator/runnable_example.cr +++ b/src/spectator/runnable_example.cr @@ -20,6 +20,7 @@ module Spectator context.run_before_hooks(self) run_example(result) context.run_after_hooks(self) + run_deferred(result) end end @@ -40,6 +41,17 @@ module Spectator 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) From 763c99f33868db8b242b705ed5bd775b1d04d840 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 14 Nov 2019 18:48:43 -0700 Subject: [PATCH 162/205] Formatting --- 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 e197e2e..33ed2f0 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -70,7 +70,7 @@ module Spectator # 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) + @mocks = Mocks::Registry.new(@example.group.context) @deferred = Deque(->).new end end From ac3a5c8515f20f80b4e327c01c1c3f8f04a1d7f3 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 14 Nov 2019 19:15:51 -0700 Subject: [PATCH 163/205] Store GenericArguments --- src/spectator/mocks/generic_method_stub.cr | 6 ++++-- src/spectator/mocks/nil_method_stub.cr | 2 +- src/spectator/mocks/proc_method_stub.cr | 4 ++-- src/spectator/mocks/value_method_stub.cr | 4 ++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/spectator/mocks/generic_method_stub.cr b/src/spectator/mocks/generic_method_stub.cr index f695646..373de46 100644 --- a/src/spectator/mocks/generic_method_stub.cr +++ b/src/spectator/mocks/generic_method_stub.cr @@ -4,8 +4,10 @@ require "./method_call" require "./method_stub" module Spectator::Mocks - abstract class GenericMethodStub(ReturnType) < MethodStub - def initialize(name, source, @args : Arguments? = nil) + abstract class GenericMethodStub(ReturnType, T, NT) < MethodStub + getter! arguments : GenericArguments(T, NT) + + def initialize(name, source, @args : GenericArguments(T, NT) = nil) super(name, source) end diff --git a/src/spectator/mocks/nil_method_stub.cr b/src/spectator/mocks/nil_method_stub.cr index 64e10bb..0bfe4d7 100644 --- a/src/spectator/mocks/nil_method_stub.cr +++ b/src/spectator/mocks/nil_method_stub.cr @@ -3,7 +3,7 @@ require "./generic_method_stub" require "./value_method_stub" module Spectator::Mocks - class NilMethodStub < GenericMethodStub(Nil) + class NilMethodStub < GenericMethodStub(Nil, Tuple, NamedTuple) def call(_args : GenericArguments(T, NT), _rt : RT.class) forall T, NT, RT nil end diff --git a/src/spectator/mocks/proc_method_stub.cr b/src/spectator/mocks/proc_method_stub.cr index a2fa2dd..f4a9603 100644 --- a/src/spectator/mocks/proc_method_stub.cr +++ b/src/spectator/mocks/proc_method_stub.cr @@ -2,12 +2,12 @@ require "./arguments" require "./generic_method_stub" module Spectator::Mocks - class ProcMethodStub(ReturnType) < GenericMethodStub(ReturnType) + class ProcMethodStub(ReturnType, T, NT) < GenericMethodStub(ReturnType, T, NT) def initialize(name, source, @proc : -> ReturnType, args = nil) super(name, source, args) end - def call(_args : GenericArguments(T, NT), rt : RT.class) forall T, NT, RT + def call(_args : GenericArguments(T2, NT2), rt : RT.class) forall T2, NT2, RT @proc.call end end diff --git a/src/spectator/mocks/value_method_stub.cr b/src/spectator/mocks/value_method_stub.cr index 5141441..f55ea49 100644 --- a/src/spectator/mocks/value_method_stub.cr +++ b/src/spectator/mocks/value_method_stub.cr @@ -2,12 +2,12 @@ require "./generic_arguments" require "./generic_method_stub" module Spectator::Mocks - class ValueMethodStub(ReturnType) < GenericMethodStub(ReturnType) + class ValueMethodStub(ReturnType, T, NT) < GenericMethodStub(ReturnType, T, NT) def initialize(name, source, @value : ReturnType, args = nil) super(name, source, args) end - def call(_args : GenericArguments(T, NT), rt : RT.class) forall T, NT, RT + def call(_args : GenericArguments(T2, NT2), rt : RT.class) forall T2, NT2, RT @value end end From 98bfed2f83fa9f8935bac7757ab7f84ae1c965c4 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 14 Nov 2019 19:17:55 -0700 Subject: [PATCH 164/205] Expose name and source --- src/spectator/mocks/method_stub.cr | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/spectator/mocks/method_stub.cr b/src/spectator/mocks/method_stub.cr index 49794fd..a297d14 100644 --- a/src/spectator/mocks/method_stub.cr +++ b/src/spectator/mocks/method_stub.cr @@ -3,7 +3,11 @@ require "./generic_method_call" module Spectator::Mocks abstract class MethodStub - def initialize(@name : Symbol, @source : Source) + getter name : Symbol + + getter source : Source + + def initialize(@name, @source) end def callable?(call : GenericMethodCall(T, NT)) : Bool forall T, NT From 2dc86c05ac9adb21547660d98c606ad886696d78 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 14 Nov 2019 19:35:13 -0700 Subject: [PATCH 165/205] Initial work on deferred have_received check --- src/spectator/expectations/expectation_partial.cr | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/spectator/expectations/expectation_partial.cr b/src/spectator/expectations/expectation_partial.cr index 698b072..e848eaf 100644 --- a/src/spectator/expectations/expectation_partial.cr +++ b/src/spectator/expectations/expectation_partial.cr @@ -24,8 +24,14 @@ module Spectator::Expectations report(match_data) end - def to(stub : Mocks::MethodStub) : Nil - raise NotImplementedError.new("`expect(double).to receive(message)` syntax not implemented yet") + def to(stub : Mocks::GenericMethodStub(RT, T, NT)) : Nil forall RT, T, NT + value = TestValue.new(stub.name, stub.to_s) + matcher = if stub.arguments? + Matchers::ReceiveArgumentsMatcher.new(value, stub.arguments) + else + Matchers::ReceiveMatcher.new(value) + end + to_eventually(matcher) end # Asserts that some criteria defined by the matcher is not satisfied. From 3c94d1f8fde87460c1485106139484129622ec6b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 15 Nov 2019 21:18:51 -0700 Subject: [PATCH 166/205] Workaround for generic argument type issue --- src/spectator/expectations/expectation_partial.cr | 14 ++++++++++---- .../matchers/receive_arguments_matcher.cr | 4 ++-- src/spectator/mocks/generic_method_stub.cr | 6 +++--- src/spectator/mocks/nil_method_stub.cr | 2 +- src/spectator/mocks/proc_method_stub.cr | 2 +- src/spectator/mocks/value_method_stub.cr | 2 +- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/spectator/expectations/expectation_partial.cr b/src/spectator/expectations/expectation_partial.cr index e848eaf..31606b2 100644 --- a/src/spectator/expectations/expectation_partial.cr +++ b/src/spectator/expectations/expectation_partial.cr @@ -24,10 +24,10 @@ module Spectator::Expectations report(match_data) end - def to(stub : Mocks::GenericMethodStub(RT, T, NT)) : Nil forall RT, T, NT + def to(stub : Mocks::MethodStub) : Nil value = TestValue.new(stub.name, stub.to_s) - matcher = if stub.arguments? - Matchers::ReceiveArgumentsMatcher.new(value, stub.arguments) + matcher = if (arguments = stub.arguments?) + Matchers::ReceiveArgumentsMatcher.new(value, arguments) else Matchers::ReceiveMatcher.new(value) end @@ -42,7 +42,13 @@ module Spectator::Expectations end def to_not(stub : Mocks::MethodStub) : Nil - raise NotImplementedError.new("`expect(double).to_not receive(message)` syntax not implemented yet") + value = TestValue.new(stub.name, stub.to_s) + matcher = if (arguments = stub.arguments?) + Matchers::ReceiveArgumentsMatcher.new(value, arguments) + else + Matchers::ReceiveMatcher.new(value) + end + to_never(matcher) end # ditto diff --git a/src/spectator/matchers/receive_arguments_matcher.cr b/src/spectator/matchers/receive_arguments_matcher.cr index bc729db..e4053cc 100644 --- a/src/spectator/matchers/receive_arguments_matcher.cr +++ b/src/spectator/matchers/receive_arguments_matcher.cr @@ -2,10 +2,10 @@ require "../mocks" require "./standard_matcher" module Spectator::Matchers - struct ReceiveArgumentsMatcher(T, NT) < StandardMatcher + struct ReceiveArgumentsMatcher < StandardMatcher alias Range = ::Range(Int32, Int32) | ::Range(Nil, Int32) | ::Range(Int32, Nil) - def initialize(@expected : TestExpression(Symbol), @args : Mocks::GenericArguments(T, NT), @range : Range? = nil) + def initialize(@expected : TestExpression(Symbol), @args : Mocks::Arguments, @range : Range? = nil) end def description : String diff --git a/src/spectator/mocks/generic_method_stub.cr b/src/spectator/mocks/generic_method_stub.cr index 373de46..c0ee38f 100644 --- a/src/spectator/mocks/generic_method_stub.cr +++ b/src/spectator/mocks/generic_method_stub.cr @@ -4,10 +4,10 @@ require "./method_call" require "./method_stub" module Spectator::Mocks - abstract class GenericMethodStub(ReturnType, T, NT) < MethodStub - getter! arguments : GenericArguments(T, NT) + abstract class GenericMethodStub(ReturnType) < MethodStub + getter! arguments : Arguments - def initialize(name, source, @args : GenericArguments(T, NT) = nil) + def initialize(name, source, @args : Arguments? = nil) super(name, source) end diff --git a/src/spectator/mocks/nil_method_stub.cr b/src/spectator/mocks/nil_method_stub.cr index 0bfe4d7..64e10bb 100644 --- a/src/spectator/mocks/nil_method_stub.cr +++ b/src/spectator/mocks/nil_method_stub.cr @@ -3,7 +3,7 @@ require "./generic_method_stub" require "./value_method_stub" module Spectator::Mocks - class NilMethodStub < GenericMethodStub(Nil, Tuple, NamedTuple) + class NilMethodStub < GenericMethodStub(Nil) def call(_args : GenericArguments(T, NT), _rt : RT.class) forall T, NT, RT nil end diff --git a/src/spectator/mocks/proc_method_stub.cr b/src/spectator/mocks/proc_method_stub.cr index f4a9603..b0d921f 100644 --- a/src/spectator/mocks/proc_method_stub.cr +++ b/src/spectator/mocks/proc_method_stub.cr @@ -2,7 +2,7 @@ require "./arguments" require "./generic_method_stub" module Spectator::Mocks - class ProcMethodStub(ReturnType, T, NT) < GenericMethodStub(ReturnType, T, NT) + class ProcMethodStub(ReturnType) < GenericMethodStub(ReturnType) def initialize(name, source, @proc : -> ReturnType, args = nil) super(name, source, args) end diff --git a/src/spectator/mocks/value_method_stub.cr b/src/spectator/mocks/value_method_stub.cr index f55ea49..6cdec7b 100644 --- a/src/spectator/mocks/value_method_stub.cr +++ b/src/spectator/mocks/value_method_stub.cr @@ -2,7 +2,7 @@ require "./generic_arguments" require "./generic_method_stub" module Spectator::Mocks - class ValueMethodStub(ReturnType, T, NT) < GenericMethodStub(ReturnType, T, NT) + class ValueMethodStub(ReturnType) < GenericMethodStub(ReturnType) def initialize(name, source, @value : ReturnType, args = nil) super(name, source, args) end From 72e4ac8fe95ddab72b21481f010c45c978ee8b8c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Nov 2019 08:17:46 -0700 Subject: [PATCH 167/205] Remove unnecessary free variables --- src/spectator/dsl/mocks.cr | 2 +- src/spectator/formatting/comment.cr | 2 +- src/spectator/matchers/change_from_matcher.cr | 4 ++-- src/spectator/matchers/change_matcher.cr | 10 +++++----- src/spectator/matchers/change_to_matcher.cr | 4 ++-- src/spectator/matchers/truthy_matcher.cr | 12 ++++++------ src/spectator/mocks/nil_method_stub.cr | 2 +- src/spectator/test_values.cr | 4 ++-- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index a398059..e155960 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -113,7 +113,7 @@ module Spectator::DSL {% end %} end - def allow(thing : T) forall T + def allow(thing) Mocks::Allow.new(thing) end diff --git a/src/spectator/formatting/comment.cr b/src/spectator/formatting/comment.cr index cbe6919..6a42685 100644 --- a/src/spectator/formatting/comment.cr +++ b/src/spectator/formatting/comment.cr @@ -13,7 +13,7 @@ module Spectator::Formatting end # Creates a colorized version of the comment. - def self.color(text : T) forall T + def self.color(text) Color.comment(new(text)) end end diff --git a/src/spectator/matchers/change_from_matcher.cr b/src/spectator/matchers/change_from_matcher.cr index cafb51d..fa3504b 100644 --- a/src/spectator/matchers/change_from_matcher.cr +++ b/src/spectator/matchers/change_from_matcher.cr @@ -63,12 +63,12 @@ module Spectator::Matchers end # Specifies what the resulting value of the expression must be. - def to(value : T) forall T + def to(value) ChangeExactMatcher.new(@expression, @expected, value) end # Specifies what the resulting value of the expression should change by. - def by(amount : T) forall T + def by(amount) ChangeExactMatcher.new(@expression, @expected, @expected + value) end diff --git a/src/spectator/matchers/change_matcher.cr b/src/spectator/matchers/change_matcher.cr index 2b016d5..5de2415 100644 --- a/src/spectator/matchers/change_matcher.cr +++ b/src/spectator/matchers/change_matcher.cr @@ -49,27 +49,27 @@ module Spectator::Matchers end # Specifies what the initial value of the expression must be. - def from(value : T) forall T + def from(value) ChangeFromMatcher.new(@expression, value) end # Specifies what the resulting value of the expression must be. - def to(value : T) forall T + def to(value) ChangeToMatcher.new(@expression, value) end # Specifies that t he resulting value must be some amount different. - def by(amount : T) forall T + def by(amount) ChangeRelativeMatcher.new(@expression, "by #{amount}") { |before, after| amount == after - before } end # Specifies that the resulting value must be at least some amount different. - def by_at_least(minimum : T) forall T + def by_at_least(minimum) ChangeRelativeMatcher.new(@expression, "by at least #{minimum}") { |before, after| minimum <= after - before } end # Specifies that the resulting value must be at most some amount different. - def by_at_most(maximum : T) forall T + def by_at_most(maximum) ChangeRelativeMatcher.new(@expression, "by at most #{maximum}") { |before, after| maximum >= after - before } end diff --git a/src/spectator/matchers/change_to_matcher.cr b/src/spectator/matchers/change_to_matcher.cr index a05587a..e29d23b 100644 --- a/src/spectator/matchers/change_to_matcher.cr +++ b/src/spectator/matchers/change_to_matcher.cr @@ -57,12 +57,12 @@ module Spectator::Matchers end # Specifies what the initial value of the expression must be. - def from(value : T) forall T + def from(value) ChangeExactMatcher.new(@expression, value, @expected) end # Specifies how much the initial value should change by. - def by(amount : T) forall T + def by(amount) ChangeExactMatcher.new(@expression, @expected - amount, @expected) end diff --git a/src/spectator/matchers/truthy_matcher.cr b/src/spectator/matchers/truthy_matcher.cr index df21d8c..09230b8 100644 --- a/src/spectator/matchers/truthy_matcher.cr +++ b/src/spectator/matchers/truthy_matcher.cr @@ -27,7 +27,7 @@ module Spectator::Matchers # ``` # expect(0).to be < 1 # ``` - def <(value : ExpectedType) forall ExpectedType + def <(value) expected = TestValue.new(value) LessThanMatcher.new(expected) end @@ -37,7 +37,7 @@ module Spectator::Matchers # ``` # expect(0).to be <= 1 # ``` - def <=(value : ExpectedType) forall ExpectedType + def <=(value) expected = TestValue.new(value) LessThanEqualMatcher.new(expected) end @@ -47,7 +47,7 @@ module Spectator::Matchers # ``` # expect(2).to be > 1 # ``` - def >(value : ExpectedType) forall ExpectedType + def >(value) expected = TestValue.new(value) GreaterThanMatcher.new(expected) end @@ -57,7 +57,7 @@ module Spectator::Matchers # ``` # expect(2).to be >= 1 # ``` - def >=(value : ExpectedType) forall ExpectedType + def >=(value) expected = TestValue.new(value) GreaterThanEqualMatcher.new(expected) end @@ -67,7 +67,7 @@ module Spectator::Matchers # ``` # expect(0).to be == 0 # ``` - def ==(value : ExpectedType) forall ExpectedType + def ==(value) expected = TestValue.new(value) EqualityMatcher.new(expected) end @@ -77,7 +77,7 @@ module Spectator::Matchers # ``` # expect(0).to be != 1 # ``` - def !=(value : ExpectedType) forall ExpectedType + def !=(value) expected = TestValue.new(value) InequalityMatcher.new(expected) end diff --git a/src/spectator/mocks/nil_method_stub.cr b/src/spectator/mocks/nil_method_stub.cr index 64e10bb..04b203a 100644 --- a/src/spectator/mocks/nil_method_stub.cr +++ b/src/spectator/mocks/nil_method_stub.cr @@ -8,7 +8,7 @@ module Spectator::Mocks nil end - def and_return(value : T) forall T + def and_return(value) ValueMethodStub.new(@name, @source, value, @args) end diff --git a/src/spectator/test_values.cr b/src/spectator/test_values.cr index d07da71..8503ca8 100644 --- a/src/spectator/test_values.cr +++ b/src/spectator/test_values.cr @@ -18,8 +18,8 @@ module Spectator # 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 : T) : TestValues forall T - wrapper = TypedValueWrapper(T).new(value) + def add(id : Symbol, name : String, value) : TestValues + wrapper = TypedValueWrapper.new(value) TestValues.new(@values.merge({ id => Entry.new(name, wrapper), })) From 6cd410c4c70713625b361566ebcd530fcbe46d88 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Nov 2019 08:32:13 -0700 Subject: [PATCH 168/205] Allow expected value to receive stub --- src/spectator/expectations/expectation_partial.cr | 1 + 1 file changed, 1 insertion(+) diff --git a/src/spectator/expectations/expectation_partial.cr b/src/spectator/expectations/expectation_partial.cr index 31606b2..568f76f 100644 --- a/src/spectator/expectations/expectation_partial.cr +++ b/src/spectator/expectations/expectation_partial.cr @@ -25,6 +25,7 @@ module Spectator::Expectations end def to(stub : Mocks::MethodStub) : Nil + Mocks::Allow.new(@actual.value).to(stub) value = TestValue.new(stub.name, stub.to_s) matcher = if (arguments = stub.arguments?) Matchers::ReceiveArgumentsMatcher.new(value, arguments) From 76c21d447ae20b07cd334c19ba0c526bd58ed0a3 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Nov 2019 08:49:09 -0700 Subject: [PATCH 169/205] Coerce operations in match? to booleans The compiler is merging null-doubles with these matchers. Methods that normally return a boolean instead return the instance (self). This causes a return type-mismatch. There should be a better alternative to this. --- src/spectator/matchers/case_matcher.cr | 2 +- src/spectator/matchers/collection_matcher.cr | 2 +- src/spectator/matchers/contain_matcher.cr | 2 +- src/spectator/matchers/empty_matcher.cr | 2 +- src/spectator/matchers/equality_matcher.cr | 2 +- src/spectator/matchers/greater_than_equal_matcher.cr | 2 +- src/spectator/matchers/greater_than_matcher.cr | 2 +- src/spectator/matchers/have_key_matcher.cr | 2 +- src/spectator/matchers/have_matcher.cr | 8 ++++---- src/spectator/matchers/have_value_matcher.cr | 2 +- src/spectator/matchers/inequality_matcher.cr | 2 +- src/spectator/matchers/less_than_equal_matcher.cr | 2 +- src/spectator/matchers/less_than_matcher.cr | 2 +- src/spectator/matchers/nil_matcher.cr | 2 +- src/spectator/matchers/range_matcher.cr | 2 +- src/spectator/matchers/reference_matcher.cr | 2 +- src/spectator/matchers/size_matcher.cr | 2 +- src/spectator/matchers/size_of_matcher.cr | 2 +- src/spectator/matchers/type_matcher.cr | 2 +- 19 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/spectator/matchers/case_matcher.cr b/src/spectator/matchers/case_matcher.cr index e7f0891..070bf2e 100644 --- a/src/spectator/matchers/case_matcher.cr +++ b/src/spectator/matchers/case_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - expected.value === actual.value + !!(expected.value === actual.value) end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/collection_matcher.cr b/src/spectator/matchers/collection_matcher.cr index 44f2ee8..154d54f 100644 --- a/src/spectator/matchers/collection_matcher.cr +++ b/src/spectator/matchers/collection_matcher.cr @@ -14,7 +14,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - expected.value.includes?(actual.value) + !!expected.value.includes?(actual.value) end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/contain_matcher.cr b/src/spectator/matchers/contain_matcher.cr index 26d3287..9dfebb0 100644 --- a/src/spectator/matchers/contain_matcher.cr +++ b/src/spectator/matchers/contain_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - expected.value.all? do |item| + !!expected.value.all? do |item| actual.value.includes?(item) end end diff --git a/src/spectator/matchers/empty_matcher.cr b/src/spectator/matchers/empty_matcher.cr index c64c6d7..3bf4659 100644 --- a/src/spectator/matchers/empty_matcher.cr +++ b/src/spectator/matchers/empty_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - actual.value.empty? + !!actual.value.empty? end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/equality_matcher.cr b/src/spectator/matchers/equality_matcher.cr index bcdcd44..911055d 100644 --- a/src/spectator/matchers/equality_matcher.cr +++ b/src/spectator/matchers/equality_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - expected.value == actual.value + !!(expected.value == actual.value) end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/greater_than_equal_matcher.cr b/src/spectator/matchers/greater_than_equal_matcher.cr index 08eb88c..b2b2dfc 100644 --- a/src/spectator/matchers/greater_than_equal_matcher.cr +++ b/src/spectator/matchers/greater_than_equal_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - actual.value >= expected.value + !!(actual.value >= expected.value) end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/greater_than_matcher.cr b/src/spectator/matchers/greater_than_matcher.cr index 5dfc90c..c69edc6 100644 --- a/src/spectator/matchers/greater_than_matcher.cr +++ b/src/spectator/matchers/greater_than_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - actual.value > expected.value + !!(actual.value > expected.value) end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/have_key_matcher.cr b/src/spectator/matchers/have_key_matcher.cr index c579930..36488e3 100644 --- a/src/spectator/matchers/have_key_matcher.cr +++ b/src/spectator/matchers/have_key_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - actual.value.has_key?(expected.value) + !!actual.value.has_key?(expected.value) end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/have_matcher.cr b/src/spectator/matchers/have_matcher.cr index d67eb35..5ec688b 100644 --- a/src/spectator/matchers/have_matcher.cr +++ b/src/spectator/matchers/have_matcher.cr @@ -23,17 +23,17 @@ module Spectator::Matchers # Checks if a `String` matches the expected values. # The `includes?` method is used for this check. - private def match_string?(value) - expected.value.all? do |item| + private def match_string?(value) : Bool + !!expected.value.all? do |item| value.includes?(item) end end # Checks if an `Enumerable` matches the expected values. # The `===` operator is used on every item. - private def match_enumerable?(value) + private def match_enumerable?(value) : Bool array = value.to_a - expected.value.all? do |item| + !!expected.value.all? do |item| array.any? do |element| item === element end diff --git a/src/spectator/matchers/have_value_matcher.cr b/src/spectator/matchers/have_value_matcher.cr index 1f3e4a9..6bc4b5a 100644 --- a/src/spectator/matchers/have_value_matcher.cr +++ b/src/spectator/matchers/have_value_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - actual.value.has_value?(expected.value) + !!actual.value.has_value?(expected.value) end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/inequality_matcher.cr b/src/spectator/matchers/inequality_matcher.cr index f721ab4..a2d65ed 100644 --- a/src/spectator/matchers/inequality_matcher.cr +++ b/src/spectator/matchers/inequality_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - expected.value != actual.value + !!(expected.value != actual.value) end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/less_than_equal_matcher.cr b/src/spectator/matchers/less_than_equal_matcher.cr index bc56dab..9b53d5e 100644 --- a/src/spectator/matchers/less_than_equal_matcher.cr +++ b/src/spectator/matchers/less_than_equal_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - actual.value <= expected.value + !!(actual.value <= expected.value) end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/less_than_matcher.cr b/src/spectator/matchers/less_than_matcher.cr index 4e14cd4..1a2811c 100644 --- a/src/spectator/matchers/less_than_matcher.cr +++ b/src/spectator/matchers/less_than_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - actual.value < expected.value + !!(actual.value < expected.value) end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/nil_matcher.cr b/src/spectator/matchers/nil_matcher.cr index 5334037..d21293b 100644 --- a/src/spectator/matchers/nil_matcher.cr +++ b/src/spectator/matchers/nil_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - actual.value.nil? + !!actual.value.nil? end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/range_matcher.cr b/src/spectator/matchers/range_matcher.cr index 32d491f..ac13310 100644 --- a/src/spectator/matchers/range_matcher.cr +++ b/src/spectator/matchers/range_matcher.cr @@ -27,7 +27,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - expected.value.includes?(actual.value) + !!expected.value.includes?(actual.value) end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/reference_matcher.cr b/src/spectator/matchers/reference_matcher.cr index 3586e14..a5a9c3c 100644 --- a/src/spectator/matchers/reference_matcher.cr +++ b/src/spectator/matchers/reference_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - expected.value.same?(actual.value) + !!expected.value.same?(actual.value) end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/size_matcher.cr b/src/spectator/matchers/size_matcher.cr index d309849..3ea6b17 100644 --- a/src/spectator/matchers/size_matcher.cr +++ b/src/spectator/matchers/size_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - expected.value == actual.value.size + !!(expected.value == actual.value.size) end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/size_of_matcher.cr b/src/spectator/matchers/size_of_matcher.cr index 0d7bc30..3c6944c 100644 --- a/src/spectator/matchers/size_of_matcher.cr +++ b/src/spectator/matchers/size_of_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - expected.value.size == actual.value.size + !!(expected.value.size == actual.value.size) end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/type_matcher.cr b/src/spectator/matchers/type_matcher.cr index 4b7aff3..1835acb 100644 --- a/src/spectator/matchers/type_matcher.cr +++ b/src/spectator/matchers/type_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - actual.value.is_a?(Expected) + !!actual.value.is_a?(Expected) end # Message displayed when the matcher isn't satisifed. From 85531fecf9c7b044ca253a7db95ed01d2165de47 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Nov 2019 09:01:03 -0700 Subject: [PATCH 170/205] Fix truthy values not being strings --- src/spectator/matchers/truthy_matcher.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/matchers/truthy_matcher.cr b/src/spectator/matchers/truthy_matcher.cr index 09230b8..bc26a6e 100644 --- a/src/spectator/matchers/truthy_matcher.cr +++ b/src/spectator/matchers/truthy_matcher.cr @@ -114,7 +114,7 @@ module Spectator::Matchers { expected: @truthy ? "Not false or nil" : "false or nil", actual: actual.value.inspect, - truthy: !!actual.value.inspect, + truthy: (!!actual.value).inspect, } end @@ -124,7 +124,7 @@ module Spectator::Matchers { expected: @truthy ? "false or nil" : "Not false or nil", actual: actual.value.inspect, - truthy: !!actual.value.inspect, + truthy: (!!actual.value).inspect, } end From ca85acefaf6c88d6b6a56ca540782d2443e82f1a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Nov 2019 09:02:57 -0700 Subject: [PATCH 171/205] Resolve issue trying to splat union of named tuples --- src/spectator/matchers/failed_match_data.cr | 4 ++++ src/spectator/matchers/standard_matcher.cr | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/spectator/matchers/failed_match_data.cr b/src/spectator/matchers/failed_match_data.cr index 3e0857a..205b31c 100644 --- a/src/spectator/matchers/failed_match_data.cr +++ b/src/spectator/matchers/failed_match_data.cr @@ -14,6 +14,10 @@ module Spectator::Matchers # Additional information from the match that can be used to debug the problem. getter values : Array(Tuple(Symbol, String)) + # Creates the match data. + def initialize(@failure_message, @values) + end + # Creates the match data. def initialize(@failure_message, **values) @values = values.to_a diff --git a/src/spectator/matchers/standard_matcher.cr b/src/spectator/matchers/standard_matcher.cr index 414e491..f29553a 100644 --- a/src/spectator/matchers/standard_matcher.cr +++ b/src/spectator/matchers/standard_matcher.cr @@ -27,7 +27,7 @@ module Spectator::Matchers if match?(actual) SuccessfulMatchData.new else - FailedMatchData.new(failure_message(actual), **values(actual)) + FailedMatchData.new(failure_message(actual), values(actual).to_a) end end @@ -42,7 +42,7 @@ module Spectator::Matchers if does_not_match?(actual) SuccessfulMatchData.new else - FailedMatchData.new(failure_message_when_negated(actual), **negated_values(actual)) + FailedMatchData.new(failure_message_when_negated(actual), negated_values(actual).to_a) end end From 23d8f4b5a6ada023f7a3251616fd85649e74dc3d Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Nov 2019 09:03:24 -0700 Subject: [PATCH 172/205] Formatting --- .../expectations/expectation_partial.cr | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/spectator/expectations/expectation_partial.cr b/src/spectator/expectations/expectation_partial.cr index 568f76f..e1cf903 100644 --- a/src/spectator/expectations/expectation_partial.cr +++ b/src/spectator/expectations/expectation_partial.cr @@ -28,10 +28,10 @@ module Spectator::Expectations Mocks::Allow.new(@actual.value).to(stub) value = TestValue.new(stub.name, stub.to_s) matcher = if (arguments = stub.arguments?) - Matchers::ReceiveArgumentsMatcher.new(value, arguments) - else - Matchers::ReceiveMatcher.new(value) - end + Matchers::ReceiveArgumentsMatcher.new(value, arguments) + else + Matchers::ReceiveMatcher.new(value) + end to_eventually(matcher) end @@ -45,10 +45,10 @@ module Spectator::Expectations def to_not(stub : Mocks::MethodStub) : Nil value = TestValue.new(stub.name, stub.to_s) matcher = if (arguments = stub.arguments?) - Matchers::ReceiveArgumentsMatcher.new(value, arguments) - else - Matchers::ReceiveMatcher.new(value) - end + Matchers::ReceiveArgumentsMatcher.new(value, arguments) + else + Matchers::ReceiveMatcher.new(value) + end to_never(matcher) end From c9fb4d2d3375511d4eaa62532a8146d10b3b175e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Nov 2019 09:06:47 -0700 Subject: [PATCH 173/205] Don't run deferred blocks if the test fails This also prevents overriding the test's original error with on that may occur in the deferred blocks. --- src/spectator/runnable_example.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/runnable_example.cr b/src/spectator/runnable_example.cr index 9bbbbed..02086d0 100644 --- a/src/spectator/runnable_example.cr +++ b/src/spectator/runnable_example.cr @@ -20,7 +20,7 @@ module Spectator context.run_before_hooks(self) run_example(result) context.run_after_hooks(self) - run_deferred(result) + run_deferred(result) unless result.error end end From 8dbfb2d6ec6c9f2a66064546bc1ee114490ac219 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Nov 2019 09:23:45 -0700 Subject: [PATCH 174/205] Revert "Allow expected value to receive stub" This reverts commit 6cd410c4c70713625b361566ebcd530fcbe46d88. Should not stub the method. The end-user needs to do this. --- src/spectator/expectations/expectation_partial.cr | 1 - 1 file changed, 1 deletion(-) diff --git a/src/spectator/expectations/expectation_partial.cr b/src/spectator/expectations/expectation_partial.cr index e1cf903..696d54c 100644 --- a/src/spectator/expectations/expectation_partial.cr +++ b/src/spectator/expectations/expectation_partial.cr @@ -25,7 +25,6 @@ module Spectator::Expectations end def to(stub : Mocks::MethodStub) : Nil - Mocks::Allow.new(@actual.value).to(stub) value = TestValue.new(stub.name, stub.to_s) matcher = if (arguments = stub.arguments?) Matchers::ReceiveArgumentsMatcher.new(value, arguments) From 186fa15a1a0d461f5edcd720ddad251ab9923933 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Nov 2019 10:06:11 -0700 Subject: [PATCH 175/205] Add method to check for stub --- src/spectator/mocks/registry.cr | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/spectator/mocks/registry.cr b/src/spectator/mocks/registry.cr index bae45f9..c27fb7c 100644 --- a/src/spectator/mocks/registry.cr +++ b/src/spectator/mocks/registry.cr @@ -41,6 +41,11 @@ module Spectator::Mocks fetch_type(type).stubs.unshift(stub) end + def stubbed?(object, method_name : Symbol) : Bool + fetch_instance(object).stubs.any? { |stub| stub.name == method_name } || + fetch_type(object.class).stubs.any? { |stub| stub.name == method_name } + end + def find_stub(object, call : GenericMethodCall(T, NT)) forall T, NT fetch_instance(object).stubs.find(&.callable?(call)) || fetch_type(object.class).stubs.find(&.callable?(call)) From 201fe614d1461ec6cd2cfef25be650b9f29c13a4 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Nov 2019 10:59:13 -0700 Subject: [PATCH 176/205] Expect messages on double, but don't stub them --- src/spectator/expectations/expectation_partial.cr | 1 + src/spectator/mocks/double.cr | 6 +++++- src/spectator/mocks/registry.cr | 9 +++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/spectator/expectations/expectation_partial.cr b/src/spectator/expectations/expectation_partial.cr index 696d54c..cf40aaf 100644 --- a/src/spectator/expectations/expectation_partial.cr +++ b/src/spectator/expectations/expectation_partial.cr @@ -25,6 +25,7 @@ module Spectator::Expectations end def to(stub : Mocks::MethodStub) : Nil + Harness.current.mocks.expect(@actual.value, stub.name) value = TestValue.new(stub.name, stub.to_s) matcher = if (arguments = stub.arguments?) Matchers::ReceiveArgumentsMatcher.new(value, arguments) diff --git a/src/spectator/mocks/double.cr b/src/spectator/mocks/double.cr index 4af8cf1..f6e3c67 100644 --- a/src/spectator/mocks/double.cr +++ b/src/spectator/mocks/double.cr @@ -62,7 +62,10 @@ module Spectator::Mocks {% if body && !body.is_a?(Nop) %} {{body.body}} {% else %} - raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{name}}") + unless ::Spectator::Harness.current.mocks.expected?(self, {{name.symbolize}}) + raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{name}}") + end + # This code shouldn't be reached, but makes the compiler happy to have a matching return type. {% if definition.is_a?(TypeDeclaration) %} %x = uninitialized {{definition.type}} @@ -75,6 +78,7 @@ module Spectator::Mocks macro method_missing(call) return self if @null + return self if ::Spectator::Harness.current.mocks.expected?(self, {{call.name.symbolize}}) raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{call.name}}") end diff --git a/src/spectator/mocks/registry.cr b/src/spectator/mocks/registry.cr index c27fb7c..d094052 100644 --- a/src/spectator/mocks/registry.cr +++ b/src/spectator/mocks/registry.cr @@ -5,6 +5,7 @@ module Spectator::Mocks private struct Entry getter stubs = Deque(MethodStub).new getter calls = Deque(MethodCall).new + getter expected = Set(Symbol).new end @all_instances = {} of String => Entry @@ -64,6 +65,14 @@ module Spectator::Mocks fetch_type(type).calls.select { |call| call.name == method_name } end + def expected?(object, method_name : Symbol) : Bool + fetch_instance(object).expected.includes?(method_name) + end + + def expect(object, method_name : Symbol) : Nil + fetch_instance(object).expected.add(method_name) + end + private def fetch_instance(object) key = unique_key(object) if @entries.has_key?(key) From f0bfd8b6d4a8f5248772e88c38980b2f8c76c0e2 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Nov 2019 13:18:09 -0700 Subject: [PATCH 177/205] Switch to run-time error for unsupported negation --- src/spectator/matchers/standard_matcher.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/matchers/standard_matcher.cr b/src/spectator/matchers/standard_matcher.cr index f29553a..2a01ab3 100644 --- a/src/spectator/matchers/standard_matcher.cr +++ b/src/spectator/matchers/standard_matcher.cr @@ -66,7 +66,7 @@ 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 - {% raise "Negation with #{@type.name} is not supported." %} + raise "Negation with #{self.class} is not supported." end # Checks whether the matcher is satisifed with the expression given to it. From d9d30c57d090c2a4d1e6fe7b3a6fcd97d99cdcf1 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Nov 2019 15:14:38 -0700 Subject: [PATCH 178/205] Merge ReceiveArgumentsMatcher and ReceiveMatcher Finally found the issue causing other matchers derived from StandardMatcher to be "leaked" doubles and mocks indirectly. The if-condition in ExpectationPartial#to and #to_not caused the matcher to be given the StandardMatcher type instead of a union type. This lead to really strange compilation errors and wasted a lot of hours. --- .../expectations/expectation_partial.cr | 12 +- .../matchers/receive_arguments_matcher.cr | 104 ------------------ src/spectator/matchers/receive_matcher.cr | 24 ++-- 3 files changed, 15 insertions(+), 125 deletions(-) delete mode 100644 src/spectator/matchers/receive_arguments_matcher.cr diff --git a/src/spectator/expectations/expectation_partial.cr b/src/spectator/expectations/expectation_partial.cr index cf40aaf..7927bd2 100644 --- a/src/spectator/expectations/expectation_partial.cr +++ b/src/spectator/expectations/expectation_partial.cr @@ -27,11 +27,7 @@ module Spectator::Expectations def to(stub : Mocks::MethodStub) : Nil Harness.current.mocks.expect(@actual.value, stub.name) value = TestValue.new(stub.name, stub.to_s) - matcher = if (arguments = stub.arguments?) - Matchers::ReceiveArgumentsMatcher.new(value, arguments) - else - Matchers::ReceiveMatcher.new(value) - end + matcher = Matchers::ReceiveMatcher.new(value, stub.arguments?) to_eventually(matcher) end @@ -44,11 +40,7 @@ module Spectator::Expectations def to_not(stub : Mocks::MethodStub) : Nil value = TestValue.new(stub.name, stub.to_s) - matcher = if (arguments = stub.arguments?) - Matchers::ReceiveArgumentsMatcher.new(value, arguments) - else - Matchers::ReceiveMatcher.new(value) - end + matcher = Matchers::ReceiveMatcher.new(value, stub.arguments?) to_never(matcher) end diff --git a/src/spectator/matchers/receive_arguments_matcher.cr b/src/spectator/matchers/receive_arguments_matcher.cr deleted file mode 100644 index e4053cc..0000000 --- a/src/spectator/matchers/receive_arguments_matcher.cr +++ /dev/null @@ -1,104 +0,0 @@ -require "../mocks" -require "./standard_matcher" - -module Spectator::Matchers - struct ReceiveArgumentsMatcher < StandardMatcher - alias Range = ::Range(Int32, Int32) | ::Range(Nil, Int32) | ::Range(Int32, Nil) - - def initialize(@expected : TestExpression(Symbol), @args : Mocks::Arguments, @range : Range? = nil) - end - - def description : String - range = @range - "received message #{@expected.label} #{range ? "#{humanize_range(range)} time(s)" : "At least once"} with #{@args}" - end - - def match?(actual : TestExpression(T)) : Bool forall T - calls = Harness.current.mocks.calls_for(actual.value, @expected.value).select { |call| @args === call.args } - if (range = @range) - range.includes?(calls.size) - else - !calls.empty? - end - end - - def failure_message(actual : TestExpression(T)) : String forall T - range = @range - "#{actual.label} did not receive #{@expected.label} #{range ? "#{humanize_range(range)} time(s)" : "at least once"} with #{@args}" - end - - def values(actual : TestExpression(T)) forall T - calls = Harness.current.mocks.calls_for(actual.value, @expected.value).select { |call| @args === call.args } - range = @range - { - expected: "#{range ? "#{humanize_range(range)} time(s)" : "At least once"} with #{@args}", - received: "#{calls.size} time(s)", - } - end - - def once - ReceiveArgumentsMatcher.new(@expected, @args, (1..1)) - end - - def twice - ReceiveArgumentsMatcher.new(@expected, @args, (2..2)) - end - - def exactly(count) - Count.new(@expected, (count..count)) - end - - def at_least(count) - Count.new(@expected, (count..)) - end - - def at_most(count) - Count.new(@expected, (..count)) - end - - def at_least_once - at_least(1).times - end - - def at_least_twice - at_least(2).times - end - - def at_most_once - at_most(1).times - end - - def at_most_twice - at_most(2).times - end - - def humanize_range(range : Range) - if (min = range.begin) - if (max = range.end) - if min == max - min - else - "#{min} to #{max}" - end - else - "At least #{min}" - end - else - if (max = range.end) - "At most #{max}" - else - raise "Unexpected endless range" - end - end - end - - private struct Count - def initialize(@expected : TestExpression(Symbol), @args : Mocks::Arguments, @range : Range) - end - - def times - ReceiveArgumentsMatcher.new(@expected, @args, @range) - end - end - end -end diff --git a/src/spectator/matchers/receive_matcher.cr b/src/spectator/matchers/receive_matcher.cr index 376047a..8cdb1ed 100644 --- a/src/spectator/matchers/receive_matcher.cr +++ b/src/spectator/matchers/receive_matcher.cr @@ -1,20 +1,21 @@ -require "../mocks/double" +require "../mocks" require "./standard_matcher" module Spectator::Matchers struct ReceiveMatcher < StandardMatcher alias Range = ::Range(Int32, Int32) | ::Range(Nil, Int32) | ::Range(Int32, Nil) - def initialize(@expected : TestExpression(Symbol), @range : Range? = nil) + def initialize(@expected : TestExpression(Symbol), @args : Mocks::Arguments? = nil, @range : Range? = nil) end def description : String range = @range - "received message #{@expected.label} #{range ? "#{humanize_range(range)} time(s)" : "at least once"} with any arguments" + "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 calls = Harness.current.mocks.calls_for(actual.value, @expected.value) + calls.select! { |call| @args === call.args } if @args if (range = @range) range.includes?(calls.size) else @@ -24,29 +25,30 @@ module Spectator::Matchers def failure_message(actual : TestExpression(T)) : String forall T range = @range - "#{actual.label} did not receive #{@expected.label} #{range ? "#{humanize_range(range)} time(s)" : "at least once"}" + "#{actual.label} did not receive #{@expected.label} #{range ? "#{humanize_range(range)} time(s)" : "at least once"} with #{@args || "any arguments"}" end def values(actual : TestExpression(T)) forall T calls = Harness.current.mocks.calls_for(actual.value, @expected.value) + calls.select! { |call| @args === call.args } if @args range = @range { - expected: "#{range ? "#{humanize_range(range)} time(s)" : "At least once"} with any arguments", - received: "#{calls.size} time(s) with any arguments", + expected: "#{range ? "#{humanize_range(range)} time(s)" : "At least once"} with #{@args || "any arguments"}", + received: "#{calls.size} time(s)", } end def with(*args, **opts) args = Mocks::GenericArguments.new(args, opts) - ReceiveArgumentsMatcher.new(@expected, args, @range) + ReceiveMatcher.new(@expected, args, @range) end def once - ReceiveMatcher.new(@expected, (1..1)) + ReceiveMatcher.new(@expected, @args, (1..1)) end def twice - ReceiveMatcher.new(@expected, (2..2)) + ReceiveMatcher.new(@expected, @args, (2..2)) end def exactly(count) @@ -98,11 +100,11 @@ module Spectator::Matchers end private struct Count - def initialize(@expected : TestExpression(Symbol), @range : Range) + def initialize(@expected : TestExpression(Symbol), @args : Mocks::Arguments?, @range : Range) end def times - ReceiveMatcher.new(@expected, @range) + ReceiveMatcher.new(@expected, @args, @range) end end end From a8e0f466458d280de1365b55060324d3d71237ae Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Nov 2019 15:15:53 -0700 Subject: [PATCH 179/205] Revert "Coerce operations in match? to booleans" This reverts commit 76c21d447ae20b07cd334c19ba0c526bd58ed0a3. Should no longer be needed since mocks and doubles should indirectly leak into StandardMatcher types. --- src/spectator/matchers/case_matcher.cr | 2 +- src/spectator/matchers/collection_matcher.cr | 2 +- src/spectator/matchers/contain_matcher.cr | 2 +- src/spectator/matchers/empty_matcher.cr | 2 +- src/spectator/matchers/equality_matcher.cr | 2 +- src/spectator/matchers/greater_than_equal_matcher.cr | 2 +- src/spectator/matchers/greater_than_matcher.cr | 2 +- src/spectator/matchers/have_key_matcher.cr | 2 +- src/spectator/matchers/have_matcher.cr | 8 ++++---- src/spectator/matchers/have_value_matcher.cr | 2 +- src/spectator/matchers/inequality_matcher.cr | 2 +- src/spectator/matchers/less_than_equal_matcher.cr | 2 +- src/spectator/matchers/less_than_matcher.cr | 2 +- src/spectator/matchers/nil_matcher.cr | 2 +- src/spectator/matchers/range_matcher.cr | 2 +- src/spectator/matchers/reference_matcher.cr | 2 +- src/spectator/matchers/size_matcher.cr | 2 +- src/spectator/matchers/size_of_matcher.cr | 2 +- src/spectator/matchers/type_matcher.cr | 2 +- 19 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/spectator/matchers/case_matcher.cr b/src/spectator/matchers/case_matcher.cr index 070bf2e..e7f0891 100644 --- a/src/spectator/matchers/case_matcher.cr +++ b/src/spectator/matchers/case_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - !!(expected.value === actual.value) + expected.value === actual.value end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/collection_matcher.cr b/src/spectator/matchers/collection_matcher.cr index 154d54f..44f2ee8 100644 --- a/src/spectator/matchers/collection_matcher.cr +++ b/src/spectator/matchers/collection_matcher.cr @@ -14,7 +14,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - !!expected.value.includes?(actual.value) + expected.value.includes?(actual.value) end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/contain_matcher.cr b/src/spectator/matchers/contain_matcher.cr index 9dfebb0..26d3287 100644 --- a/src/spectator/matchers/contain_matcher.cr +++ b/src/spectator/matchers/contain_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - !!expected.value.all? do |item| + expected.value.all? do |item| actual.value.includes?(item) end end diff --git a/src/spectator/matchers/empty_matcher.cr b/src/spectator/matchers/empty_matcher.cr index 3bf4659..c64c6d7 100644 --- a/src/spectator/matchers/empty_matcher.cr +++ b/src/spectator/matchers/empty_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - !!actual.value.empty? + actual.value.empty? end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/equality_matcher.cr b/src/spectator/matchers/equality_matcher.cr index 911055d..bcdcd44 100644 --- a/src/spectator/matchers/equality_matcher.cr +++ b/src/spectator/matchers/equality_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - !!(expected.value == actual.value) + expected.value == actual.value end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/greater_than_equal_matcher.cr b/src/spectator/matchers/greater_than_equal_matcher.cr index b2b2dfc..08eb88c 100644 --- a/src/spectator/matchers/greater_than_equal_matcher.cr +++ b/src/spectator/matchers/greater_than_equal_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - !!(actual.value >= expected.value) + actual.value >= expected.value end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/greater_than_matcher.cr b/src/spectator/matchers/greater_than_matcher.cr index c69edc6..5dfc90c 100644 --- a/src/spectator/matchers/greater_than_matcher.cr +++ b/src/spectator/matchers/greater_than_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - !!(actual.value > expected.value) + actual.value > expected.value end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/have_key_matcher.cr b/src/spectator/matchers/have_key_matcher.cr index 36488e3..c579930 100644 --- a/src/spectator/matchers/have_key_matcher.cr +++ b/src/spectator/matchers/have_key_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - !!actual.value.has_key?(expected.value) + actual.value.has_key?(expected.value) end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/have_matcher.cr b/src/spectator/matchers/have_matcher.cr index 5ec688b..d67eb35 100644 --- a/src/spectator/matchers/have_matcher.cr +++ b/src/spectator/matchers/have_matcher.cr @@ -23,17 +23,17 @@ module Spectator::Matchers # Checks if a `String` matches the expected values. # The `includes?` method is used for this check. - private def match_string?(value) : Bool - !!expected.value.all? do |item| + private def match_string?(value) + expected.value.all? do |item| value.includes?(item) end end # Checks if an `Enumerable` matches the expected values. # The `===` operator is used on every item. - private def match_enumerable?(value) : Bool + private def match_enumerable?(value) array = value.to_a - !!expected.value.all? do |item| + expected.value.all? do |item| array.any? do |element| item === element end diff --git a/src/spectator/matchers/have_value_matcher.cr b/src/spectator/matchers/have_value_matcher.cr index 6bc4b5a..1f3e4a9 100644 --- a/src/spectator/matchers/have_value_matcher.cr +++ b/src/spectator/matchers/have_value_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - !!actual.value.has_value?(expected.value) + actual.value.has_value?(expected.value) end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/inequality_matcher.cr b/src/spectator/matchers/inequality_matcher.cr index a2d65ed..f721ab4 100644 --- a/src/spectator/matchers/inequality_matcher.cr +++ b/src/spectator/matchers/inequality_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - !!(expected.value != actual.value) + expected.value != actual.value end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/less_than_equal_matcher.cr b/src/spectator/matchers/less_than_equal_matcher.cr index 9b53d5e..bc56dab 100644 --- a/src/spectator/matchers/less_than_equal_matcher.cr +++ b/src/spectator/matchers/less_than_equal_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - !!(actual.value <= expected.value) + actual.value <= expected.value end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/less_than_matcher.cr b/src/spectator/matchers/less_than_matcher.cr index 1a2811c..4e14cd4 100644 --- a/src/spectator/matchers/less_than_matcher.cr +++ b/src/spectator/matchers/less_than_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - !!(actual.value < expected.value) + actual.value < expected.value end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/nil_matcher.cr b/src/spectator/matchers/nil_matcher.cr index d21293b..5334037 100644 --- a/src/spectator/matchers/nil_matcher.cr +++ b/src/spectator/matchers/nil_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - !!actual.value.nil? + actual.value.nil? end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/range_matcher.cr b/src/spectator/matchers/range_matcher.cr index ac13310..32d491f 100644 --- a/src/spectator/matchers/range_matcher.cr +++ b/src/spectator/matchers/range_matcher.cr @@ -27,7 +27,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - !!expected.value.includes?(actual.value) + expected.value.includes?(actual.value) end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/reference_matcher.cr b/src/spectator/matchers/reference_matcher.cr index a5a9c3c..3586e14 100644 --- a/src/spectator/matchers/reference_matcher.cr +++ b/src/spectator/matchers/reference_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - !!expected.value.same?(actual.value) + expected.value.same?(actual.value) end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/size_matcher.cr b/src/spectator/matchers/size_matcher.cr index 3ea6b17..d309849 100644 --- a/src/spectator/matchers/size_matcher.cr +++ b/src/spectator/matchers/size_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - !!(expected.value == actual.value.size) + expected.value == actual.value.size end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/size_of_matcher.cr b/src/spectator/matchers/size_of_matcher.cr index 3c6944c..0d7bc30 100644 --- a/src/spectator/matchers/size_of_matcher.cr +++ b/src/spectator/matchers/size_of_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - !!(expected.value.size == actual.value.size) + expected.value.size == actual.value.size end # Message displayed when the matcher isn't satisifed. diff --git a/src/spectator/matchers/type_matcher.cr b/src/spectator/matchers/type_matcher.cr index 1835acb..4b7aff3 100644 --- a/src/spectator/matchers/type_matcher.cr +++ b/src/spectator/matchers/type_matcher.cr @@ -13,7 +13,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T - !!actual.value.is_a?(Expected) + actual.value.is_a?(Expected) end # Message displayed when the matcher isn't satisifed. From a15e2a97b18d12ec49904ae7f0f4c7334eff4b68 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Nov 2019 15:24:23 -0700 Subject: [PATCH 180/205] Allow deferred expectation of multiple stubs --- src/spectator/expectations/expectation_partial.cr | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/spectator/expectations/expectation_partial.cr b/src/spectator/expectations/expectation_partial.cr index 7927bd2..00e26dd 100644 --- a/src/spectator/expectations/expectation_partial.cr +++ b/src/spectator/expectations/expectation_partial.cr @@ -31,6 +31,10 @@ module Spectator::Expectations 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 @@ -44,6 +48,10 @@ module Spectator::Expectations 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 From ac9b3ad1fec85fdaa36ecc85562220afba867212 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 16 Nov 2019 21:27:18 -0700 Subject: [PATCH 181/205] Implement expect_any_instance_of --- src/spectator/dsl/mocks.cr | 5 + .../matchers/receive_type_matcher.cr | 111 ++++++++++++++++++ src/spectator/mocks/expect_any_instance.cr | 23 ++++ src/spectator/mocks/registry.cr | 9 +- 4 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 src/spectator/matchers/receive_type_matcher.cr create mode 100644 src/spectator/mocks/expect_any_instance.cr diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index e155960..fca156d 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -121,6 +121,11 @@ module Spectator::DSL 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__) %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) ::Spectator::Mocks::NilMethodStub.new({{method_name.id.symbolize}}, %source) diff --git a/src/spectator/matchers/receive_type_matcher.cr b/src/spectator/matchers/receive_type_matcher.cr new file mode 100644 index 0000000..b5ffce4 --- /dev/null +++ b/src/spectator/matchers/receive_type_matcher.cr @@ -0,0 +1,111 @@ +require "../mocks" +require "./standard_matcher" + +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) + end + + def description : String + range = @range + "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 + calls = Harness.current.mocks.calls_for_type(actual.value, @expected.value) + calls.select! { |call| @args === call.args } if @args + if (range = @range) + range.includes?(calls.size) + else + !calls.empty? + end + end + + def failure_message(actual : TestExpression(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 values(actual : TestExpression(T)) forall T + calls = Harness.current.mocks.calls_for_type(T, @expected.value) + calls.select! { |call| @args === call.args } if @args + range = @range + { + expected: "#{range ? "#{humanize_range(range)} time(s)" : "At least once"} with #{@args || "any arguments"}", + received: "#{calls.size} time(s)", + } + end + + def with(*args, **opts) + args = Mocks::GenericArguments.new(args, opts) + ReceiveTypeMatcher.new(@expected, args, @range) + end + + def once + ReceiveTypeMatcher.new(@expected, @args, (1..1)) + end + + def twice + ReceiveTypeMatcher.new(@expected, @args, (2..2)) + end + + def exactly(count) + Count.new(@expected, (count..count)) + end + + def at_least(count) + Count.new(@expected, (count..)) + end + + def at_most(count) + Count.new(@expected, (..count)) + end + + def at_least_once + at_least(1).times + end + + def at_least_twice + at_least(2).times + end + + def at_most_once + at_most(1).times + end + + def at_most_twice + at_most(2).times + end + + def humanize_range(range : Range) + if (min = range.begin) + if (max = range.end) + if min == max + min + else + "#{min} to #{max}" + end + else + "At least #{min}" + end + else + if (max = range.end) + "At most #{max}" + else + raise "Unexpected endless range" + end + end + end + + private struct Count + def initialize(@expected : TestExpression(Symbol), @args : Mocks::Arguments?, @range : Range) + end + + def times + ReceiveTypeMatcher.new(@expected, @args, @range) + end + end + end +end diff --git a/src/spectator/mocks/expect_any_instance.cr b/src/spectator/mocks/expect_any_instance.cr new file mode 100644 index 0000000..c51c0e8 --- /dev/null +++ b/src/spectator/mocks/expect_any_instance.cr @@ -0,0 +1,23 @@ +require "./registry" + +module Spectator::Mocks + struct ExpectAnyInstance(T) + def initialize(@source : Source) + end + + def to(stub : MethodStub) : Nil + actual = TestValue.new(T) + Harness.current.mocks.expect(T, stub.name) + value = TestValue.new(stub.name, stub.to_s) + matcher = Matchers::ReceiveTypeMatcher.new(value, stub.arguments?) + partial = Expectations::ExpectationPartial.new(actual, @source) + partial.to_eventually(matcher) + end + + def to(stubs : Enumerable(MethodStub)) : Nil + stubs.each do |stub| + to(stub) + end + end + end +end diff --git a/src/spectator/mocks/registry.cr b/src/spectator/mocks/registry.cr index d094052..e01d186 100644 --- a/src/spectator/mocks/registry.cr +++ b/src/spectator/mocks/registry.cr @@ -61,18 +61,23 @@ module Spectator::Mocks fetch_instance(object).calls.select { |call| call.name == method_name } end - def calls_for_type(type, method_name : Symbol) + def calls_for_type(type : T.class, method_name : Symbol) forall T fetch_type(type).calls.select { |call| call.name == method_name } end def expected?(object, method_name : Symbol) : Bool - fetch_instance(object).expected.includes?(method_name) + fetch_instance(object).expected.includes?(method_name) || + fetch_type(object.class).expected.includes?(method_name) end def expect(object, method_name : Symbol) : Nil fetch_instance(object).expected.add(method_name) end + def expect(type : T.class, method_name : Symbol) : Nil forall T + fetch_type(type).expected.add(method_name) + end + private def fetch_instance(object) key = unique_key(object) if @entries.has_key?(key) From 45fe6217b4553c9a9d58c56d117a79a9e1acc4c0 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 17 Nov 2019 09:42:19 -0700 Subject: [PATCH 182/205] Missing args --- src/spectator/matchers/receive_matcher.cr | 6 +++--- src/spectator/matchers/receive_type_matcher.cr | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/spectator/matchers/receive_matcher.cr b/src/spectator/matchers/receive_matcher.cr index 8cdb1ed..bb5fb00 100644 --- a/src/spectator/matchers/receive_matcher.cr +++ b/src/spectator/matchers/receive_matcher.cr @@ -52,15 +52,15 @@ module Spectator::Matchers end def exactly(count) - Count.new(@expected, (count..count)) + Count.new(@expected, @args, (count..count)) end def at_least(count) - Count.new(@expected, (count..)) + Count.new(@expected, @args, (count..)) end def at_most(count) - Count.new(@expected, (..count)) + Count.new(@expected, @args, (..count)) end def at_least_once diff --git a/src/spectator/matchers/receive_type_matcher.cr b/src/spectator/matchers/receive_type_matcher.cr index b5ffce4..f317ea6 100644 --- a/src/spectator/matchers/receive_type_matcher.cr +++ b/src/spectator/matchers/receive_type_matcher.cr @@ -52,15 +52,15 @@ module Spectator::Matchers end def exactly(count) - Count.new(@expected, (count..count)) + Count.new(@expected, @args, (count..count)) end def at_least(count) - Count.new(@expected, (count..)) + Count.new(@expected, @args, (count..)) end def at_most(count) - Count.new(@expected, (..count)) + Count.new(@expected, @args, (..count)) end def at_least_once From 5da231a5e87df0b047e4ea182caa765963373fb0 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 17 Nov 2019 10:14:03 -0700 Subject: [PATCH 183/205] Check expectations for anonymous double --- src/spectator/mocks/anonymous_double.cr | 1 + 1 file changed, 1 insertion(+) diff --git a/src/spectator/mocks/anonymous_double.cr b/src/spectator/mocks/anonymous_double.cr index d1aea1d..d2e67c9 100644 --- a/src/spectator/mocks/anonymous_double.cr +++ b/src/spectator/mocks/anonymous_double.cr @@ -15,6 +15,7 @@ module Spectator::Mocks stub.call!(args, typeof(@values.fetch({{call.name.symbolize}}) { raise })) else @values.fetch({{call.name.symbolize}}) do + return nil if ::Spectator::Harness.current.mocks.expected?(self, {{call.name.symbolize}}) raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{call.name}}") end end From a63d3d05a53bba181f8cabfe526030869ab2afde Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 17 Nov 2019 10:32:37 -0700 Subject: [PATCH 184/205] Negation for receive matchers --- src/spectator/matchers/receive_matcher.cr | 15 +++++++++++++++ src/spectator/matchers/receive_type_matcher.cr | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/spectator/matchers/receive_matcher.cr b/src/spectator/matchers/receive_matcher.cr index bb5fb00..9a96f82 100644 --- a/src/spectator/matchers/receive_matcher.cr +++ b/src/spectator/matchers/receive_matcher.cr @@ -28,6 +28,11 @@ module Spectator::Matchers "#{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) : String + 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 calls = Harness.current.mocks.calls_for(actual.value, @expected.value) calls.select! { |call| @args === call.args } if @args @@ -38,6 +43,16 @@ module Spectator::Matchers } end + def negated_values(actual : TestExpression(T)) forall T + calls = Harness.current.mocks.calls_for(actual.value, @expected.value) + calls.select! { |call| @args === call.args } if @args + range = @range + { + expected: "#{range ? "Not #{humanize_range(range)} time(s)" : "Never"} with #{@args || "any arguments"}", + received: "#{calls.size} time(s)", + } + end + def with(*args, **opts) args = Mocks::GenericArguments.new(args, opts) ReceiveMatcher.new(@expected, args, @range) diff --git a/src/spectator/matchers/receive_type_matcher.cr b/src/spectator/matchers/receive_type_matcher.cr index f317ea6..c716f05 100644 --- a/src/spectator/matchers/receive_type_matcher.cr +++ b/src/spectator/matchers/receive_type_matcher.cr @@ -28,6 +28,11 @@ module Spectator::Matchers "#{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 + 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 calls = Harness.current.mocks.calls_for_type(T, @expected.value) calls.select! { |call| @args === call.args } if @args @@ -38,6 +43,16 @@ module Spectator::Matchers } end + def negated_values(actual : TestExpression(T)) forall T + calls = Harness.current.mocks.calls_for_type(T, @expected.value) + calls.select! { |call| @args === call.args } if @args + range = @range + { + expected: "#{range ? "Not #{humanize_range(range)} time(s)" : "Never"} with #{@args || "any arguments"}", + received: "#{calls.size} time(s)", + } + end + def with(*args, **opts) args = Mocks::GenericArguments.new(args, opts) ReceiveTypeMatcher.new(@expected, args, @range) From 1a3f663b7050b7aec3017f190052944410341569 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 17 Nov 2019 10:47:51 -0700 Subject: [PATCH 185/205] Name is optional for doubles --- src/spectator/dsl/mocks.cr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index fca156d..7470f16 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -1,7 +1,7 @@ require "../mocks" module Spectator::DSL - macro double(name, **stubs, &block) + macro double(name = "Anonymous", **stubs, &block) {% if name.is_a?(StringLiteral) %} anonymous_double({{name}}, {{stubs.double_splat}}) {% else %} @@ -44,7 +44,7 @@ module Spectator::DSL end end - def anonymous_double(name : String, **stubs) + def anonymous_double(name = "Anonymous", **stubs) Mocks::AnonymousDouble.new(name, stubs) end @@ -91,7 +91,7 @@ module Spectator::DSL end end - def anonymous_null_double(name : String, **stubs) + def anonymous_null_double(name = "Anonymous", **stubs) AnonymousNullDouble.new(name, stubs) end From 6e287f864bffc97c1d24264f5122adf4fe7e4be2 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 17 Nov 2019 12:26:02 -0700 Subject: [PATCH 186/205] Implement returning multiple values --- .../mocks/multi_value_method_stub.cr | 19 +++++++++++++++++++ src/spectator/mocks/nil_method_stub.cr | 8 ++++++++ 2 files changed, 27 insertions(+) create mode 100644 src/spectator/mocks/multi_value_method_stub.cr diff --git a/src/spectator/mocks/multi_value_method_stub.cr b/src/spectator/mocks/multi_value_method_stub.cr new file mode 100644 index 0000000..6ab3781 --- /dev/null +++ b/src/spectator/mocks/multi_value_method_stub.cr @@ -0,0 +1,19 @@ +require "./generic_arguments" +require "./generic_method_stub" + +module Spectator::Mocks + class MultiValueMethodStub(ReturnType) < GenericMethodStub(ReturnType) + @index = 0 + + def initialize(name, source, @values : ReturnType, args = nil) + super(name, source, args) + raise ArgumentError.new("Values must have at least one item") if @values.size < 1 + end + + def call(_args : GenericArguments(T2, NT2), rt : RT.class) forall T2, NT2, RT + value = @values[@index] + @index += 1 if @index < @values.size - 1 + value + end + end +end diff --git a/src/spectator/mocks/nil_method_stub.cr b/src/spectator/mocks/nil_method_stub.cr index 04b203a..6fef3a2 100644 --- a/src/spectator/mocks/nil_method_stub.cr +++ b/src/spectator/mocks/nil_method_stub.cr @@ -8,10 +8,18 @@ module Spectator::Mocks nil end + def and_return + self + end + def and_return(value) ValueMethodStub.new(@name, @source, value, @args) end + def and_return(*values) + MultiValueMethodStub.new(@name, @source, values.to_a, @args) + end + def with(*args : *T, **opts : **NT) forall T, NT args = GenericArguments.new(args, opts) NilMethodStub.new(@name, @source, args) From b896a7f1d5e1d06f244013389c546087f4528bc4 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 17 Nov 2019 12:53:18 -0700 Subject: [PATCH 187/205] Implement stub that raises --- src/spectator/mocks/exception_method_stub.cr | 14 ++++++++++++++ src/spectator/mocks/nil_method_stub.cr | 16 ++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 src/spectator/mocks/exception_method_stub.cr diff --git a/src/spectator/mocks/exception_method_stub.cr b/src/spectator/mocks/exception_method_stub.cr new file mode 100644 index 0000000..062e47f --- /dev/null +++ b/src/spectator/mocks/exception_method_stub.cr @@ -0,0 +1,14 @@ +require "./generic_arguments" +require "./generic_method_stub" + +module Spectator::Mocks + class ExceptionMethodStub(ExceptionType) < GenericMethodStub(Nil) + def initialize(name, source, @exception : ExceptionType, args = nil) + super(name, source, args) + end + + def call(_args : GenericArguments(T2, NT2), rt : RT.class) forall T2, NT2, RT + raise @exception + end + end +end diff --git a/src/spectator/mocks/nil_method_stub.cr b/src/spectator/mocks/nil_method_stub.cr index 6fef3a2..43a6f82 100644 --- a/src/spectator/mocks/nil_method_stub.cr +++ b/src/spectator/mocks/nil_method_stub.cr @@ -20,6 +20,22 @@ module Spectator::Mocks MultiValueMethodStub.new(@name, @source, values.to_a, @args) end + def and_raise(exception_type : Exception.class) + ExceptionMethodStub.new(@name, @source, exception_type.new, @args) + end + + def and_raise(exception : Exception) + ExceptionMethodStub.new(@name, @source, exception, @args) + end + + def and_raise(message : String) + ExceptionMethodStub.new(@name, @source, Exception.new(message), @args) + end + + def and_raise(exception_type : Exception.class, *args) forall T + ExceptionMethodStub.new(@name, @source, 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) From e49bd0d57ad6235ea3f32e77ba50becbddbbdbed Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 17 Nov 2019 13:12:39 -0700 Subject: [PATCH 188/205] Initial support for and_yield --- src/spectator/mocks/anonymous_double.cr | 6 +++++- src/spectator/mocks/anonymous_null_double.cr | 6 +++++- src/spectator/mocks/double.cr | 4 ++-- src/spectator/mocks/method_stub.cr | 13 +++++++++++++ src/spectator/mocks/nil_method_stub.cr | 4 ++++ src/spectator/mocks/yield_method_stub.cr | 19 +++++++++++++++++++ 6 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 src/spectator/mocks/yield_method_stub.cr diff --git a/src/spectator/mocks/anonymous_double.cr b/src/spectator/mocks/anonymous_double.cr index d2e67c9..f04d194 100644 --- a/src/spectator/mocks/anonymous_double.cr +++ b/src/spectator/mocks/anonymous_double.cr @@ -12,7 +12,11 @@ module Spectator::Mocks call = ::Spectator::Mocks::GenericMethodCall.new({{call.name.symbolize}}, args) ::Spectator::Harness.current.mocks.record_call(self, call) if (stub = ::Spectator::Harness.current.mocks.find_stub(self, call)) - stub.call!(args, typeof(@values.fetch({{call.name.symbolize}}) { raise })) + {% if call.block.is_a?(Nop) %} + stub.call!(args, typeof(@values.fetch({{call.name.symbolize}}) { raise })) + {% else %} + stub.call!(args, typeof(@values.fetch({{call.name.symbolize}}) { raise })) { |*ya| yield *ya } + {% end %} else @values.fetch({{call.name.symbolize}}) do return nil if ::Spectator::Harness.current.mocks.expected?(self, {{call.name.symbolize}}) diff --git a/src/spectator/mocks/anonymous_null_double.cr b/src/spectator/mocks/anonymous_null_double.cr index 3da8122..960962e 100644 --- a/src/spectator/mocks/anonymous_null_double.cr +++ b/src/spectator/mocks/anonymous_null_double.cr @@ -8,7 +8,11 @@ module Spectator::Mocks call = ::Spectator::Mocks::GenericMethodCall.new({{call.name.symbolize}}, args) ::Spectator::Harness.current.mocks.record_call(self, call) if (stub = ::Spectator::Harness.current.mocks.find_stub(self, call)) - stub.call!(args, typeof(@values.fetch({{call.name.symbolize}}) { self })) + {% if call.block.is_a?(Nop) %} + stub.call!(args, typeof(@values.fetch({{call.name.symbolize}}) { self })) + {% else %} + stub.call!(args, typeof(@values.fetch({{call.name.symbolize}}) { self })) { |*ya| yield *ya } + {% end %} else @values.fetch({{call.name.symbolize}}) { self } end diff --git a/src/spectator/mocks/double.cr b/src/spectator/mocks/double.cr index f6e3c67..20d5f6f 100644 --- a/src/spectator/mocks/double.cr +++ b/src/spectator/mocks/double.cr @@ -45,12 +45,12 @@ module Spectator::Mocks end end - def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} + def {{name}}({{params.splat}} &block){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) ::Spectator::Harness.current.mocks.record_call(self, %call) if (%stub = ::Spectator::Harness.current.mocks.find_stub(self, %call)) - %stub.call!(%args, typeof(%method({{args.splat}}) { |*%ya| yield *%ya })) + %stub.call!(%args, typeof(%method({{args.splat}}) { |*%ya| yield *%ya })) { |*%ya| yield *%ya } else %method({{args.splat}}) do |*%yield_args| yield *%yield_args diff --git a/src/spectator/mocks/method_stub.cr b/src/spectator/mocks/method_stub.cr index a297d14..80050b7 100644 --- a/src/spectator/mocks/method_stub.cr +++ b/src/spectator/mocks/method_stub.cr @@ -16,6 +16,10 @@ module Spectator::Mocks abstract def call(args : GenericArguments(T, NT), rt : RT.class) forall T, NT, RT + def call(args : GenericArguments(T, NT), rt : RT.class, &) forall T, NT, RT + call(args, rt) + end + def call!(args : GenericArguments(T, NT), rt : RT.class) : RT forall T, NT, RT value = call(args, rt) if value.is_a?(RT) @@ -25,6 +29,15 @@ module Spectator::Mocks end end + def call!(args : GenericArguments(T, NT), rt : RT.class) : RT forall T, NT, RT + value = call(args, rt) { |*ya| yield *ya } + if value.is_a?(RT) + value.as(RT) + else + raise TypeCastError.new("The return type of stub #{self} doesn't match the expected type #{RT}") + end + end + def to_s(io) io << '#' io << @name diff --git a/src/spectator/mocks/nil_method_stub.cr b/src/spectator/mocks/nil_method_stub.cr index 43a6f82..410ca56 100644 --- a/src/spectator/mocks/nil_method_stub.cr +++ b/src/spectator/mocks/nil_method_stub.cr @@ -36,6 +36,10 @@ module Spectator::Mocks ExceptionMethodStub.new(@name, @source, exception_type.new(*args), @args) end + def and_yield(*yield_args) + YieldMethodStub.new(@name, @source, yield_args, @args) + end + def with(*args : *T, **opts : **NT) forall T, NT args = GenericArguments.new(args, opts) NilMethodStub.new(@name, @source, args) diff --git a/src/spectator/mocks/yield_method_stub.cr b/src/spectator/mocks/yield_method_stub.cr new file mode 100644 index 0000000..8adf254 --- /dev/null +++ b/src/spectator/mocks/yield_method_stub.cr @@ -0,0 +1,19 @@ +require "./generic_arguments" +require "./generic_method_stub" + +module Spectator::Mocks + class YieldMethodStub(YieldArgs) < GenericMethodStub(Nil) + def initialize(name, source, @yield_args : YieldArgs, args = nil) + super(name, source, args) + end + + def call(_args : GenericArguments(T2, NT2), rt : RT.class) forall T2, NT2, RT + raise "Asked to yield |#{@yield_args}| but no block was passed" + end + + def call(_args : GenericArguments(T2, NT2), rt : RT.class) forall T2, NT2, RT + yield *@yield_args + nil + end + end +end From b062472d98d93960bd736c4831020d4b9332f612 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 17 Nov 2019 13:30:09 -0700 Subject: [PATCH 189/205] Revert "Initial support for and_yield" This reverts commit e49bd0d57ad6235ea3f32e77ba50becbddbbdbed. Removing support for stubbed yield. Attempting to yield with different a different arity or argument types causes compilation errors. I don't see any easy fix for this. I would rather have no yield support than broken support. --- src/spectator/mocks/anonymous_double.cr | 6 +----- src/spectator/mocks/anonymous_null_double.cr | 6 +----- src/spectator/mocks/double.cr | 4 ++-- src/spectator/mocks/method_stub.cr | 13 ------------- src/spectator/mocks/nil_method_stub.cr | 4 ---- src/spectator/mocks/yield_method_stub.cr | 19 ------------------- 6 files changed, 4 insertions(+), 48 deletions(-) delete mode 100644 src/spectator/mocks/yield_method_stub.cr diff --git a/src/spectator/mocks/anonymous_double.cr b/src/spectator/mocks/anonymous_double.cr index f04d194..d2e67c9 100644 --- a/src/spectator/mocks/anonymous_double.cr +++ b/src/spectator/mocks/anonymous_double.cr @@ -12,11 +12,7 @@ module Spectator::Mocks call = ::Spectator::Mocks::GenericMethodCall.new({{call.name.symbolize}}, args) ::Spectator::Harness.current.mocks.record_call(self, call) if (stub = ::Spectator::Harness.current.mocks.find_stub(self, call)) - {% if call.block.is_a?(Nop) %} - stub.call!(args, typeof(@values.fetch({{call.name.symbolize}}) { raise })) - {% else %} - stub.call!(args, typeof(@values.fetch({{call.name.symbolize}}) { raise })) { |*ya| yield *ya } - {% end %} + stub.call!(args, typeof(@values.fetch({{call.name.symbolize}}) { raise })) else @values.fetch({{call.name.symbolize}}) do return nil if ::Spectator::Harness.current.mocks.expected?(self, {{call.name.symbolize}}) diff --git a/src/spectator/mocks/anonymous_null_double.cr b/src/spectator/mocks/anonymous_null_double.cr index 960962e..3da8122 100644 --- a/src/spectator/mocks/anonymous_null_double.cr +++ b/src/spectator/mocks/anonymous_null_double.cr @@ -8,11 +8,7 @@ module Spectator::Mocks call = ::Spectator::Mocks::GenericMethodCall.new({{call.name.symbolize}}, args) ::Spectator::Harness.current.mocks.record_call(self, call) if (stub = ::Spectator::Harness.current.mocks.find_stub(self, call)) - {% if call.block.is_a?(Nop) %} - stub.call!(args, typeof(@values.fetch({{call.name.symbolize}}) { self })) - {% else %} - stub.call!(args, typeof(@values.fetch({{call.name.symbolize}}) { self })) { |*ya| yield *ya } - {% end %} + stub.call!(args, typeof(@values.fetch({{call.name.symbolize}}) { self })) else @values.fetch({{call.name.symbolize}}) { self } end diff --git a/src/spectator/mocks/double.cr b/src/spectator/mocks/double.cr index 20d5f6f..f6e3c67 100644 --- a/src/spectator/mocks/double.cr +++ b/src/spectator/mocks/double.cr @@ -45,12 +45,12 @@ module Spectator::Mocks end end - def {{name}}({{params.splat}} &block){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} + def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) ::Spectator::Harness.current.mocks.record_call(self, %call) if (%stub = ::Spectator::Harness.current.mocks.find_stub(self, %call)) - %stub.call!(%args, typeof(%method({{args.splat}}) { |*%ya| yield *%ya })) { |*%ya| yield *%ya } + %stub.call!(%args, typeof(%method({{args.splat}}) { |*%ya| yield *%ya })) else %method({{args.splat}}) do |*%yield_args| yield *%yield_args diff --git a/src/spectator/mocks/method_stub.cr b/src/spectator/mocks/method_stub.cr index 80050b7..a297d14 100644 --- a/src/spectator/mocks/method_stub.cr +++ b/src/spectator/mocks/method_stub.cr @@ -16,10 +16,6 @@ module Spectator::Mocks abstract def call(args : GenericArguments(T, NT), rt : RT.class) forall T, NT, RT - def call(args : GenericArguments(T, NT), rt : RT.class, &) forall T, NT, RT - call(args, rt) - end - def call!(args : GenericArguments(T, NT), rt : RT.class) : RT forall T, NT, RT value = call(args, rt) if value.is_a?(RT) @@ -29,15 +25,6 @@ module Spectator::Mocks end end - def call!(args : GenericArguments(T, NT), rt : RT.class) : RT forall T, NT, RT - value = call(args, rt) { |*ya| yield *ya } - if value.is_a?(RT) - value.as(RT) - else - raise TypeCastError.new("The return type of stub #{self} doesn't match the expected type #{RT}") - end - end - def to_s(io) io << '#' io << @name diff --git a/src/spectator/mocks/nil_method_stub.cr b/src/spectator/mocks/nil_method_stub.cr index 410ca56..43a6f82 100644 --- a/src/spectator/mocks/nil_method_stub.cr +++ b/src/spectator/mocks/nil_method_stub.cr @@ -36,10 +36,6 @@ module Spectator::Mocks ExceptionMethodStub.new(@name, @source, exception_type.new(*args), @args) end - def and_yield(*yield_args) - YieldMethodStub.new(@name, @source, yield_args, @args) - end - def with(*args : *T, **opts : **NT) forall T, NT args = GenericArguments.new(args, opts) NilMethodStub.new(@name, @source, args) diff --git a/src/spectator/mocks/yield_method_stub.cr b/src/spectator/mocks/yield_method_stub.cr deleted file mode 100644 index 8adf254..0000000 --- a/src/spectator/mocks/yield_method_stub.cr +++ /dev/null @@ -1,19 +0,0 @@ -require "./generic_arguments" -require "./generic_method_stub" - -module Spectator::Mocks - class YieldMethodStub(YieldArgs) < GenericMethodStub(Nil) - def initialize(name, source, @yield_args : YieldArgs, args = nil) - super(name, source, args) - end - - def call(_args : GenericArguments(T2, NT2), rt : RT.class) forall T2, NT2, RT - raise "Asked to yield |#{@yield_args}| but no block was passed" - end - - def call(_args : GenericArguments(T2, NT2), rt : RT.class) forall T2, NT2, RT - yield *@yield_args - nil - end - end -end From 27acf7bced1c3f859cd4859fc6a3793a75677dd9 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 19 Nov 2019 17:28:00 -0700 Subject: [PATCH 190/205] Allow receive block syntax --- src/spectator/dsl/mocks.cr | 8 ++++++-- src/spectator/mocks/proc_method_stub.cr | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 7470f16..ef24d6c 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -126,9 +126,13 @@ module Spectator::DSL ::Spectator::Mocks::ExpectAnyInstance({{type}}).new(%source) end - macro receive(method_name, _source_file = __FILE__, _source_line = __LINE__) + macro receive(method_name, _source_file = __FILE__, _source_line = __LINE__, &block) %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - ::Spectator::Mocks::NilMethodStub.new({{method_name.id.symbolize}}, %source) + {% 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) diff --git a/src/spectator/mocks/proc_method_stub.cr b/src/spectator/mocks/proc_method_stub.cr index b0d921f..eeb0569 100644 --- a/src/spectator/mocks/proc_method_stub.cr +++ b/src/spectator/mocks/proc_method_stub.cr @@ -7,6 +7,10 @@ module Spectator::Mocks super(name, source, args) end + def self.create(name, source, args = nil, &block : -> T) forall T + ProcMethodStub.new(name, source, block, args) + end + def call(_args : GenericArguments(T2, NT2), rt : RT.class) forall T2, NT2, RT @proc.call end From 51a47fec98200d4cc10ad749f4b2157effeb5d41 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 20 Nov 2019 15:05:04 -0700 Subject: [PATCH 191/205] Remove unused RT parameter --- src/spectator/mocks/exception_method_stub.cr | 2 +- src/spectator/mocks/method_stub.cr | 6 +++--- src/spectator/mocks/multi_value_method_stub.cr | 2 +- src/spectator/mocks/nil_method_stub.cr | 2 +- src/spectator/mocks/proc_method_stub.cr | 2 +- src/spectator/mocks/value_method_stub.cr | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/spectator/mocks/exception_method_stub.cr b/src/spectator/mocks/exception_method_stub.cr index 062e47f..8e0948c 100644 --- a/src/spectator/mocks/exception_method_stub.cr +++ b/src/spectator/mocks/exception_method_stub.cr @@ -7,7 +7,7 @@ module Spectator::Mocks super(name, source, args) end - def call(_args : GenericArguments(T2, NT2), rt : RT.class) forall T2, NT2, RT + def call(_args : GenericArguments(T2, NT2)) forall T2, NT2 raise @exception end end diff --git a/src/spectator/mocks/method_stub.cr b/src/spectator/mocks/method_stub.cr index a297d14..12b9999 100644 --- a/src/spectator/mocks/method_stub.cr +++ b/src/spectator/mocks/method_stub.cr @@ -14,10 +14,10 @@ module Spectator::Mocks @name == call.name end - abstract def call(args : GenericArguments(T, NT), rt : RT.class) forall T, NT, RT + abstract def call(args : GenericArguments(T, NT)) forall T, NT - def call!(args : GenericArguments(T, NT), rt : RT.class) : RT forall T, NT, RT - value = call(args, rt) + def call!(args : GenericArguments(T, NT), _rt : RT.class) : RT forall T, NT, RT + value = call(args) if value.is_a?(RT) value.as(RT) else diff --git a/src/spectator/mocks/multi_value_method_stub.cr b/src/spectator/mocks/multi_value_method_stub.cr index 6ab3781..b0495e5 100644 --- a/src/spectator/mocks/multi_value_method_stub.cr +++ b/src/spectator/mocks/multi_value_method_stub.cr @@ -10,7 +10,7 @@ module Spectator::Mocks raise ArgumentError.new("Values must have at least one item") if @values.size < 1 end - def call(_args : GenericArguments(T2, NT2), rt : RT.class) forall T2, NT2, RT + def call(_args : GenericArguments(T2, NT2)) forall T2, NT2 value = @values[@index] @index += 1 if @index < @values.size - 1 value diff --git a/src/spectator/mocks/nil_method_stub.cr b/src/spectator/mocks/nil_method_stub.cr index 43a6f82..3667762 100644 --- a/src/spectator/mocks/nil_method_stub.cr +++ b/src/spectator/mocks/nil_method_stub.cr @@ -4,7 +4,7 @@ require "./value_method_stub" module Spectator::Mocks class NilMethodStub < GenericMethodStub(Nil) - def call(_args : GenericArguments(T, NT), _rt : RT.class) forall T, NT, RT + def call(_args : GenericArguments(T, NT)) forall T, NT nil end diff --git a/src/spectator/mocks/proc_method_stub.cr b/src/spectator/mocks/proc_method_stub.cr index eeb0569..b6af797 100644 --- a/src/spectator/mocks/proc_method_stub.cr +++ b/src/spectator/mocks/proc_method_stub.cr @@ -11,7 +11,7 @@ module Spectator::Mocks ProcMethodStub.new(name, source, block, args) end - def call(_args : GenericArguments(T2, NT2), rt : RT.class) forall T2, NT2, RT + def call(_args : GenericArguments(T2, NT2)) forall T2, NT2 @proc.call end end diff --git a/src/spectator/mocks/value_method_stub.cr b/src/spectator/mocks/value_method_stub.cr index 6cdec7b..cb0efa5 100644 --- a/src/spectator/mocks/value_method_stub.cr +++ b/src/spectator/mocks/value_method_stub.cr @@ -7,7 +7,7 @@ module Spectator::Mocks super(name, source, args) end - def call(_args : GenericArguments(T2, NT2), rt : RT.class) forall T2, NT2, RT + def call(_args : GenericArguments(T2, NT2)) forall T2, NT2 @value end end From 2dee50f19eea00fbe2cad6bbc98e353fa70183d0 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 20 Nov 2019 15:43:45 -0700 Subject: [PATCH 192/205] Pass original (and type constraint) to stub via block --- src/spectator/mocks/anonymous_double.cr | 4 +++- src/spectator/mocks/anonymous_null_double.cr | 2 +- src/spectator/mocks/double.cr | 4 ++-- src/spectator/mocks/method_stub.cr | 2 +- src/spectator/mocks/stubs.cr | 4 ++-- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/spectator/mocks/anonymous_double.cr b/src/spectator/mocks/anonymous_double.cr index d2e67c9..1fd9a47 100644 --- a/src/spectator/mocks/anonymous_double.cr +++ b/src/spectator/mocks/anonymous_double.cr @@ -12,7 +12,9 @@ module Spectator::Mocks call = ::Spectator::Mocks::GenericMethodCall.new({{call.name.symbolize}}, args) ::Spectator::Harness.current.mocks.record_call(self, call) if (stub = ::Spectator::Harness.current.mocks.find_stub(self, call)) - stub.call!(args, typeof(@values.fetch({{call.name.symbolize}}) { raise })) + stub.call!(args) do + @values.fetch({{call.name.symbolize}}) { raise "Consistency error - method stubbed with no implementation" } + end else @values.fetch({{call.name.symbolize}}) do return nil if ::Spectator::Harness.current.mocks.expected?(self, {{call.name.symbolize}}) diff --git a/src/spectator/mocks/anonymous_null_double.cr b/src/spectator/mocks/anonymous_null_double.cr index 3da8122..8fa338c 100644 --- a/src/spectator/mocks/anonymous_null_double.cr +++ b/src/spectator/mocks/anonymous_null_double.cr @@ -8,7 +8,7 @@ module Spectator::Mocks call = ::Spectator::Mocks::GenericMethodCall.new({{call.name.symbolize}}, args) ::Spectator::Harness.current.mocks.record_call(self, call) if (stub = ::Spectator::Harness.current.mocks.find_stub(self, call)) - stub.call!(args, typeof(@values.fetch({{call.name.symbolize}}) { self })) + stub.call!(args) { @values.fetch({{call.name.symbolize}}) { self } } else @values.fetch({{call.name.symbolize}}) { self } end diff --git a/src/spectator/mocks/double.cr b/src/spectator/mocks/double.cr index f6e3c67..ce83f53 100644 --- a/src/spectator/mocks/double.cr +++ b/src/spectator/mocks/double.cr @@ -39,7 +39,7 @@ module Spectator::Mocks %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) ::Spectator::Harness.current.mocks.record_call(self, %call) if (%stub = ::Spectator::Harness.current.mocks.find_stub(self, %call)) - %stub.call!(%args, typeof(%method({{args.splat}}))) + %stub.call!(%args) { %method({{args.splat}}) } else %method({{args.splat}}) end @@ -50,7 +50,7 @@ module Spectator::Mocks %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) ::Spectator::Harness.current.mocks.record_call(self, %call) if (%stub = ::Spectator::Harness.current.mocks.find_stub(self, %call)) - %stub.call!(%args, typeof(%method({{args.splat}}) { |*%ya| yield *%ya })) + %stub.call!(%args) { %method({{args.splat}}) { |*%ya| yield *%ya } } else %method({{args.splat}}) do |*%yield_args| yield *%yield_args diff --git a/src/spectator/mocks/method_stub.cr b/src/spectator/mocks/method_stub.cr index 12b9999..4ee3a6e 100644 --- a/src/spectator/mocks/method_stub.cr +++ b/src/spectator/mocks/method_stub.cr @@ -16,7 +16,7 @@ module Spectator::Mocks abstract def call(args : GenericArguments(T, NT)) forall T, NT - def call!(args : GenericArguments(T, NT), _rt : RT.class) : RT forall T, NT, RT + def call!(args : GenericArguments(T, NT), &original : -> RT) : RT forall T, NT, RT value = call(args) if value.is_a?(RT) value.as(RT) diff --git a/src/spectator/mocks/stubs.cr b/src/spectator/mocks/stubs.cr index 2c2cf1f..089e637 100644 --- a/src/spectator/mocks/stubs.cr +++ b/src/spectator/mocks/stubs.cr @@ -56,7 +56,7 @@ module Spectator::Mocks %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) %harness.mocks.record_call(self, %call) if (%stub = %harness.mocks.find_stub(self, %call)) - return %stub.call!(%args, typeof({{original}}({{args.splat}}))) + return %stub.call!(%args) { {{original}}({{args.splat}}) } end end {{original}}({{args.splat}}) @@ -68,7 +68,7 @@ module Spectator::Mocks %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) %harness.mocks.record_call(self, %call) if (%stub = %harness.mocks.find_stub(self, %call)) - return %stub.call!(%args, typeof({{original}}({{args.splat}}) { |*%ya| yield *%ya })) + return %stub.call!(%args) { {{original}}({{args.splat}}) { |*%ya| yield *%ya } } end end {{original}}({{args.splat}}) do |*%yield_args| From f8563e86c7d7aef58961f0cf92b980a5342728e0 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 20 Nov 2019 16:29:06 -0700 Subject: [PATCH 193/205] Pass along original block --- src/spectator/mocks/exception_method_stub.cr | 2 +- src/spectator/mocks/method_stub.cr | 4 ++-- src/spectator/mocks/multi_value_method_stub.cr | 2 +- src/spectator/mocks/nil_method_stub.cr | 2 +- src/spectator/mocks/proc_method_stub.cr | 2 +- src/spectator/mocks/value_method_stub.cr | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/spectator/mocks/exception_method_stub.cr b/src/spectator/mocks/exception_method_stub.cr index 8e0948c..96342bc 100644 --- a/src/spectator/mocks/exception_method_stub.cr +++ b/src/spectator/mocks/exception_method_stub.cr @@ -7,7 +7,7 @@ module Spectator::Mocks super(name, source, args) end - def call(_args : GenericArguments(T2, NT2)) forall T2, NT2 + def call(_args : GenericArguments(T, NT), &_original : -> RT) forall T, NT, RT raise @exception end end diff --git a/src/spectator/mocks/method_stub.cr b/src/spectator/mocks/method_stub.cr index 4ee3a6e..245bff3 100644 --- a/src/spectator/mocks/method_stub.cr +++ b/src/spectator/mocks/method_stub.cr @@ -14,10 +14,10 @@ module Spectator::Mocks @name == call.name end - abstract def call(args : GenericArguments(T, NT)) forall T, NT + abstract def call(args : GenericArguments(T, NT), &original : -> RT) forall T, NT, RT def call!(args : GenericArguments(T, NT), &original : -> RT) : RT forall T, NT, RT - value = call(args) + value = call(args, &original) if value.is_a?(RT) value.as(RT) else diff --git a/src/spectator/mocks/multi_value_method_stub.cr b/src/spectator/mocks/multi_value_method_stub.cr index b0495e5..c623a4c 100644 --- a/src/spectator/mocks/multi_value_method_stub.cr +++ b/src/spectator/mocks/multi_value_method_stub.cr @@ -10,7 +10,7 @@ module Spectator::Mocks raise ArgumentError.new("Values must have at least one item") if @values.size < 1 end - def call(_args : GenericArguments(T2, NT2)) forall T2, NT2 + def call(_args : GenericArguments(T, NT), &_original : -> RT) forall T, NT, RT value = @values[@index] @index += 1 if @index < @values.size - 1 value diff --git a/src/spectator/mocks/nil_method_stub.cr b/src/spectator/mocks/nil_method_stub.cr index 3667762..032c1fd 100644 --- a/src/spectator/mocks/nil_method_stub.cr +++ b/src/spectator/mocks/nil_method_stub.cr @@ -4,7 +4,7 @@ require "./value_method_stub" module Spectator::Mocks class NilMethodStub < GenericMethodStub(Nil) - def call(_args : GenericArguments(T, NT)) forall T, NT + def call(_args : GenericArguments(T, NT), &_original : -> RT) forall T, NT, RT nil end diff --git a/src/spectator/mocks/proc_method_stub.cr b/src/spectator/mocks/proc_method_stub.cr index b6af797..1d55db3 100644 --- a/src/spectator/mocks/proc_method_stub.cr +++ b/src/spectator/mocks/proc_method_stub.cr @@ -11,7 +11,7 @@ module Spectator::Mocks ProcMethodStub.new(name, source, block, args) end - def call(_args : GenericArguments(T2, NT2)) forall T2, NT2 + def call(_args : GenericArguments(T, NT), &_original : -> RT) forall T, NT, RT @proc.call end end diff --git a/src/spectator/mocks/value_method_stub.cr b/src/spectator/mocks/value_method_stub.cr index cb0efa5..43bfcaa 100644 --- a/src/spectator/mocks/value_method_stub.cr +++ b/src/spectator/mocks/value_method_stub.cr @@ -7,7 +7,7 @@ module Spectator::Mocks super(name, source, args) end - def call(_args : GenericArguments(T2, NT2)) forall T2, NT2 + def call(_args : GenericArguments(T, NT), &_original : -> RT) forall T, NT, RT @value end end From 5d84536e1e45561e52f87763aa933314c08573df Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 20 Nov 2019 19:17:51 -0700 Subject: [PATCH 194/205] Implement and_call_original --- src/spectator/mocks/nil_method_stub.cr | 4 ++++ src/spectator/mocks/original_method_stub.cr | 10 ++++++++++ 2 files changed, 14 insertions(+) create mode 100644 src/spectator/mocks/original_method_stub.cr diff --git a/src/spectator/mocks/nil_method_stub.cr b/src/spectator/mocks/nil_method_stub.cr index 032c1fd..592d3b6 100644 --- a/src/spectator/mocks/nil_method_stub.cr +++ b/src/spectator/mocks/nil_method_stub.cr @@ -40,5 +40,9 @@ module Spectator::Mocks args = GenericArguments.new(args, opts) NilMethodStub.new(@name, @source, args) end + + def and_call_original + OriginalMethodStub.new(@name, @source, @args) + end end end diff --git a/src/spectator/mocks/original_method_stub.cr b/src/spectator/mocks/original_method_stub.cr new file mode 100644 index 0000000..2097eb6 --- /dev/null +++ b/src/spectator/mocks/original_method_stub.cr @@ -0,0 +1,10 @@ +require "./generic_arguments" +require "./generic_method_stub" + +module Spectator::Mocks + class OriginalMethodStub < GenericMethodStub(Nil) + def call(_args : GenericArguments(T, NT), &_original : -> RT) forall T, NT, RT + yield + end + end +end From d6c1169bc305d0c57484cc551c291bae61b553d6 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 20 Nov 2019 20:40:53 -0700 Subject: [PATCH 195/205] Fix dumb syntax --- src/spectator/matchers/all_matcher.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/matchers/all_matcher.cr b/src/spectator/matchers/all_matcher.cr index f413fbd..bf6231d 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 ? found : SuccessfulMatchData.new + found || SuccessfulMatchData.new end # Negated matching for this matcher is not supported. From c710961be1b099be4d7a2174029244032093bb51 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 29 Nov 2019 08:53:36 -0700 Subject: [PATCH 196/205] Fix returned type inferred to NoReturn --- src/spectator/mocks/anonymous_double.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/mocks/anonymous_double.cr b/src/spectator/mocks/anonymous_double.cr index 1fd9a47..6bfbb5a 100644 --- a/src/spectator/mocks/anonymous_double.cr +++ b/src/spectator/mocks/anonymous_double.cr @@ -13,7 +13,7 @@ module Spectator::Mocks ::Spectator::Harness.current.mocks.record_call(self, call) if (stub = ::Spectator::Harness.current.mocks.find_stub(self, call)) stub.call!(args) do - @values.fetch({{call.name.symbolize}}) { raise "Consistency error - method stubbed with no implementation" } + @values.fetch({{call.name.symbolize}}) { raise "Consistency error - method stubbed with no implementation"; nil } end else @values.fetch({{call.name.symbolize}}) do From 8197a82ace5c589bab9153e20d4cf5967a578477 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 29 Nov 2019 09:25:58 -0700 Subject: [PATCH 197/205] Expect stubs not method names Needed for argument syntax: `expect(dbl).to receive(:foo).with(:bar)` --- .../expectations/expectation_partial.cr | 2 +- src/spectator/mocks/anonymous_double.cr | 2 +- src/spectator/mocks/double.cr | 10 ++++++++-- src/spectator/mocks/registry.cr | 16 ++++++++-------- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/spectator/expectations/expectation_partial.cr b/src/spectator/expectations/expectation_partial.cr index 00e26dd..b7a602d 100644 --- a/src/spectator/expectations/expectation_partial.cr +++ b/src/spectator/expectations/expectation_partial.cr @@ -25,7 +25,7 @@ module Spectator::Expectations end def to(stub : Mocks::MethodStub) : Nil - Harness.current.mocks.expect(@actual.value, stub.name) + 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) diff --git a/src/spectator/mocks/anonymous_double.cr b/src/spectator/mocks/anonymous_double.cr index 6bfbb5a..439901a 100644 --- a/src/spectator/mocks/anonymous_double.cr +++ b/src/spectator/mocks/anonymous_double.cr @@ -17,7 +17,7 @@ module Spectator::Mocks end else @values.fetch({{call.name.symbolize}}) do - return nil if ::Spectator::Harness.current.mocks.expected?(self, {{call.name.symbolize}}) + return nil if ::Spectator::Harness.current.mocks.expected?(self, call) raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{call.name}}") end end diff --git a/src/spectator/mocks/double.cr b/src/spectator/mocks/double.cr index ce83f53..de218ba 100644 --- a/src/spectator/mocks/double.cr +++ b/src/spectator/mocks/double.cr @@ -62,7 +62,9 @@ module Spectator::Mocks {% if body && !body.is_a?(Nop) %} {{body.body}} {% else %} - unless ::Spectator::Harness.current.mocks.expected?(self, {{name.symbolize}}) + %args = ::Spectator::Mocks::GenericArguments.create({{params.splat}}) + %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) + unless ::Spectator::Harness.current.mocks.expected?(self, %call) raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{name}}") end @@ -77,8 +79,12 @@ module Spectator::Mocks end macro method_missing(call) + args = ::Spectator::Mocks::GenericArguments.create({{call.args.splat}}) + call = ::Spectator::Mocks::GenericMethodCall.new({{call.name.symbolize}}, args) + ::Spectator::Harness.current.mocks.record_call(self, call) + return self if @null - return self if ::Spectator::Harness.current.mocks.expected?(self, {{call.name.symbolize}}) + return self if ::Spectator::Harness.current.mocks.expected?(self, call) raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{call.name}}") end diff --git a/src/spectator/mocks/registry.cr b/src/spectator/mocks/registry.cr index e01d186..a946ae4 100644 --- a/src/spectator/mocks/registry.cr +++ b/src/spectator/mocks/registry.cr @@ -5,7 +5,7 @@ module Spectator::Mocks private struct Entry getter stubs = Deque(MethodStub).new getter calls = Deque(MethodCall).new - getter expected = Set(Symbol).new + getter expected = Set(MethodStub).new end @all_instances = {} of String => Entry @@ -65,17 +65,17 @@ module Spectator::Mocks fetch_type(type).calls.select { |call| call.name == method_name } end - def expected?(object, method_name : Symbol) : Bool - fetch_instance(object).expected.includes?(method_name) || - fetch_type(object.class).expected.includes?(method_name) + def expected?(object, call : GenericMethodCall(T, NT)) : Bool forall T, NT + fetch_instance(object).expected.any?(&.callable?(call)) || + fetch_type(object.class).expected.any?(&.callable?(call)) end - def expect(object, method_name : Symbol) : Nil - fetch_instance(object).expected.add(method_name) + def expect(object, stub : MethodStub) : Nil + fetch_instance(object).expected.add(stub) end - def expect(type : T.class, method_name : Symbol) : Nil forall T - fetch_type(type).expected.add(method_name) + def expect(type : T.class, stub : MethodStub) : Nil forall T + fetch_type(type).expected.add(stub) end private def fetch_instance(object) From 8b0f509689dd4a9b527be0c9cc7da0468887188b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Dec 2019 11:37:08 -0700 Subject: [PATCH 198/205] Detect when to use a verifying double --- src/spectator/dsl/mocks.cr | 14 ++-- src/spectator/mocks/verifying_double.cr | 94 +++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 src/spectator/mocks/verifying_double.cr diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index ef24d6c..93a50ec 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -29,10 +29,15 @@ module Spectator::DSL end macro define_double(type_name, name, **stubs, &block) - class {{type_name}} < ::Spectator::Mocks::Double - def initialize(null = false) - super({{name.id.stringify}}, null) - end + {% begin %} + {% if (name.is_a?(Path) || name.is_a?(Generic)) && (resolved = name.resolve?) %} + 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 %} def as_null_object {{type_name}}.new(true) @@ -42,6 +47,7 @@ module Spectator::DSL {{block.body}} end + {% end %} end def anonymous_double(name = "Anonymous", **stubs) diff --git a/src/spectator/mocks/verifying_double.cr b/src/spectator/mocks/verifying_double.cr new file mode 100644 index 0000000..ecdb1e9 --- /dev/null +++ b/src/spectator/mocks/verifying_double.cr @@ -0,0 +1,94 @@ +module Spectator::Mocks + class VerifyingDouble(T) + def initialize(@null = false) + end + + private macro stub(definition, &block) + {% + name = nil + params = nil + args = nil + body = nil + if definition.is_a?(Call) # stub foo { :bar } + named = false + name = definition.name.id + params = definition.args + args = params.map do |p| + n = p.is_a?(TypeDeclaration) ? p.var : p.id + r = named ? "#{n}: #{n}".id : n + named = true if n.starts_with?('*') + r + end + body = definition.block.is_a?(Nop) ? block : definition.block + elsif definition.is_a?(TypeDeclaration) # stub foo : Symbol + name = definition.var + params = [] of MacroId + args = [] of MacroId + body = block + else + raise "Unrecognized stub format" + end + %} + + def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} + %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) + %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) + ::Spectator::Harness.current.mocks.record_call(self, %call) + if (%stub = ::Spectator::Harness.current.mocks.find_stub(self, %call)) + %stub.call!(%args) { %method({{args.splat}}) } + else + %method({{args.splat}}) + end + end + + def {{name}}({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} + %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) + %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) + ::Spectator::Harness.current.mocks.record_call(self, %call) + if (%stub = ::Spectator::Harness.current.mocks.find_stub(self, %call)) + %stub.call!(%args) { %method({{args.splat}}) { |*%ya| yield *%ya } } + else + %method({{args.splat}}) do |*%yield_args| + yield *%yield_args + end + end + end + + def %method({{params.splat}}){% if definition.is_a?(TypeDeclaration) %} : {{definition.type}}{% end %} + {% if body && !body.is_a?(Nop) %} + {{body.body}} + {% else %} + %args = ::Spectator::Mocks::GenericArguments.create({{params.splat}}) + %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) + unless ::Spectator::Harness.current.mocks.expected?(self, %call) + raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{name}}") + end + + # This code shouldn't be reached, but makes the compiler happy to have a matching return type. + {% if definition.is_a?(TypeDeclaration) %} + %x = uninitialized {{definition.type}} + {% else %} + nil + {% end %} + {% end %} + end + end + + macro method_missing(call) + args = ::Spectator::Mocks::GenericArguments.create({{call.args.splat}}) + call = ::Spectator::Mocks::GenericMethodCall.new({{call.name.symbolize}}, args) + ::Spectator::Harness.current.mocks.record_call(self, call) + + return self if @null + return self if ::Spectator::Harness.current.mocks.expected?(self, call) + + raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{call.name}}") + end + + def to_s(io) + io << "Double(" + io << T + io << ')' + end + end +end From 07ce835724352aad07d1113c67d8add59db31478 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Dec 2019 14:51:48 -0700 Subject: [PATCH 199/205] VerifyingDouble should be abstract --- src/spectator/mocks/verifying_double.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/mocks/verifying_double.cr b/src/spectator/mocks/verifying_double.cr index ecdb1e9..2c64b14 100644 --- a/src/spectator/mocks/verifying_double.cr +++ b/src/spectator/mocks/verifying_double.cr @@ -1,5 +1,5 @@ module Spectator::Mocks - class VerifyingDouble(T) + abstract class VerifyingDouble(T) def initialize(@null = false) end From 074aff531c05a8a1b12cf9d4218303676b1629bf Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Dec 2019 14:52:28 -0700 Subject: [PATCH 200/205] Add "anything" which always returns true when compared against --- src/spectator/anything.cr | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/spectator/anything.cr diff --git a/src/spectator/anything.cr b/src/spectator/anything.cr new file mode 100644 index 0000000..511a024 --- /dev/null +++ b/src/spectator/anything.cr @@ -0,0 +1,15 @@ +module Spectator + struct Anything + def ==(other) + true + end + + def ===(other) + true + end + + def =~(other) + true + end + end +end From 067c789019cbccad5ec36f7ce28392d45e059608 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Dec 2019 14:52:54 -0700 Subject: [PATCH 201/205] Initial work on type reflection for verifying doubles --- src/spectator/dsl/mocks.cr | 21 +++++++++++++++++++ src/spectator/mocks/mock.cr | 30 ---------------------------- src/spectator/mocks/reflection.cr | 22 ++++++++++++++++++++ src/spectator/mocks/type_registry.cr | 19 ++++++++++++++++++ 4 files changed, 62 insertions(+), 30 deletions(-) delete mode 100644 src/spectator/mocks/mock.cr create mode 100644 src/spectator/mocks/reflection.cr create mode 100644 src/spectator/mocks/type_registry.cr diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 93a50ec..07d3c00 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -31,6 +31,7 @@ module Spectator::DSL 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 @@ -119,6 +120,26 @@ module Spectator::DSL {% 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 diff --git a/src/spectator/mocks/mock.cr b/src/spectator/mocks/mock.cr deleted file mode 100644 index e46b6a9..0000000 --- a/src/spectator/mocks/mock.cr +++ /dev/null @@ -1,30 +0,0 @@ -module Spectator::Mocks - module Mock - macro included - {% for meth in @type.methods %} - {% if meth.visibility != :public %}{{meth.visibility.id}} {% end %}def {{meth.name.id}}( - {% for arg, i in meth.args %} - {% if meth.splat_index && i == meth.splat_index %} - *{{arg}}{% if i + (meth.accepts_block? ? 0 : 1) < meth.args.size %},{% end %} - {% else %} - {{arg}}{% if i + (meth.accepts_block? ? 0 : 1) < meth.args.size %},{% end %} - {% end %} - {% end %} - {% if meth.accepts_block? %}&{% if meth.block_arg %}{{meth.block_arg}}{% else %}__spec_block{% end %}{% end %} - ){% if meth.return_type %} : {{meth.return_type}}{% end %} - previous_def( - {% for arg, i in meth.args %} - {% if !meth.splat_index || i < meth.splat_index %} - {{arg.name.id}}{% if i + (meth.accepts_block? ? 0 : 1) < meth.args.size %},{% end %} - {% elsif meth.splat_index && i > meth.splat_index %} - {{arg.name.id}}: {{arg.name}}{% if i + (meth.accepts_block? ? 0 : 1) < meth.args.size %},{% end %} - {% end %} - {% end %} - {% if meth.accepts_block? %}&{% if meth.block_arg %}{{meth.block_arg}}{% else %}__spec_block{% end %}{% end %} - ) - end - {% end %} - {% debug %} - end - end -end diff --git a/src/spectator/mocks/reflection.cr b/src/spectator/mocks/reflection.cr new file mode 100644 index 0000000..32ddee4 --- /dev/null +++ b/src/spectator/mocks/reflection.cr @@ -0,0 +1,22 @@ +require "../anything" + +module Spectator::Mocks + module Reflection + private macro _spectator_reflect + {% for meth in @type.methods %} + %source = ::Spectator::Source.new({{meth.filename}}, {{meth.line_number}}) + %args = ::Spectator::Mocks::GenericArguments.create( + {% for arg, i in meth.args %} + {% if meth.splat_index && i == meth.splat_index %} + *{{arg.restriction || "::Spectator::Anything.new".id}}{% if i < meth.args.size %},{% end %} + {% else %} + {{arg.restriction || "::Spectator::Anything.new".id}}{% if i < meth.args.size %},{% end %} + {% end %} + {% end %} + ) + ::Spectator::Mocks::TypeRegistry.add({{@type.id.stringify}}, {{meth.name.symbolize}}, %source, %args) + {% end %} + {% debug %} + end + end +end diff --git a/src/spectator/mocks/type_registry.cr b/src/spectator/mocks/type_registry.cr new file mode 100644 index 0000000..5c4481e --- /dev/null +++ b/src/spectator/mocks/type_registry.cr @@ -0,0 +1,19 @@ +module Spectator::Mocks + module TypeRegistry + extend self + + alias Key = Tuple(String, Symbol) + + @@entries = {} of Key => Deque(MethodStub) + + def add(type_name : String, method_name : Symbol, source : Source, 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) + end + end +end From 91696d490f70dae084865018d07018caacea5e7e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Dec 2019 15:08:13 -0700 Subject: [PATCH 202/205] Check if underlying type responds to method --- src/spectator/mocks/type_registry.cr | 6 ++++++ src/spectator/mocks/verifying_double.cr | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/spectator/mocks/type_registry.cr b/src/spectator/mocks/type_registry.cr index 5c4481e..0508c9f 100644 --- a/src/spectator/mocks/type_registry.cr +++ b/src/spectator/mocks/type_registry.cr @@ -15,5 +15,11 @@ module Spectator::Mocks end list << NilMethodStub.new(method_name, source, args) end + + def exists?(type_name : String, call : GenericMethodCall(T, NT)) : Bool forall T, NT + key = {type_name, call.name} + list = @@entries.fetch(key) { return false } + list.any?(&.callable?(call)) + end end end diff --git a/src/spectator/mocks/verifying_double.cr b/src/spectator/mocks/verifying_double.cr index 2c64b14..3f451cb 100644 --- a/src/spectator/mocks/verifying_double.cr +++ b/src/spectator/mocks/verifying_double.cr @@ -34,6 +34,11 @@ module Spectator::Mocks %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) ::Spectator::Harness.current.mocks.record_call(self, %call) + + unless ::Spectator::Mocks::TypeRegistry.exists?(T.to_s, %call) + raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{name}} - #{T} does not respond to #{%call}") + end + if (%stub = ::Spectator::Harness.current.mocks.find_stub(self, %call)) %stub.call!(%args) { %method({{args.splat}}) } else @@ -45,6 +50,11 @@ module Spectator::Mocks %args = ::Spectator::Mocks::GenericArguments.create({{args.splat}}) %call = ::Spectator::Mocks::GenericMethodCall.new({{name.symbolize}}, %args) ::Spectator::Harness.current.mocks.record_call(self, %call) + + unless ::Spectator::Mocks::TypeRegistry.exists?(T.to_s, %call) + raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{name}} - #{T} does not respond to #{%call}") + end + if (%stub = ::Spectator::Harness.current.mocks.find_stub(self, %call)) %stub.call!(%args) { %method({{args.splat}}) { |*%ya| yield *%ya } } else @@ -79,6 +89,10 @@ module Spectator::Mocks call = ::Spectator::Mocks::GenericMethodCall.new({{call.name.symbolize}}, args) ::Spectator::Harness.current.mocks.record_call(self, call) + unless ::Spectator::Mocks::TypeRegistry.exists?(T.to_s, call) + raise ::Spectator::Mocks::UnexpectedMessageError.new("#{self} received unexpected message {{call.name}} - #{T} does not respond to #{call}") + end + return self if @null return self if ::Spectator::Harness.current.mocks.expected?(self, call) From 1a30a56f2a955c2c2e99aeec26e45d068524eca3 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Dec 2019 15:10:49 -0700 Subject: [PATCH 203/205] Better stringified call --- src/spectator/mocks/generic_method_call.cr | 7 +++++++ src/spectator/mocks/method_call.cr | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/src/spectator/mocks/generic_method_call.cr b/src/spectator/mocks/generic_method_call.cr index 628e40f..64f8999 100644 --- a/src/spectator/mocks/generic_method_call.cr +++ b/src/spectator/mocks/generic_method_call.cr @@ -8,5 +8,12 @@ module Spectator::Mocks def initialize(name : Symbol, @args : GenericArguments(T, NT)) super(name) end + + def to_s(io) + super + io << '(' + io << @args + io << ')' + end end end diff --git a/src/spectator/mocks/method_call.cr b/src/spectator/mocks/method_call.cr index 7c85862..00c6d0c 100644 --- a/src/spectator/mocks/method_call.cr +++ b/src/spectator/mocks/method_call.cr @@ -4,5 +4,10 @@ module Spectator::Mocks def initialize(@name : Symbol) end + + def to_s(io) + io << '#' + io << @name + end end end From 0ae5cdc9ae95fead69bee793f32eec8198ff5cc1 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Dec 2019 15:11:08 -0700 Subject: [PATCH 204/205] Remove debug --- src/spectator/mocks/reflection.cr | 1 - 1 file changed, 1 deletion(-) diff --git a/src/spectator/mocks/reflection.cr b/src/spectator/mocks/reflection.cr index 32ddee4..43f20f2 100644 --- a/src/spectator/mocks/reflection.cr +++ b/src/spectator/mocks/reflection.cr @@ -16,7 +16,6 @@ module Spectator::Mocks ) ::Spectator::Mocks::TypeRegistry.add({{@type.id.stringify}}, {{meth.name.symbolize}}, %source, %args) {% end %} - {% debug %} end end end From 4c9bbbe07bdd7633e8684d6928309325345e56d8 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Dec 2019 15:11:23 -0700 Subject: [PATCH 205/205] Formatting --- src/spectator/mocks/type_registry.cr | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/spectator/mocks/type_registry.cr b/src/spectator/mocks/type_registry.cr index 0508c9f..c3cb0b6 100644 --- a/src/spectator/mocks/type_registry.cr +++ b/src/spectator/mocks/type_registry.cr @@ -2,17 +2,17 @@ module Spectator::Mocks module TypeRegistry extend self - alias Key = Tuple(String, Symbol) + alias Key = Tuple(String, Symbol) @@entries = {} of Key => Deque(MethodStub) def add(type_name : String, method_name : Symbol, source : Source, args : Arguments) : Nil key = {type_name, method_name} list = if @@entries.has_key?(key) - @@entries[key] - else - @@entries[key] = Deque(MethodStub).new - end + @@entries[key] + else + @@entries[key] = Deque(MethodStub).new + end list << NilMethodStub.new(method_name, source, args) end