diff --git a/spec/spectator/mocks/arguments_spec.cr b/spec/spectator/mocks/arguments_spec.cr new file mode 100644 index 0000000..20ab31b --- /dev/null +++ b/spec/spectator/mocks/arguments_spec.cr @@ -0,0 +1,162 @@ +require "../../spec_helper" + +Spectator.describe Spectator::Arguments do + subject(arguments) do + Spectator::Arguments.new( + args: {42, "foo"}, + kwargs: {bar: "baz", qux: 123} + ) + end + + it "stores the arguments" do + expect(arguments.args).to eq({42, "foo"}) + end + + it "stores the keyword arguments" do + expect(arguments.kwargs).to eq({bar: "baz", qux: 123}) + end + + describe ".capture" do + subject { Spectator::Arguments.capture(42, "foo", bar: "baz", qux: 123) } + + it "stores the arguments and keyword arguments" do + is_expected.to have_attributes(args: {42, "foo"}, kwargs: {bar: "baz", qux: 123}) + end + end + + describe "#to_s" do + subject { arguments.to_s } + + it "formats the arguments" do + is_expected.to eq("(42, \"foo\", bar: \"baz\", qux: 123)") + end + end + + describe "#==" do + subject { arguments == other } + + context "with equal arguments" do + let(other) { arguments } + + it "returns true" do + is_expected.to be_true + end + end + + context "with different arguments" do + let(other) do + Spectator::Arguments.new( + args: {123, :foo, "bar"}, + kwargs: {opt: "foobar"} + ) + end + + it "returns false" do + is_expected.to be_false + end + end + + context "with the same kwargs in a different order" do + let(other) do + Spectator::Arguments.new( + args: arguments.args, + kwargs: {qux: 123, bar: "baz"} + ) + end + + it "returns true" do + is_expected.to be_true + end + end + + context "with a missing kwarg" do + let(other) do + Spectator::Arguments.new( + args: arguments.args, + kwargs: {bar: "baz"} + ) + end + + it "returns false" do + is_expected.to be_false + end + end + end + + describe "#===" do + subject { pattern === arguments } + + context "with equal arguments" do + let(pattern) { arguments } + + it "returns true" do + is_expected.to be_true + end + end + + context "with different arguments" do + let(pattern) do + Spectator::Arguments.new( + args: {123, :foo, "bar"}, + kwargs: {opt: "foobar"} + ) + end + + it "returns false" do + is_expected.to be_false + end + end + + context "with the same kwargs in a different order" do + let(pattern) do + Spectator::Arguments.new( + args: arguments.args, + kwargs: {qux: 123, bar: "baz"} + ) + end + + it "returns true" do + is_expected.to be_true + end + end + + context "with a missing kwarg" do + let(pattern) do + Spectator::Arguments.new( + args: arguments.args, + kwargs: {bar: "baz"} + ) + end + + it "returns false" do + is_expected.to be_false + end + end + + context "with matching types and regex" do + let(pattern) do + Spectator::Arguments.new( + args: {Int32, /foo/}, + kwargs: {bar: String, qux: 123} + ) + end + + it "returns true" do + is_expected.to be_true + end + end + + context "with different types and regex" do + let(pattern) do + Spectator::Arguments.new( + args: {Symbol, /bar/}, + kwargs: {bar: String, qux: 42} + ) + end + + it "returns false" do + is_expected.to be_false + end + end + end +end diff --git a/src/spectator/mocks/arguments.cr b/src/spectator/mocks/arguments.cr new file mode 100644 index 0000000..ed89947 --- /dev/null +++ b/src/spectator/mocks/arguments.cr @@ -0,0 +1,69 @@ +module Spectator + # Arguments used in a method call. + # + # Can also be used to match arguments. + # *T* must be a `Tuple` type representing the positional arguments. + # *NT* must be a `NamedTuple` type representing the keyword arguments. + class Arguments(T, NT) + # Positional arguments. + getter args : T + + # Keyword arguments. + getter kwargs : NT + + # Creates arguments used in a method call. + def initialize(@args : T, @kwargs : NT) + end + + # Constructs an instance from literal arguments. + def self.capture(*args, **kwargs) : self + new(args, kwargs) + end + + # Constructs a string representation of the arguments. + def to_s(io : IO) : Nil + io << '(' + + # Add the positional arguments. + args.each_with_index do |arg, i| + io << ", " if i > 0 + arg.inspect(io) + end + + # Add the keyword arguments. + size = args.size + kwargs.size + kwargs.each_with_index(args.size) do |k, v, i| + io << ", " if 0 < i < size + io << k << ": " + v.inspect(io) + end + + io << ')' + end + + # Checks if this set of arguments and another are equal. + def ==(other : Arguments) + args == other.args && kwargs == other.kwargs + end + + # Checks if another set of arguments matches this set of arguments. + def ===(other : Arguments) + args === other.args && named_tuples_match?(kwargs, other.kwargs) + end + + # Checks if two named tuples match. + # + # Uses case equality (`===`) on every key-value pair. + # NamedTuple doesn't have a `===` operator, even though Tuple does. + private def named_tuples_match?(a : NamedTuple, b : NamedTuple) + return false if a.size != b.size + + a.each do |k, v| + return false unless b.has_key?(k) + return false unless v === b[k] + end + + true + end + end +end