Merge branch 'gh-49' into 'master'

Fix splat argument expansion in method redefinition

See merge request arctic-fox/spectator!36
This commit is contained in:
Mike Miller 2023-01-27 00:28:42 +00:00
commit 3852606b28
22 changed files with 75 additions and 58 deletions

View File

@ -5,15 +5,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
## [0.11.6] - 2023-01-26
### Added ### Added
- Added ability to cast types using the return value from expect/should statements with a type matcher. - Added ability to cast types using the return value from expect/should statements with a type matcher.
- Added support for string interpolation in context names/labels. - Added support for string interpolation in context names/labels.
### Fixed ### Fixed
- Fix invalid syntax (unterminated call) when recording calls to stubs with an un-named splat. - Fix invalid syntax (unterminated call) when recording calls to stubs with an un-named splat. [#51](https://github.com/icy-arctic-fox/spectator/issues/51)
- Fix malformed method signature when using named splat with keyword arguments in mocked type. [#49](https://github.com/icy-arctic-fox/spectator/issues/49)
### Changed ### Changed
- Expectations using 'should' syntax report file and line where the 'should' keyword is instead of the test start. - Expectations using 'should' syntax report file and line where the 'should' keyword is instead of the test start.
- Add non-captured block argument in preparation for Crystal 1.8.0.
## [0.11.5] - 2022-12-18 ## [0.11.5] - 2022-12-18
### Added ### Added
@ -444,7 +448,8 @@ This has been changed so that it compiles and raises an error at runtime with a
First version ready for public use. First version ready for public use.
[Unreleased]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.5...master [Unreleased]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.6...master
[0.11.6]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.5...v0.11.6
[0.11.5]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.4...v0.11.5 [0.11.5]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.4...v0.11.5
[0.11.4]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.3...v0.11.4 [0.11.4]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.3...v0.11.4
[0.11.3]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.2...v0.11.3 [0.11.3]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.2...v0.11.3

View File

@ -1,5 +1,5 @@
name: spectator name: spectator
version: 0.11.5 version: 0.11.6
description: | description: |
Feature-rich testing framework for Crystal inspired by RSpec. Feature-rich testing framework for Crystal inspired by RSpec.

View File

@ -0,0 +1,6 @@
require "../spec_helper"
# https://github.com/icy-arctic-fox/spectator/issues/49
Spectator.describe "GitHub Issue #49" do
# mock File
end

View File

@ -168,7 +168,7 @@ Spectator.describe "Double DSL", :smoke do
context "methods accepting blocks" do context "methods accepting blocks" do
double(:test7) do double(:test7) do
stub def foo stub def foo(&)
yield yield
end end

View File

@ -40,17 +40,17 @@ Spectator.describe "Mock DSL", :smoke do
arg arg
end end
def method4 : Symbol def method4(&) : Symbol
@_spectator_invocations << :method4 @_spectator_invocations << :method4
yield yield
end end
def method5 def method5(&)
@_spectator_invocations << :method5 @_spectator_invocations << :method5
yield.to_i yield.to_i
end end
def method6 def method6(&)
@_spectator_invocations << :method6 @_spectator_invocations << :method6
yield yield
end end
@ -60,7 +60,7 @@ Spectator.describe "Mock DSL", :smoke do
{arg, args, kwarg, kwargs} {arg, args, kwarg, kwargs}
end end
def method8(arg, *args, kwarg, **kwargs) def method8(arg, *args, kwarg, **kwargs, &)
@_spectator_invocations << :method8 @_spectator_invocations << :method8
yield yield
{arg, args, kwarg, kwargs} {arg, args, kwarg, kwargs}
@ -80,7 +80,7 @@ Spectator.describe "Mock DSL", :smoke do
"stubbed" "stubbed"
end end
stub def method4 : Symbol stub def method4(&) : Symbol
yield yield
:block :block
end end
@ -258,12 +258,12 @@ Spectator.describe "Mock DSL", :smoke do
# NOTE: Abstract methods that yield must have yield functionality defined in the method. # NOTE: Abstract methods that yield must have yield functionality defined in the method.
# This requires that yielding methods have a default implementation. # This requires that yielding methods have a default implementation.
# Just providing `&` in the arguments gets dropped by the compiler unless `yield` is in the method definition. # Just providing `&` in the arguments gets dropped by the compiler unless `yield` is in the method definition.
stub def method5 stub def method5(&)
yield yield
end end
# NOTE: Another quirk where a default implementation must be provided because `&` is dropped. # NOTE: Another quirk where a default implementation must be provided because `&` is dropped.
stub def method6 : Symbol stub def method6(&) : Symbol
yield yield
end end
@ -381,12 +381,12 @@ Spectator.describe "Mock DSL", :smoke do
# NOTE: Abstract methods that yield must have yield functionality defined in the method. # NOTE: Abstract methods that yield must have yield functionality defined in the method.
# This requires that yielding methods have a default implementation. # This requires that yielding methods have a default implementation.
# Just providing `&` in the arguments gets dropped by the compiler unless `yield` is in the method definition. # Just providing `&` in the arguments gets dropped by the compiler unless `yield` is in the method definition.
stub def method5 stub def method5(&)
yield yield
end end
# NOTE: Another quirk where a default implementation must be provided because `&` is dropped. # NOTE: Another quirk where a default implementation must be provided because `&` is dropped.
stub def method6 : Symbol stub def method6(&) : Symbol
yield yield
end end
end end
@ -454,12 +454,12 @@ Spectator.describe "Mock DSL", :smoke do
# NOTE: Abstract methods that yield must have yield functionality defined in the method. # NOTE: Abstract methods that yield must have yield functionality defined in the method.
# This requires that yielding methods have a default implementation. # This requires that yielding methods have a default implementation.
# Just providing `&` in the arguments gets dropped by the compiler unless `yield` is in the method definition. # Just providing `&` in the arguments gets dropped by the compiler unless `yield` is in the method definition.
stub def method5 stub def method5(&)
yield yield
end end
# NOTE: Another quirk where a default implementation must be provided because `&` is dropped. # NOTE: Another quirk where a default implementation must be provided because `&` is dropped.
stub def method6 : Symbol stub def method6(&) : Symbol
yield yield
end end
@ -577,12 +577,12 @@ Spectator.describe "Mock DSL", :smoke do
# NOTE: Abstract methods that yield must have yield functionality defined in the method. # NOTE: Abstract methods that yield must have yield functionality defined in the method.
# This requires that yielding methods have a default implementation. # This requires that yielding methods have a default implementation.
# Just providing `&` in the arguments gets dropped by the compiler unless `yield` is in the method definition. # Just providing `&` in the arguments gets dropped by the compiler unless `yield` is in the method definition.
stub def method5 stub def method5(&)
yield yield
end end
# NOTE: Another quirk where a default implementation must be provided because `&` is dropped. # NOTE: Another quirk where a default implementation must be provided because `&` is dropped.
stub def method6 : Symbol stub def method6(&) : Symbol
yield yield
end end
end end
@ -620,11 +620,11 @@ Spectator.describe "Mock DSL", :smoke do
:original :original
end end
def method3 def method3(&)
yield yield
end end
def method4 : Int32 def method4(&) : Int32
yield.to_i yield.to_i
end end
@ -749,11 +749,11 @@ Spectator.describe "Mock DSL", :smoke do
:original :original
end end
def method3 def method3(&)
yield yield
end end
def method4 : Int32 def method4(&) : Int32
yield.to_i yield.to_i
end end
@ -1108,17 +1108,17 @@ Spectator.describe "Mock DSL", :smoke do
arg arg
end end
def method4 : Symbol def method4(&) : Symbol
@_spectator_invocations << :method4 @_spectator_invocations << :method4
yield yield
end end
def method5 def method5(&)
@_spectator_invocations << :method5 @_spectator_invocations << :method5
yield.to_i yield.to_i
end end
def method6 def method6(&)
@_spectator_invocations << :method6 @_spectator_invocations << :method6
yield yield
end end
@ -1128,7 +1128,7 @@ Spectator.describe "Mock DSL", :smoke do
{arg, args, kwarg, kwargs} {arg, args, kwarg, kwargs}
end end
def method8(arg, *args, kwarg, **kwargs) def method8(arg, *args, kwarg, **kwargs, &)
@_spectator_invocations << :method8 @_spectator_invocations << :method8
yield yield
{arg, args, kwarg, kwargs} {arg, args, kwarg, kwargs}
@ -1148,7 +1148,7 @@ Spectator.describe "Mock DSL", :smoke do
"stubbed" "stubbed"
end end
stub def method4 : Symbol stub def method4(&) : Symbol
yield yield
:block :block
end end

View File

@ -156,7 +156,7 @@ Spectator.describe "Null double DSL" do
context "methods accepting blocks" do context "methods accepting blocks" do
double(:test7) do double(:test7) do
stub def foo stub def foo(&)
yield yield
end end

View File

@ -297,7 +297,7 @@ Spectator.describe Spectator::Double do
arg arg
end end
stub def self.baz(arg) stub def self.baz(arg, &)
yield yield
end end
end end

View File

@ -364,7 +364,7 @@ Spectator.describe Spectator::Mock do
arg arg
end end
def self.baz(arg) def self.baz(arg, &)
yield yield
end end
@ -929,7 +929,7 @@ Spectator.describe Spectator::Mock do
arg arg
end end
def self.baz(arg) def self.baz(arg, &)
yield yield
end end
end end

View File

@ -259,7 +259,7 @@ Spectator.describe Spectator::NullDouble do
arg arg
end end
stub def self.baz(arg) stub def self.baz(arg, &)
yield yield
end end
end end

View File

@ -5,7 +5,7 @@
# The reason for this is to prevent name collision when using the DSL to define a spec. # The reason for this is to prevent name collision when using the DSL to define a spec.
abstract class SpectatorContext abstract class SpectatorContext
# Evaluates the contents of a block within the scope of the context. # Evaluates the contents of a block within the scope of the context.
def eval def eval(&)
with self yield with self yield
end end

View File

@ -182,7 +182,7 @@ module Spectator::DSL
# expect(false).to be_true # expect(false).to be_true
# end # end
# ``` # ```
def aggregate_failures(label = nil) def aggregate_failures(label = nil, &)
::Spectator::Harness.current.aggregate_failures(label) do ::Spectator::Harness.current.aggregate_failures(label) do
yield yield
end end

View File

@ -11,7 +11,7 @@ module Spectator
end end
# Calls the `error` method on *visitor*. # Calls the `error` method on *visitor*.
def accept(visitor) def accept(visitor, &)
visitor.error(yield self) visitor.error(yield self)
end end

View File

@ -164,7 +164,7 @@ module Spectator
# The context casted to an instance of *klass* is provided as a block argument. # The context casted to an instance of *klass* is provided as a block argument.
# #
# TODO: Benchmark compiler performance using this method versus client-side casting in a proc. # TODO: Benchmark compiler performance using this method versus client-side casting in a proc.
protected def with_context(klass) protected def with_context(klass, &)
context = klass.cast(@context) context = klass.cast(@context)
with context yield with context yield
end end
@ -184,7 +184,7 @@ module Spectator
end end
# Yields this example and all parent groups. # Yields this example and all parent groups.
def ascend def ascend(&)
node = self node = self
while node while node
yield node yield node
@ -279,7 +279,7 @@ module Spectator
# The block given to this method will be executed within the test context. # The block given to this method will be executed within the test context.
# #
# TODO: Benchmark compiler performance using this method versus client-side casting in a proc. # TODO: Benchmark compiler performance using this method versus client-side casting in a proc.
protected def with_context(klass) protected def with_context(klass, &)
context = @example.cast_context(klass) context = @example.cast_context(klass)
with context yield with context yield
end end

View File

@ -87,7 +87,7 @@ module Spectator
delegate size, unsafe_fetch, to: @nodes delegate size, unsafe_fetch, to: @nodes
# Yields this group and all parent groups. # Yields this group and all parent groups.
def ascend def ascend(&)
group = self group = self
while group while group
yield group yield group

View File

@ -24,7 +24,7 @@ module Spectator
end end
# Calls the `failure` method on *visitor*. # Calls the `failure` method on *visitor*.
def accept(visitor) def accept(visitor, &)
visitor.fail(yield self) visitor.fail(yield self)
end end

View File

@ -13,7 +13,7 @@ module Spectator::Formatting::Components
end end
# Increases the indent by the a specific *amount* for the duration of the block. # Increases the indent by the a specific *amount* for the duration of the block.
private def indent(amount = INDENT) private def indent(amount = INDENT, &)
@indent += amount @indent += amount
yield yield
@indent -= amount @indent -= amount
@ -23,7 +23,7 @@ module Spectator::Formatting::Components
# The contents of the line should be generated by a block provided to this method. # The contents of the line should be generated by a block provided to this method.
# Ensure that _only_ one line is produced by the block, # Ensure that _only_ one line is produced by the block,
# otherwise the indent will be lost. # otherwise the indent will be lost.
private def line(io) private def line(io, &)
@indent.times { io << ' ' } @indent.times { io << ' ' }
yield yield
io.puts io.puts

View File

@ -43,7 +43,7 @@ module Spectator
# The value of `.current` is set to the harness for the duration of the test. # The value of `.current` is set to the harness for the duration of the test.
# It will be reset after the test regardless of the outcome. # It will be reset after the test regardless of the outcome.
# The result of running the test code will be returned. # The result of running the test code will be returned.
def self.run : Result def self.run(&) : Result
with_harness do |harness| with_harness do |harness|
harness.run { yield } harness.run { yield }
end end
@ -53,7 +53,7 @@ module Spectator
# The `.current` harness is set to the new harness for the duration of the block. # The `.current` harness is set to the new harness for the duration of the block.
# `.current` is reset to the previous value (probably nil) afterwards, even if the block raises. # `.current` is reset to the previous value (probably nil) afterwards, even if the block raises.
# The result of the block is returned. # The result of the block is returned.
private def self.with_harness private def self.with_harness(&)
previous = @@current previous = @@current
begin begin
@@current = harness = new @@current = harness = new
@ -70,7 +70,7 @@ module Spectator
# Runs test code and produces a result based on the outcome. # Runs test code and produces a result based on the outcome.
# The test code should be called from within the block given to this method. # The test code should be called from within the block given to this method.
def run : Result def run(&) : Result
elapsed, error = capture { yield } elapsed, error = capture { yield }
elapsed2, error2 = capture { run_deferred } elapsed2, error2 = capture { run_deferred }
run_cleanup run_cleanup
@ -106,7 +106,7 @@ module Spectator
@cleanup << block @cleanup << block
end end
def aggregate_failures(label = nil) def aggregate_failures(label = nil, &)
previous = @aggregate previous = @aggregate
@aggregate = aggregate = [] of Expectation @aggregate = aggregate = [] of Expectation
begin begin
@ -135,7 +135,7 @@ module Spectator
# Yields to run the test code and returns information about the outcome. # Yields to run the test code and returns information about the outcome.
# Returns a tuple with the elapsed time and an error if one occurred (otherwise nil). # Returns a tuple with the elapsed time and an error if one occurred (otherwise nil).
private def capture : Tuple(Time::Span, Exception?) private def capture(&) : Tuple(Time::Span, Exception?)
error = nil error = nil
elapsed = Time.measure do elapsed = Time.measure do
error = catch { yield } error = catch { yield }
@ -146,7 +146,7 @@ module Spectator
# Yields to run a block of code and captures exceptions. # Yields to run a block of code and captures exceptions.
# If the block of code raises an error, the error is caught and returned. # If the block of code raises an error, the error is caught and returned.
# If the block doesn't raise an error, then nil is returned. # If the block doesn't raise an error, then nil is returned.
private def catch : Exception? private def catch(&) : Exception?
yield yield
rescue e rescue e
e e

View File

@ -97,7 +97,7 @@ module Spectator::Matchers
# Runs a block of code and returns the exception it threw. # Runs a block of code and returns the exception it threw.
# If no exception was thrown, *nil* is returned. # If no exception was thrown, *nil* is returned.
private def capture_exception private def capture_exception(&)
exception = nil exception = nil
begin begin
yield yield

View File

@ -133,13 +133,12 @@ module Spectator
if method.splat_index if method.splat_index
method.args.each_with_index do |arg, i| method.args.each_with_index do |arg, i|
if i == method.splat_index if i == method.splat_index
original += '*'
if arg.internal_name && arg.internal_name.size > 0 if arg.internal_name && arg.internal_name.size > 0
original += "#{arg.internal_name}, " original += "*#{arg.internal_name}, "
end end
original += "**#{method.double_splat}, " if method.double_splat original += "**#{method.double_splat}, " if method.double_splat
elsif i > method.splat_index elsif i > method.splat_index
original += "#{arg.name}: #{arg.internal_name}" original += "#{arg.name}: #{arg.internal_name}, "
else else
original += "#{arg.internal_name}, " original += "#{arg.internal_name}, "
end end
@ -283,9 +282,8 @@ module Spectator
if method.splat_index if method.splat_index
method.args.each_with_index do |arg, i| method.args.each_with_index do |arg, i|
if i == method.splat_index if i == method.splat_index
original += '*'
if arg.internal_name && arg.internal_name.size > 0 if arg.internal_name && arg.internal_name.size > 0
original += "#{arg.internal_name}, " original += "*#{arg.internal_name}, "
end end
original += "**#{method.double_splat}, " if method.double_splat original += "**#{method.double_splat}, " if method.double_splat
elsif i > method.splat_index elsif i > method.splat_index
@ -539,6 +537,7 @@ module Spectator
# Get the value as-is from the stub. # Get the value as-is from the stub.
# This will be compiled as a union of all known stubbed value types. # This will be compiled as a union of all known stubbed value types.
%value = {{stub}}.call({{call}}) %value = {{stub}}.call({{call}})
%type = {{type}}
# Attempt to cast the value to the method's return type. # Attempt to cast the value to the method's return type.
# If successful, which it will be in most cases, return it. # If successful, which it will be in most cases, return it.
@ -549,12 +548,12 @@ module Spectator
%cast %cast
{% elsif fail_cast == :raise %} {% elsif fail_cast == :raise %}
# Check if nil was returned by the stub and if its okay to return it. # Check if nil was returned by the stub and if its okay to return it.
if %value.nil? && {{type}}.nilable? if %value.nil? && %type.nilable?
# Value was nil and nil is allowed to be returned. # Value was nil and nil is allowed to be returned.
%cast.as({{type}}) %type.cast(%cast)
elsif %cast.nil? elsif %cast.nil?
# The stubbed value was something else entirely and cannot be cast to the return type. # The stubbed value was something else entirely and cannot be cast to the return type.
raise TypeCastError.new("#{_spectator_stubbed_name} received message #{ {{call}} } and is attempting to return a `#{%value.class}`, but returned type must be `#{ {{type}} }`.") raise TypeCastError.new("#{_spectator_stubbed_name} received message #{ {{call}} } and is attempting to return a `#{%value.class}`, but returned type must be `#{%type}`.")
else else
# Types match and value can be returned as cast type. # Types match and value can be returned as cast type.
%cast %cast

View File

@ -9,7 +9,7 @@ module Spectator
end end
# Calls the `pass` method on *visitor*. # Calls the `pass` method on *visitor*.
def accept(visitor) def accept(visitor, &)
visitor.pass(yield self) visitor.pass(yield self)
end end

View File

@ -28,7 +28,7 @@ module Spectator
end end
# Calls the `pending` method on the *visitor*. # Calls the `pending` method on the *visitor*.
def accept(visitor) def accept(visitor, &)
visitor.pending(yield self) visitor.pending(yield self)
end end

7
util/nightly.sh Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env sh
set -e
readonly image=crystallang/crystal:nightly
readonly code=/project
docker run -it -v "$PWD:${code}" -w "${code}" "${image}" crystal spec "$@"