From a49f8eaa71af70e1af3af56827c3605d03e349d8 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 26 Aug 2019 21:04:54 -0600 Subject: [PATCH 001/142] 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/142] 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 70e01364cefe2d882afda338fdaf8d42b8af1780 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 26 Aug 2019 21:11:21 -0600 Subject: [PATCH 003/142] 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 004/142] 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 005/142] 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 c458a490bf5db68537041258c056e22176a7f5dc Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 4 Oct 2019 23:01:46 -0600 Subject: [PATCH 006/142] 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 007/142] 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 008/142] 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 009/142] 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 010/142] 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 011/142] 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 012/142] 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 013/142] 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 014/142] 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 015/142] 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 016/142] 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 017/142] 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 018/142] 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 019/142] 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 020/142] 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 021/142] 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 022/142] 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 023/142] 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 024/142] 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 025/142] 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 026/142] 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 027/142] 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 028/142] 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 029/142] 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 030/142] 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 031/142] 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 032/142] 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 033/142] 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 034/142] 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 035/142] 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 036/142] 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 037/142] 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 038/142] 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 039/142] 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 040/142] 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 041/142] 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 042/142] 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 043/142] 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 044/142] 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 045/142] 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 046/142] 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 047/142] 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 048/142] 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 049/142] 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 050/142] 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 051/142] 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 052/142] 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 053/142] 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 054/142] 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 055/142] 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 056/142] 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 057/142] 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 058/142] 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 059/142] 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 060/142] 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 061/142] 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 062/142] 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 063/142] 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 064/142] 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 065/142] 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 066/142] 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 067/142] 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 068/142] 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 069/142] 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 070/142] 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 071/142] 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 072/142] 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 073/142] 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 074/142] 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 075/142] 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 076/142] 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 077/142] 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 078/142] 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 079/142] 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 080/142] 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 081/142] 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 082/142] 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 083/142] 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 084/142] 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 085/142] 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 086/142] 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 087/142] 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 088/142] 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 089/142] 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 090/142] 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 091/142] 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 092/142] 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 093/142] 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 094/142] 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 095/142] 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 096/142] 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 097/142] 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 098/142] 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 763c99f33868db8b242b705ed5bd775b1d04d840 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 14 Nov 2019 18:48:43 -0700 Subject: [PATCH 099/142] 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 100/142] 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 101/142] 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 102/142] 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 103/142] 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 104/142] 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 105/142] 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 106/142] 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 107/142] 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 108/142] 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 109/142] 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 110/142] 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 111/142] 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 112/142] 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 113/142] 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 114/142] 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 115/142] 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 116/142] 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 117/142] 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 118/142] 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 119/142] 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 120/142] 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 121/142] 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 122/142] 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 123/142] 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 124/142] 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 125/142] 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 126/142] 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 127/142] 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 128/142] 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 129/142] 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 130/142] 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 131/142] 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 132/142] 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 133/142] 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 134/142] 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 135/142] 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 136/142] 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 137/142] 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 138/142] 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 139/142] 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 140/142] 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 141/142] 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 142/142] 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