From e38e3ecc32567bbbbd2a4e5edef33096911f0d71 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 23 Oct 2022 15:22:50 -0600 Subject: [PATCH] Initial rework of arguments to support named positionals --- spec/spectator/mocks/arguments_spec.cr | 76 +++----------- src/spectator/mocks/arguments.cr | 135 +++++++++++++++++++------ src/spectator/mocks/stubbable.cr | 18 ++-- 3 files changed, 132 insertions(+), 97 deletions(-) diff --git a/spec/spectator/mocks/arguments_spec.cr b/spec/spectator/mocks/arguments_spec.cr index 422b6c6..f51c3c6 100644 --- a/spec/spectator/mocks/arguments_spec.cr +++ b/spec/spectator/mocks/arguments_spec.cr @@ -1,26 +1,22 @@ require "../../spec_helper" Spectator.describe Spectator::Arguments do - subject(arguments) do - Spectator::Arguments.new( - args: {42, "foo"}, - kwargs: {bar: "baz", qux: 123} - ) - end + subject(arguments) { Spectator::Arguments.new({42, "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } 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}) + expect(arguments).to have_attributes( + positional: {42, "foo"}, + splat_name: :splat, + extra: {:x, :y, :z}, + kwargs: {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}) + is_expected.to have_attributes(positional: {42, "foo"}, kwargs: {bar: "baz", qux: 123}) end end @@ -72,12 +68,7 @@ Spectator.describe Spectator::Arguments do end context "with different arguments" do - let(other) do - Spectator::Arguments.new( - args: {123, :foo, "bar"}, - kwargs: {opt: "foobar"} - ) - end + let(other) { Spectator::Arguments.new({123, :foo, "bar"}, nil, nil, {opt: "foobar"}) } it "returns false" do is_expected.to be_false @@ -85,12 +76,7 @@ Spectator.describe Spectator::Arguments do 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 + let(other) { Spectator::Arguments.new(arguments.positional, nil, nil, {qux: 123, bar: "baz"}) } it "returns true" do is_expected.to be_true @@ -98,12 +84,7 @@ Spectator.describe Spectator::Arguments do end context "with a missing kwarg" do - let(other) do - Spectator::Arguments.new( - args: arguments.args, - kwargs: {bar: "baz"} - ) - end + let(other) { Spectator::Arguments.new(arguments.positional, nil, nil, {bar: "baz"}) } it "returns false" do is_expected.to be_false @@ -123,12 +104,7 @@ Spectator.describe Spectator::Arguments do end context "with different arguments" do - let(pattern) do - Spectator::Arguments.new( - args: {123, :foo, "bar"}, - kwargs: {opt: "foobar"} - ) - end + let(pattern) { Spectator::Arguments.new({123, :foo, "bar"}, nil, nil, {opt: "foobar"}) } it "returns false" do is_expected.to be_false @@ -136,12 +112,7 @@ Spectator.describe Spectator::Arguments do 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 + let(pattern) { Spectator::Arguments.new(arguments.positional, nil, nil, {qux: 123, bar: "baz"}) } it "returns true" do is_expected.to be_true @@ -149,12 +120,7 @@ Spectator.describe Spectator::Arguments do end context "with a missing kwarg" do - let(pattern) do - Spectator::Arguments.new( - args: arguments.args, - kwargs: {bar: "baz"} - ) - end + let(pattern) { Spectator::Arguments.new(arguments.positional, nil, nil, {bar: "baz"}) } it "returns false" do is_expected.to be_false @@ -162,12 +128,7 @@ Spectator.describe Spectator::Arguments do end context "with matching types and regex" do - let(pattern) do - Spectator::Arguments.new( - args: {Int32, /foo/}, - kwargs: {bar: String, qux: 123} - ) - end + let(pattern) { Spectator::Arguments.new({Int32, /foo/}, nil, nil, {bar: String, qux: 123}) } it "returns true" do is_expected.to be_true @@ -175,12 +136,7 @@ Spectator.describe Spectator::Arguments do end context "with different types and regex" do - let(pattern) do - Spectator::Arguments.new( - args: {Symbol, /bar/}, - kwargs: {bar: String, qux: 42} - ) - end + let(pattern) { Spectator::Arguments.new({Symbol, /bar/}, nil, nil, {bar: String, qux: 42}) } it "returns false" do is_expected.to be_false diff --git a/src/spectator/mocks/arguments.cr b/src/spectator/mocks/arguments.cr index 264f0fc..db9da32 100644 --- a/src/spectator/mocks/arguments.cr +++ b/src/spectator/mocks/arguments.cr @@ -4,22 +4,29 @@ 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) < AbstractArguments + # *Positional* must be a `Tuple` or `NamedTuple` type representing the standard arguments. + # *Splat* must be a `Tuple` type representing the extra positional arguments. + # *DoubleSplat* must be a `NamedTuple` type representing extra keyword arguments. + class Arguments(Positional, Splat, DoubleSplat) < AbstractArguments # Positional arguments. - getter args : T + getter positional : Positional + + # Additional positional arguments. + getter extra : Splat # Keyword arguments. - getter kwargs : NT + getter kwargs : DoubleSplat + + # Name of the splat argument, if used. + getter splat_name : Symbol? # Creates arguments used in a method call. - def initialize(@args : T, @kwargs : NT) + def initialize(@positional : Positional, @splat_name : Symbol?, @extra : Splat, @kwargs : DoubleSplat) end - # Constructs an instance from literal arguments. - def self.capture(*args, **kwargs) : AbstractArguments - new(args, kwargs).as(AbstractArguments) + # Creates arguments used in a method call. + def self.new(positional : Positional, kwargs : DoubleSplat) + new(positional, nil, nil, kwargs) end # Instance of empty arguments. @@ -30,34 +37,80 @@ module Spectator nil.as(AbstractArguments?) end + # Captures arguments passed to a call. + def self.build(positional = Tuple.new, kwargs = NamedTuple.new) + new(positional, nil, nil, kwargs) + end + + # :ditto: + def self.build(positional : NamedTuple, splat_name : Symbol, extra : Tuple, kwargs = NamedTuple.new) + new(positional, splat_name, extra, kwargs) + end + + # Friendlier constructor for capturing arguments. + def self.capture(*args, **kwargs) + new(args, nil, nil, kwargs) + end + # Returns the positional argument at the specified index. def [](index : Int) - @args[index] + {% if Positional < NamedTuple %} + @positional.values[index] + {% else %} + @positional[index] + {% end %} end # Returns the specified named argument. def [](arg : Symbol) + {% if Positional < NamedTuple %} + return @positional[arg] if @positional.has_key?(arg) + {% end %} @kwargs[arg] end # Constructs a string representation of the arguments. def to_s(io : IO) : Nil - return io << "(no args)" if args.empty? && kwargs.empty? + return io << "(no args)" if positional.empty? && ((extra = @extra).nil? || extra.empty?) && kwargs.empty? io << '(' # Add the positional arguments. - args.each_with_index do |arg, i| - io << ", " if i > 0 - arg.inspect(io) + {% if Positional < NamedTuple %} + # Include argument names. + positional.each_with_index do |name, value, i| + io << ", " if i > 0 + io << name << ": " + value.inspect(io) + end + {% else %} + positional.each_with_index do |arg, i| + io << ", " if i > 0 + arg.inspect(io) + end + {% end %} + + # Add the splat arguments. + if (extra = @extra) && !extra.empty? + if splat = @splat_name + io << splat << ": {" + end + io << ", " unless positional.empty? + extra.each_with_index do |arg, i| + io << ", " if i > 0 + arg.inspect(io) + end + io << '}' if @splat_name + io << ", " unless kwargs.empty? 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) + offset = positional.size + offset += extra.size if (extra = @extra) + kwargs.each_with_index(offset) do |name, value, i| + io << ", " if i > 0 + io << name << ": " + value.inspect(io) end io << ')' @@ -65,27 +118,47 @@ module Spectator # Checks if this set of arguments and another are equal. def ==(other : Arguments) - args == other.args && kwargs == other.kwargs + ordered = simplify_positional + other_ordered = other.simplify_positional + ordered == other_ordered && 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 + {% if Positional < NamedTuple %} + if (other_positional = other.positional).is_a?(NamedTuple) + positional.each do |k, v| + return false unless other_positional.has_key?(k) + return false unless v === other_positional[k] + end + else + return false if positional.size != other_positional + positional.each_with_index do |k, v, i| + return false unless v === other_positional.unsafe_fetch(i) + end + end + {% else %} + return false unless positional === other.simplify_positional + {% 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 + if extra = @extra + return false unless extra === other.extra + end - a.each do |k, v| - return false unless b.has_key?(k) - return false unless v === b[k] + kwargs.each do |k, v| + return false unless other.kwargs.has_key?(k) + return false unless v === other.kwargs[k] end true end + + protected def simplify_positional + if (extra = @extra) + {% if Positional < NamedTuple %}positional.values{% else %}positional{% end %} + extra + else + {% if Positional < NamedTuple %}positional.values{% else %}positional{% end %} + end + end end end diff --git a/src/spectator/mocks/stubbable.cr b/src/spectator/mocks/stubbable.cr index 061cbe1..ae8d3f4 100644 --- a/src/spectator/mocks/stubbable.cr +++ b/src/spectator/mocks/stubbable.cr @@ -140,9 +140,12 @@ module Spectator ){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %} # Capture information about the call. - %args = ::Spectator::Arguments.capture( - {% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg.internal_name}}, {% end %} - {% if method.double_splat %}**{{method.double_splat}}{% end %} + %args = ::Spectator::Arguments.build( + ::NamedTuple.new( + {% for arg, i in method.args %}{% if !method.splat_index || i < method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %} + ), + {% if method.splat_index && (splat = method.args[method.splat_index].internal_name) %}{{splat.symbolize}}, {{splat}},{% end %} + {{method.double_splat}} ) %call = ::Spectator::MethodCall.new({{method.name.symbolize}}, %args) _spectator_record_call(%call) @@ -237,9 +240,12 @@ module Spectator ){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %} # Capture information about the call. - %args = ::Spectator::Arguments.capture( - {% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg.internal_name}}, {% end %} - {% if method.double_splat %}**{{method.double_splat}}{% end %} + %args = ::Spectator::Arguments.build( + ::NamedTuple.new( + {% for arg, i in method.args %}{% if !method.splat_index || i < method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %} + ), + {% if method.splat_index && (splat = method.args[method.splat_index].internal_name) %}{{splat.symbolize}}, {{splat}},{% end %} + {{method.double_splat}} ) %call = ::Spectator::MethodCall.new({{method.name.symbolize}}, %args) _spectator_record_call(%call)