From e2130d12d34626e0d5d6de547d7417008351f34a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 23 Oct 2022 20:42:08 -0600 Subject: [PATCH] Implement arguments case equality Implements https://github.com/icy-arctic-fox/spectator/issues/47 Some specs are failing and need to be resolved before the new feature is considered done. --- spec/issues/github_issue_47_spec.cr | 2 +- spec/spectator/mocks/arguments_spec.cr | 202 +++++++++++++++++++------ src/spectator/mocks/arguments.cr | 66 +++++--- 3 files changed, 205 insertions(+), 65 deletions(-) diff --git a/spec/issues/github_issue_47_spec.cr b/spec/issues/github_issue_47_spec.cr index 8b1d3d3..3576a2d 100644 --- a/spec/issues/github_issue_47_spec.cr +++ b/spec/issues/github_issue_47_spec.cr @@ -11,7 +11,7 @@ Spectator.describe "GitHub Issue #47" do let(fake) { mock(Original) } - xspecify do + specify do expect(fake).to receive(:foo).with("arg1", arg2: "arg2") fake.foo("arg1", "arg2") end diff --git a/spec/spectator/mocks/arguments_spec.cr b/spec/spectator/mocks/arguments_spec.cr index d2f2d2a..8b43b95 100644 --- a/spec/spectator/mocks/arguments_spec.cr +++ b/spec/spectator/mocks/arguments_spec.cr @@ -20,18 +20,61 @@ Spectator.describe Spectator::Arguments do end end - describe "#[]" do - context "with an index" do + describe "#[](index)" do + it "returns a positional argument" do + aggregate_failures do + expect(arguments[0]).to eq(42) + expect(arguments[1]).to eq("foo") + end + end + + it "returns splat arguments" do + aggregate_failures do + expect(arguments[2]).to eq(:x) + expect(arguments[3]).to eq(:y) + expect(arguments[4]).to eq(:z) + end + end + + context "with named positional arguments" do + subject(arguments) { Spectator::Arguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } + it "returns a positional argument" do aggregate_failures do expect(arguments[0]).to eq(42) expect(arguments[1]).to eq("foo") end end + + it "returns splat arguments" do + aggregate_failures do + expect(arguments[2]).to eq(:x) + expect(arguments[3]).to eq(:y) + expect(arguments[4]).to eq(:z) + end + end + end + end + + describe "#[](symbol)" do + it "returns a keyword argument" do + aggregate_failures do + expect(arguments[:bar]).to eq("baz") + expect(arguments[:qux]).to eq(123) + end end - context "with a symbol" do - it "returns a named argument" do + context "with named positional arguments" do + subject(arguments) { Spectator::Arguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } + + it "returns a positional argument" do + aggregate_failures do + expect(arguments[:arg1]).to eq(42) + expect(arguments[:arg2]).to eq("foo") + end + end + + it "returns a keyword argument" do aggregate_failures do expect(arguments[:bar]).to eq("baz") expect(arguments[:qux]).to eq(123) @@ -62,33 +105,57 @@ Spectator.describe Spectator::Arguments do context "with equal arguments" do let(other) { arguments } - it "returns true" do - is_expected.to be_true - end + it { is_expected.to be_true } end context "with different arguments" do - let(other) { Spectator::Arguments.new({123, :foo, "bar"}, nil, nil, {opt: "foobar"}) } + let(other) { Spectator::Arguments.new({123, :foo, "bar"}, :splat, {1, 2, 3}, {opt: "foobar"}) } - it "returns false" do - is_expected.to be_false - end + it { is_expected.to be_false } end context "with the same kwargs in a different order" do - let(other) { Spectator::Arguments.new(arguments.args, nil, nil, {qux: 123, bar: "baz"}) } + let(other) { Spectator::Arguments.new(arguments.args, arguments.splat_name, arguments.splat, {qux: 123, bar: "baz"}) } - it "returns true" do - is_expected.to be_true - end + it { is_expected.to be_true } end context "with a missing kwarg" do - let(other) { Spectator::Arguments.new(arguments.args, nil, nil, {bar: "baz"}) } + let(other) { Spectator::Arguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: "baz"}) } - it "returns false" do - is_expected.to be_false - end + it { is_expected.to be_false } + end + + context "with an extra kwarg" do + let(other) { Spectator::Arguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: "baz", qux: 123, extra: 0}) } + + it { is_expected.to be_false } + end + + context "with different splat arguments" do + let(other) { Spectator::Arguments.new(arguments.args, arguments.splat_name, {1, 2, 3}, arguments.kwargs) } + + it { is_expected.to be_false } + end + + context "with mixed positional tuple types" do + let(other) { Spectator::Arguments.new({arg1: 42, arg2: "foo"}, arguments.splat_name, arguments.splat, arguments.kwargs) } + + it { is_expected.to be_true } + end + + context "with mixed positional tuple types (flipped)" do + let(arguments) { Spectator::Arguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } + let(other) { Spectator::Arguments.new({42, "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } + + it { is_expected.to be_true } + end + + context "with args spilling over into splat and mixed positional tuple types" do + let(arguments) { Spectator::Arguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } + let(other) { Spectator::Arguments.new({42, "foo", :x, :y, :z}, nil, nil, {bar: "baz", qux: 123}) } + + it { is_expected.to be_true } end end @@ -98,49 +165,96 @@ Spectator.describe Spectator::Arguments do context "with equal arguments" do let(pattern) { arguments } - it "returns true" do - is_expected.to be_true - end + it { is_expected.to be_true } + end + + context "with matching arguments" do + let(pattern) { Spectator::Arguments.new({Int32, /foo/}, :splat, {Symbol, Symbol, :z}, {bar: /baz/, qux: Int32}) } + + it { is_expected.to be_true } + end + + context "with non-matching arguments" do + let(pattern) { Spectator::Arguments.new({Float64, /bar/}, :splat, {String, Int32, :x}, {bar: /foo/, qux: "123"}) } + + it { is_expected.to be_false } end context "with different arguments" do - let(pattern) { Spectator::Arguments.new({123, :foo, "bar"}, nil, nil, {opt: "foobar"}) } + let(pattern) { Spectator::Arguments.new({123, :foo, "bar"}, :splat, {1, 2, 3}, {opt: "foobar"}) } - it "returns false" do - is_expected.to be_false - end + it { is_expected.to be_false } end context "with the same kwargs in a different order" do - let(pattern) { Spectator::Arguments.new(arguments.args, nil, nil, {qux: 123, bar: "baz"}) } + let(pattern) { Spectator::Arguments.new(arguments.args, arguments.splat_name, arguments.splat, {qux: Int32, bar: /baz/}) } - it "returns true" do - is_expected.to be_true - end + it { is_expected.to be_true } + end + + context "with an additional kwarg" do + let(pattern) { Spectator::Arguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: /baz/}) } + + it { is_expected.to be_true } end context "with a missing kwarg" do - let(pattern) { Spectator::Arguments.new(arguments.args, nil, nil, {bar: "baz"}) } + let(pattern) { Spectator::Arguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: /baz/, qux: Int32, extra: 0}) } - it "returns false" do - is_expected.to be_false - end + it { is_expected.to be_false } end - context "with matching types and regex" do - let(pattern) { Spectator::Arguments.new({Int32, /foo/}, nil, nil, {bar: String, qux: 123}) } + context "with different splat arguments" do + let(pattern) { Spectator::Arguments.new(arguments.args, arguments.splat_name, {1, 2, 3}, arguments.kwargs) } - it "returns true" do - is_expected.to be_true - end + it { is_expected.to be_false } end - context "with different types and regex" do - let(pattern) { Spectator::Arguments.new({Symbol, /bar/}, nil, nil, {bar: String, qux: 42}) } + context "with matching mixed positional tuple types" do + let(pattern) { Spectator::Arguments.new({arg1: Int32, arg2: /foo/}, arguments.splat_name, arguments.splat, arguments.kwargs) } - it "returns false" do - is_expected.to be_false - end + it { is_expected.to be_true } + end + + context "with non-matching mixed positional tuple types" do + let(pattern) { Spectator::Arguments.new({arg1: Float64, arg2: /bar/}, arguments.splat_name, arguments.splat, arguments.kwargs) } + + it { is_expected.to be_false } + end + + context "with matching args spilling over into splat and mixed positional tuple types" do + let(arguments) { Spectator::Arguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } + let(pattern) { Spectator::Arguments.capture(Int32, /foo/, Symbol, Symbol, :z, bar: /baz/, qux: Int32) } + + it { is_expected.to be_true } + end + + context "with non-matching args spilling over into splat and mixed positional tuple types" do + let(arguments) { Spectator::Arguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } + let(pattern) { Spectator::Arguments.capture(Float64, /bar/, Symbol, String, :z, bar: /foo/, qux: Int32) } + + it { is_expected.to be_false } + end + + context "with matching mixed named positional and keyword arguments" do + let(arguments) { Spectator::Arguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } + let(pattern) { Spectator::Arguments.capture(/foo/, Symbol, :y, Symbol, arg1: Int32, bar: /baz/, qux: 123) } + + it { is_expected.to be_true } + end + + context "with non-matching mixed named positional and keyword arguments" do + let(arguments) { Spectator::Arguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } + let(pattern) { Spectator::Arguments.capture(5, Symbol, :z, Symbol, arg2: /foo/, bar: /baz/, qux: Int32) } + + it { is_expected.to be_false } + end + + context "with non-matching mixed named positional and keyword arguments" do + let(arguments) { Spectator::Arguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } + let(pattern) { Spectator::Arguments.capture(/bar/, String, :y, Symbol, arg1: 0, bar: /foo/, qux: Float64) } + + it { is_expected.to be_false } end end end diff --git a/src/spectator/mocks/arguments.cr b/src/spectator/mocks/arguments.cr index b033c71..965d52b 100644 --- a/src/spectator/mocks/arguments.cr +++ b/src/spectator/mocks/arguments.cr @@ -74,6 +74,15 @@ module Spectator end end + # Returns all named positional and keyword arguments as a named tuple. + def named : NamedTuple + {% if Args < NamedTuple %} + args.merge(kwargs) + {% else %} + kwargs + {% end %} + end + # Constructs a string representation of the arguments. def to_s(io : IO) : Nil return io << "(no args)" if args.empty? && ((splat = @splat).nil? || splat.empty?) && kwargs.empty? @@ -127,31 +136,48 @@ module Spectator # Checks if another set of arguments matches this set of arguments. def ===(other : Arguments) - {% if Args < NamedTuple %} - if (other_args = other.args).is_a?(NamedTuple) - args.each do |k, v| - return false unless other_args.has_key?(k) - return false unless v === other_args[k] - end - else - return false if args.size != other_args - args.each_with_index do |k, v, i| - return false unless v === other_args.unsafe_fetch(i) - end - end - {% else %} - return false unless args === other.positional - {% end %} + self_args = args + other_args = other.args - if splat = @splat - return false unless splat === other.splat + case {self_args, other_args} + when {Tuple, Tuple} then compare(positional, other.positional, kwargs, other.kwargs) + when {Tuple, NamedTuple} then compare(kwargs, other.named, positional, other_args, other.splat) + when {NamedTuple, Tuple} then compare(positional, other.positional, kwargs, other.kwargs) + else + self_args === other_args && (!splat || splat === other.splat) && compare_named_tuples(kwargs, other.kwargs) + end + end + + private def compare(self_positional : Tuple, other_positional : Tuple, self_kwargs : NamedTuple, other_kwargs : NamedTuple) + self_positional === other_positional && compare_named_tuples(self_kwargs, other_kwargs) + end + + private def compare(self_kwargs : NamedTuple, other_named : NamedTuple, self_positional : Tuple, other_args : NamedTuple, other_splat : Tuple?) + return false unless compare_named_tuples(self_kwargs, other_named) + + i = 0 + other_args.each do |k, v2| + next if self_kwargs.has_key?(k) # Covered by named arguments. + + v1 = self_positional.fetch(i) { return false } + i += 1 + return false unless v1 === v2 end - kwargs.each do |k, v| - return false unless other.kwargs.has_key?(k) - return false unless v === other.kwargs[k] + other_splat.try &.each do |v2| + v1 = self_positional.fetch(i) { return false } + i += 1 + return false unless v1 === v2 end + i == self_positional.size + end + + private def compare_named_tuples(a : NamedTuple, b : NamedTuple) + a.each do |k, v1| + v2 = b.fetch(k) { return false } + return false unless v1 === v2 + end true end end