Compare commits

..

No commits in common. "master" and "v0.11.0" have entirely different histories.

114 changed files with 828 additions and 3441 deletions

2
.gitignore vendored
View file

@ -10,5 +10,3 @@
# Ignore JUnit output # Ignore JUnit output
output.xml output.xml
/test.cr

View file

@ -13,7 +13,7 @@ before_script:
spec: spec:
script: script:
- crystal spec --error-on-warnings --junit_output=. spec/matchers/ spec/spectator/*.cr - crystal spec --error-on-warnings --junit_output=. spec/runtime_example_spec.cr spec/matchers/ spec/spectator/*.cr
artifacts: artifacts:
when: always when: always
paths: paths:

View file

@ -4,82 +4,6 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 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).
## [0.12.1] - 2024-08-13
### Fixed
- Fixed some global namespace issues with Crystal 1.13. [#57](https://github.com/icy-arctic-fox/spectator/pull/57) Thanks @GrantBirki !
- Remove usage of deprecated double splat in macros.
## [0.12.0] - 2024-02-03
### Added
- Added ability to use matchers for case equality. [#55](https://github.com/icy-arctic-fox/spectator/issues/55)
- Added support for nested case equality when checking arguments with Array, Tuple, Hash, and NamedTuple.
### Fixed
- Fixed some issues with the `be_within` matcher when used with expected and union types.
## [0.11.7] - 2023-10-16
### Fixed
- Fix memoized value (`let`) with a union type causing segfault. [#81](https://gitlab.com/arctic-fox/spectator/-/issues/81)
## [0.11.6] - 2023-01-26
### Added
- 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.
### Fixed
- 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
- 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
### Added
- Added support for mock modules and types that include mocked modules.
### Fixed
- Fix macro logic to support free variables, 'self', and variants on stubbed methods. [#48](https://github.com/icy-arctic-fox/spectator/issues/48)
- Fix method stubs used on methods that capture blocks.
- Fix type name resolution for when using custom types in a mocked typed.
- Prevent comparing range arguments with non-compatible types in stubs. [#48](https://github.com/icy-arctic-fox/spectator/issues/48)
### Changed
- Simplify string representation of mock-related types.
- Remove unnecessary redefinitions of methods when adding stub functionality to a type.
- Allow metadata to be stored as nil to reduce overhead when tracking nodes without tags.
- Use normal equality (==) instead of case-equality (===) with proc arguments in stubs.
- Change stub value cast logic to avoid compiler bug. [#80](https://gitlab.com/arctic-fox/spectator/-/issues/80)
## [0.11.4] - 2022-11-27
### Added
- Add support for using named (keyword) arguments in place of positional arguments in stubs. [#47](https://github.com/icy-arctic-fox/spectator/issues/47)
- Add `before`, `after`, and `around` as aliases for `before_each`, `after_each`, and `around_each` respectively.
### Fixed
- Clear stubs defined with `expect().to receive()` syntax after test finishes to prevent leakage between tests.
- Ensure stubs defined with `allow().to receive()` syntax are cleared after test finishes when used inside a test (another leakage).
- Fix crash caused when logging is enabled after running an example that attempts to exit.
### Removed
- Removed support for stubbing undefined (untyped) methods in lazy doubles. Avoids possible segfault.
## [0.11.3] - 2022-09-03
### Fixed
- Display error block (failure message and stack trace) when using `fail`. [#78](https://gitlab.com/arctic-fox/spectator/-/issues/78)
- Defining a custom matcher outside of the `Spectator` namespace no longer produces a compilation error. [#46](https://github.com/icy-arctic-fox/spectator/issues/46)
## [0.11.2] - 2022-08-07
### Fixed
- `expect_raises` with block and no arguments produces compilation error. [#77](https://gitlab.com/arctic-fox/spectator/-/issues/77)
### Changed
- `-e` (`--example`) CLI option performs a partial match instead of exact match. [#71](https://gitlab.com/arctic-fox/spectator/-/issues/71) [#45](https://github.com/icy-arctic-fox/spectator/issues/45)
## [0.11.1] - 2022-07-18
### Fixed
- Workaround nilable type issue with memoized value. [#76](https://gitlab.com/arctic-fox/spectator/-/issues/76)
## [0.11.0] - 2022-07-14 ## [0.11.0] - 2022-07-14
### Changed ### Changed
- Overhauled mock and double system. [#63](https://gitlab.com/arctic-fox/spectator/-/issues/63) - Overhauled mock and double system. [#63](https://gitlab.com/arctic-fox/spectator/-/issues/63)
@ -463,16 +387,7 @@ 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.12.1...master [Unreleased]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.0...master
[0.12.1]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.12.0...v0.12.1
[0.12.0]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.7...v0.12.0
[0.11.7]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.6...v0.11.7
[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.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.2]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.1...v0.11.2
[0.11.1]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.0...v0.11.1
[0.11.0]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.10.6...v0.11.0 [0.11.0]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.10.6...v0.11.0
[0.10.6]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.10.5...v0.10.6 [0.10.6]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.10.5...v0.10.6
[0.10.5]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.10.4...v0.10.5 [0.10.5]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.10.4...v0.10.5

View file

@ -25,7 +25,7 @@ Add this to your application's `shard.yml`:
development_dependencies: development_dependencies:
spectator: spectator:
gitlab: arctic-fox/spectator gitlab: arctic-fox/spectator
version: ~> 0.12.0 version: ~> 0.11.0
``` ```
Usage Usage
@ -287,7 +287,7 @@ Spectator.describe Driver do
# Call the mock method. # Call the mock method.
subject.do_something(interface, dbl) subject.do_something(interface, dbl)
# Verify everything went okay. # Verify everything went okay.
expect(interface).to have_received(:invoke).with(dbl) expect(interface).to have_received(:invoke).with(thing)
end end
end end
``` ```

View file

@ -1,16 +1,16 @@
name: spectator name: spectator
version: 0.12.1 version: 0.11.0
description: | description: |
Feature-rich testing framework for Crystal inspired by RSpec. A feature-rich spec testing framework for Crystal with similarities to RSpec.
authors: authors:
- Michael Miller <icy.arctic.fox@gmail.com> - Michael Miller <icy.arctic.fox@gmail.com>
crystal: 1.6.0 crystal: 1.5.0
license: MIT license: MIT
development_dependencies: development_dependencies:
ameba: ameba:
github: crystal-ameba/ameba github: crystal-ameba/ameba
version: ~> 1.2.0 version: ~> 1.0.0

View file

@ -1,91 +0,0 @@
require "../spec_helper"
# https://gitlab.com/arctic-fox/spectator/-/wikis/Custom-Matchers
Spectator.describe "Custom Matchers Docs" do
context "value matcher" do
# Sub-type of Matcher to suit our needs.
# Notice this is a struct.
struct MultipleOfMatcher(ExpectedType) < Spectator::Matchers::ValueMatcher(ExpectedType)
# Short text about the matcher's purpose.
# This explains what condition satisfies the matcher.
# The description is used when the one-liner syntax is used.
def description : String
"is a multiple of #{expected.label}"
end
# Checks whether the matcher is satisfied with the expression given to it.
private def match?(actual : Spectator::Expression(T)) : Bool forall T
actual.value % expected.value == 0
end
# Message displayed when the matcher isn't satisfied.
# The message should typically only contain the test expression labels.
private def failure_message(actual : Spectator::Expression(T)) : String forall T
"#{actual.label} is not a multiple of #{expected.label}"
end
# Message displayed when the matcher isn't satisfied and is negated.
# This is essentially what would satisfy the matcher if it wasn't negated.
# The message should typically only contain the test expression labels.
private def failure_message_when_negated(actual : Spectator::Expression(T)) : String forall T
"#{actual.label} is a multiple of #{expected.label}"
end
end
# The DSL portion of the matcher.
# This captures the test expression and creates an instance of the matcher.
macro be_a_multiple_of(expected)
%value = ::Spectator::Value.new({{expected}}, {{expected.stringify}})
MultipleOfMatcher.new(%value)
end
specify do
expect(9).to be_a_multiple_of(3)
# or negated:
expect(5).to_not be_a_multiple_of(2)
end
specify "failure messages" do
expect { expect(9).to be_a_multiple_of(5) }.to raise_error(Spectator::ExpectationFailed, "9 is not a multiple of 5")
expect { expect(6).to_not be_a_multiple_of(3) }.to raise_error(Spectator::ExpectationFailed, "6 is a multiple of 3")
end
end
context "standard matcher" do
struct OddMatcher < Spectator::Matchers::StandardMatcher
def description : String
"is odd"
end
private def match?(actual : Spectator::Expression(T)) : Bool forall T
actual.value % 2 == 1
end
private def failure_message(actual : Spectator::Expression(T)) : String forall T
"#{actual.label} is not odd"
end
private def failure_message_when_negated(actual : Spectator::Expression(T)) : String forall T
"#{actual.label} is odd"
end
private def does_not_match?(actual : Spectator::Expression(T)) : Bool forall T
actual.value % 2 == 0
end
end
macro be_odd
OddMatcher.new
end
specify do
expect(9).to be_odd
expect(2).to_not be_odd
end
specify "failure messages" do
expect { expect(2).to be_odd }.to raise_error(Spectator::ExpectationFailed, "2 is not odd")
expect { expect(3).to_not be_odd }.to raise_error(Spectator::ExpectationFailed, "3 is odd")
end
end
end

View file

@ -123,109 +123,6 @@ Spectator.describe "Mocks Docs" do
end end
end end
context "Mock Modules" do
module MyModule
def something
# ...
end
end
describe "#something" do
# Define a mock for MyModule.
mock MyClass
it "does something" do
# Use mock here.
end
end
module MyFileUtils
def self.rm_rf(path)
# ...
end
end
mock MyFileUtils
it "deletes all of my files" do
utils = class_mock(MyFileUtils)
allow(utils).to receive(:rm_rf)
utils.rm_rf("/")
expect(utils).to have_received(:rm_rf).with("/")
end
module MyFileUtils2
extend self
def rm_rf(path)
# ...
end
end
mock(MyFileUtils2) do
# Define a default stub for the method.
stub def self.rm_rf(path)
# ...
end
end
it "deletes all of my files part 2" do
utils = class_mock(MyFileUtils2)
allow(utils).to receive(:rm_rf)
utils.rm_rf("/")
expect(utils).to have_received(:rm_rf).with("/")
end
module Runnable
def run
# ...
end
end
mock Runnable
specify do
runnable = mock(Runnable) # or new_mock(Runnable)
runnable.run
end
module Runnable2
abstract def command : String
def run_one
"Running #{command}"
end
end
mock Runnable2, command: "ls -l"
specify do
runnable = mock(Runnable2)
expect(runnable.run_one).to eq("Running ls -l")
runnable = mock(Runnable2, command: "echo foo")
expect(runnable.run_one).to eq("Running echo foo")
end
context "Injecting Mocks" do
module MyFileUtils
def self.rm_rf(path)
true
end
end
inject_mock MyFileUtils do
stub def self.rm_rf(path)
"Simulating deletion of #{path}"
false
end
end
specify do
expect(MyFileUtils.rm_rf("/")).to be_false
end
end
end
context "Injecting Mocks" do context "Injecting Mocks" do
struct MyStruct struct MyStruct
def something def something
@ -249,9 +146,9 @@ Spectator.describe "Mocks Docs" do
inst.something inst.something
end end
it "reverts to default stub for other examples" do it "leaks stubs to other examples" do
inst = mock(MyStruct) inst = mock(MyStruct)
expect(inst.something).to eq(5) # Default stub used instead of original behavior. expect(inst.something).to eq(7) # Previous stub was leaked.
end end
end end
end end

View file

@ -1,28 +1,26 @@
require "../spec_helper" require "../spec_helper"
module Readme private abstract class Interface
abstract class Interface
abstract def invoke(thing) : String abstract def invoke(thing) : String
end end
# Type being tested. # Type being tested.
class Driver private class Driver
def do_something(interface : Interface, thing) def do_something(interface : Interface, thing)
interface.invoke(thing) interface.invoke(thing)
end end
end
end end
Spectator.describe Readme::Driver do Spectator.describe Driver do
# Define a mock for Interface. # Define a mock for Interface.
mock Readme::Interface mock Interface
# Define a double that the interface will use. # Define a double that the interface will use.
double(:my_double, foo: 42) double(:my_double, foo: 42)
it "does a thing" do it "does a thing" do
# Create an instance of the mock interface. # Create an instance of the mock interface.
interface = mock(Readme::Interface) interface = mock(Interface)
# Indicate that `#invoke` should return "test" when called. # Indicate that `#invoke` should return "test" when called.
allow(interface).to receive(:invoke).and_return("test") allow(interface).to receive(:invoke).and_return("test")

View file

@ -1,70 +0,0 @@
require "../spec_helper"
Spectator.describe "Expect Type", :smoke do
context "with expect syntax" do
it "ensures a type is cast" do
value = 42.as(String | Int32)
expect(value).to be_a(String | Int32)
expect(value).to compile_as(String | Int32)
value = expect(value).to be_a(Int32)
expect(value).to eq(42)
expect(value).to be_a(Int32)
expect(value).to compile_as(Int32)
expect(value).to_not respond_to(:downcase)
end
it "ensures a type is not nil" do
value = 42.as(Int32?)
expect(value).to be_a(Int32?)
expect(value).to compile_as(Int32?)
value = expect(value).to_not be_nil
expect(value).to eq(42)
expect(value).to be_a(Int32)
expect(value).to compile_as(Int32)
expect { value.not_nil! }.to_not raise_error(NilAssertionError)
end
it "removes types from a union" do
value = 42.as(String | Int32)
expect(value).to be_a(String | Int32)
expect(value).to compile_as(String | Int32)
value = expect(value).to_not be_a(String)
expect(value).to eq(42)
expect(value).to be_a(Int32)
expect(value).to compile_as(Int32)
expect(value).to_not respond_to(:downcase)
end
end
context "with should syntax" do
it "ensures a type is cast" do
value = 42.as(String | Int32)
value.should be_a(String | Int32)
value = value.should be_a(Int32)
value.should eq(42)
value.should be_a(Int32)
value.should compile_as(Int32)
value.should_not respond_to(:downcase)
end
it "ensures a type is not nil" do
value = 42.as(Int32?)
value.should be_a(Int32?)
value = value.should_not be_nil
value.should eq(42)
value.should be_a(Int32)
value.should compile_as(Int32)
expect { value.not_nil! }.to_not raise_error(NilAssertionError)
end
it "removes types from a union" do
value = 42.as(String | Int32)
value.should be_a(String | Int32)
value = value.should_not be_a(String)
value.should eq(42)
value.should be_a(Int32)
value.should compile_as(Int32)
value.should_not respond_to(:downcase)
end
end
end

View file

@ -1,22 +0,0 @@
require "../spec_helper"
Spectator.describe "Interpolated Label", :smoke do
let(foo) { "example" }
let(bar) { "context" }
it "interpolates #{foo} labels" do |example|
expect(example.name).to eq("interpolates example labels")
end
context "within a #{bar}" do
let(foo) { "multiple" }
it "interpolates context labels" do |example|
expect(example.group.name).to eq("within a context")
end
it "interpolates #{foo} levels" do |example|
expect(example.name).to eq("interpolates multiple levels")
end
end
end

View file

71
spec/helpers/example.cr Normal file
View file

@ -0,0 +1,71 @@
require "ecr"
require "json"
require "./result"
module Spectator::SpecHelpers
# Wrapper for compiling and running an example at runtime and getting a result.
class Example
# Creates the example.
# The *spec_helper_path* is the path to spec_helper.cr file.
# The name or ID of the example is given by *example_id*.
# Lastly, the source code for the example is given by *example_code*.
def initialize(@spec_helper_path : String, @example_id : String, @example_code : String)
end
# Instructs the Crystal compiler to compile the test.
# Returns an instance of `JSON::Any`.
# This will be the outcome and information about the test.
# Output will be suppressed for the test.
# If an error occurs while attempting to compile and run the test, an error will be raised.
def compile
# Create a temporary file containing the test.
with_tempfile do |source_file|
args = ["run", "--no-color", source_file, "--", "--json"]
Process.run(crystal_executable, args) do |process|
JSON.parse(process.output)
rescue JSON::ParseException
raise "Compilation of example #{@example_id} failed\n\n#{process.error.gets_to_end}"
end
end
end
# Same as `#compile`, but returns the result of the first example in the test.
# Returns a `SpectatorHelpers::Result` instance.
def result
output = compile
example = output["examples"][0]
Result.from_json_any(example)
end
# Constructs the string representation of the example.
# This produces the Crystal source code.
# *io* is the file handle to write to.
# The *dir* is the directory of the file being written to.
# This is needed to resolve the relative path to the spec_helper.cr file.
private def write(io, dir)
spec_helper_path = Path[@spec_helper_path].relative_to(dir) # ameba:disable Lint/UselessAssign
ECR.embed(__DIR__ + "/example.ecr", io)
end
# Creates a temporary file containing the compilable example code.
# Yields the path of the temporary file.
# Ensures the file is deleted after it is done being used.
private def with_tempfile
tempfile = File.tempfile("_#{@example_id}_spec.cr") do |file|
dir = File.dirname(file.path)
write(file, dir)
end
begin
yield tempfile.path
ensure
tempfile.delete
end
end
# Attempts to find the Crystal compiler on the system or raises an error.
private def crystal_executable
Process.find_executable("crystal") || raise("Could not find Crystal compiler")
end
end
end

5
spec/helpers/example.ecr Normal file
View file

@ -0,0 +1,5 @@
require "<%= spec_helper_path %>"
Spectator.describe "<%= @example_id %>" do
<%= @example_code %>
end

View file

@ -0,0 +1,28 @@
module Spectator::SpecHelpers
# Information about an `expect` call in an example.
struct Expectation
# Indicates whether the expectation passed or failed.
getter? satisfied : Bool
# Message when the expectation failed.
# Only available when `#satisfied?` is false.
getter! message : String
# Additional information about the expectation.
# Only available when `#satisfied?` is false.
getter! values : Hash(String, String)
# Creates the expectation outcome.
def initialize(@satisfied, @message, @values)
end
# Extracts the expectation information from a `JSON::Any` object.
def self.from_json_any(object : JSON::Any)
satisfied = object["satisfied"].as_bool
message = object["failure"]?.try(&.as_s?)
values = object["values"]?.try(&.as_h?)
values = values.transform_values(&.as_s) if values
new(satisfied, message, values)
end
end
end

67
spec/helpers/result.cr Normal file
View file

@ -0,0 +1,67 @@
module Spectator::SpecHelpers
# Information about an example compiled and run at runtime.
struct Result
# Status of the example after running.
enum Outcome
Success
Failure
Error
Unknown
end
# Full name and description of the example.
getter name : String
# Status of the example after running.
getter outcome : Outcome
# List of expectations ran in the example.
getter expectations : Array(Expectation)
# Creates the result.
def initialize(@name, @outcome, @expectations)
end
# Checks if the example was successful.
def success?
outcome.success?
end
# :ditto:
def successful?
outcome.success?
end
# Checks if the example failed, but did not error.
def failure?
outcome.failure?
end
# Checks if the example encountered an error.
def error?
outcome.error?
end
# Extracts the result information from a `JSON::Any` object.
def self.from_json_any(object : JSON::Any)
name = object["description"].as_s
outcome = parse_outcome_string(object["status"].as_s)
expectations = if (list = object["expectations"].as_a?)
list.map { |e| Expectation.from_json_any(e) }
else
[] of Expectation
end
new(name, outcome, expectations)
end
# Converts a result string, such as "fail" to an enum value.
private def self.parse_outcome_string(string)
case string
when /pass/i then Outcome::Success
when /fail/i then Outcome::Failure
when /error/i then Outcome::Error
else Outcome::Unknown
end
end
end
end

View file

@ -9,32 +9,12 @@ Spectator.describe "GitHub Issue #44" do
let(command) { "ls -l" } let(command) { "ls -l" }
let(exception) { File::NotFoundError.new("File not found", file: "test.file") } let(exception) { File::NotFoundError.new("File not found", file: "test.file") }
context "with positional arguments" do
before_each do before_each do
pipe = Process::Redirect::Pipe expect(Process).to receive(:run).with(command, shell: true, output: :pipe).and_raise(exception)
expect(Process).to receive(:run).with(command, nil, nil, false, true, pipe, pipe, pipe, nil).and_raise(exception)
end end
it "must stub Process.run" do skip "must stub Process.run", skip: "Method mock not applied" do
expect do
Process.run(command, shell: true, output: :pipe) do |_process| Process.run(command, shell: true, output: :pipe) do |_process|
end end
end.to raise_error(File::NotFoundError, "File not found")
end
end
# Original issue uses keyword arguments in place of positional arguments.
context "keyword arguments in place of positional arguments" do
before_each do
pipe = Process::Redirect::Pipe
expect(Process).to receive(:run).with(command, shell: true, output: pipe).and_raise(exception)
end
it "must stub Process.run" do
expect do
Process.run(command, shell: true, output: :pipe) do |_process|
end
end.to raise_error(File::NotFoundError, "File not found")
end
end end
end end

View file

@ -1,18 +0,0 @@
require "../spec_helper"
Spectator.describe "GitHub Issue #47" do
class Original
def foo(arg1, arg2)
# ...
end
end
mock Original
let(fake) { mock(Original) }
specify do
expect(fake).to receive(:foo).with("arg1", arg2: "arg2")
fake.foo("arg1", "arg2")
end
end

View file

@ -1,135 +0,0 @@
require "../spec_helper"
Spectator.describe "GitHub Issue #48" do
class Test
def return_this(thing : T) : T forall T
thing
end
def map(thing : T, & : T -> U) : U forall T, U
yield thing
end
def make_nilable(thing : T) : T? forall T
thing.as(T?)
end
def itself : self
self
end
def itself? : self?
self.as(self?)
end
def generic(thing : T) : Array(T) forall T
Array.new(100) { thing }
end
def union : Int32 | String
42.as(Int32 | String)
end
def capture(&block : -> T) forall T
block
end
def capture(thing : T, &block : T -> T) forall T
block.call(thing)
block
end
def range(r : Range)
r
end
end
mock Test, make_nilable: nil
let(fake) { mock(Test) }
it "handles free variables" do
allow(fake).to receive(:return_this).and_return("different")
expect(fake.return_this("test")).to eq("different")
end
it "raises on type cast error with free variables" do
allow(fake).to receive(:return_this).and_return(42)
expect { fake.return_this("test") }.to raise_error(TypeCastError, /String/)
end
it "handles free variables with a block" do
allow(fake).to receive(:map).and_return("stub")
expect(fake.map(:mapped, &.to_s)).to eq("stub")
end
it "raises on type cast error with a block and free variables" do
allow(fake).to receive(:map).and_return(42)
expect { fake.map(:mapped, &.to_s) }.to raise_error(TypeCastError, /String/)
end
it "handles nilable free variables" do
expect(fake.make_nilable("foo")).to be_nil
end
it "handles 'self' return type" do
not_self = mock(Test)
allow(fake).to receive(:itself).and_return(not_self)
expect(fake.itself).to be(not_self)
end
it "raises on type cast error with 'self' return type" do
allow(fake).to receive(:itself).and_return(42)
expect { fake.itself }.to raise_error(TypeCastError, /#{class_mock(Test)}/)
end
it "handles nilable 'self' return type" do
not_self = mock(Test)
allow(fake).to receive(:itself?).and_return(not_self)
expect(fake.itself?).to be(not_self)
end
it "handles generic return type" do
allow(fake).to receive(:generic).and_return([42])
expect(fake.generic(42)).to eq([42])
end
it "raises on type cast error with generic return type" do
allow(fake).to receive(:generic).and_return("test")
expect { fake.generic(42) }.to raise_error(TypeCastError, /Array\(Int32\)/)
end
it "handles union return types" do
allow(fake).to receive(:union).and_return("test")
expect(fake.union).to eq("test")
end
it "raises on type cast error with union return type" do
allow(fake).to receive(:union).and_return(:test)
expect { fake.union }.to raise_error(TypeCastError, /Symbol/)
end
it "handles captured blocks" do
proc = ->{}
allow(fake).to receive(:capture).and_return(proc)
expect(fake.capture { nil }).to be(proc)
end
it "raises on type cast error with captured blocks" do
proc = ->{ 42 }
allow(fake).to receive(:capture).and_return(proc)
expect { fake.capture { "other" } }.to raise_error(TypeCastError, /Proc\(String\)/)
end
it "handles captured blocks with arguments" do
proc = ->(x : Int32) { x * 2 }
allow(fake).to receive(:capture).and_return(proc)
expect(fake.capture(5) { 5 }).to be(proc)
end
it "handles range comparisons against non-comparable types" do
range = 1..10
allow(fake).to receive(:range).and_return(range)
expect(fake.range(1..3)).to eq(range)
end
end

View file

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

View file

@ -1,48 +0,0 @@
require "../spec_helper"
Spectator.describe "GitHub Issue #55" do
GROUP_NAME = "CallCenter"
let(name) { "TimeTravel" }
let(source) { "my.time.travel.experiment" }
class Analytics(T)
property start_time = Time.local
property end_time = Time.local
def initialize(@brain_talker : T)
end
def instrument(*, name, source, &)
@brain_talker.send(payload: {
:group => GROUP_NAME,
:name => name,
:source => source,
:start => start_time,
:end => end_time,
}, action: "analytics")
end
end
double(:brain_talker, send: nil)
let(brain_talker) { double(:brain_talker) }
let(analytics) { Analytics.new(brain_talker) }
it "tracks the time it takes to run the block" do
analytics.start_time = expected_start_time = Time.local
expected_end_time = expected_start_time + 10.seconds
analytics.end_time = expected_end_time + 0.5.seconds # Offset to ensure non-exact match.
analytics.instrument(name: name, source: source) do
end
expect(brain_talker).to have_received(:send).with(payload: {
:group => GROUP_NAME,
:name => name,
:source => source,
:start => expected_start_time,
:end => be_within(1.second).of(expected_end_time),
}, action: "analytics")
end
end

View file

@ -1,109 +0,0 @@
require "../spec_helper"
module GitLabIssue51
class Foo
def call(str : String) : String?
""
end
def alt1_call(str : String) : String?
nil
end
def alt2_call(str : String) : String?
[str, nil].sample
end
end
class Bar
def call(a_foo) : Nil # Must add nil restriction here, otherwise a segfault occurs from returning the result of #alt2_call.
a_foo.call("")
a_foo.alt1_call("")
a_foo.alt2_call("")
end
end
end
Spectator.describe GitLabIssue51::Bar do
mock GitLabIssue51::Foo, call: "", alt1_call: "", alt2_call: ""
let(:foo) { mock(GitLabIssue51::Foo) }
subject(:call) { described_class.new.call(foo) }
describe "#call" do
it "invokes Foo#call" do
call
expect(foo).to have_received(:call)
end
it "invokes Foo#alt1_call" do
call
expect(foo).to have_received(:alt1_call)
end
it "invokes Foo#alt2_call" do
call
expect(foo).to have_received(:alt2_call)
end
describe "with an explicit return of nil" do
it "should invoke Foo#call?" do
allow(foo).to receive(:call).and_return(nil)
call
expect(foo).to have_received(:call)
end
it "invokes Foo#alt1_call" do
allow(foo).to receive(:alt1_call).and_return(nil)
call
expect(foo).to have_received(:alt1_call)
end
it "invokes Foo#alt2_call" do
allow(foo).to receive(:alt2_call).and_return(nil)
call
expect(foo).to have_received(:alt2_call)
end
end
describe "with returns set in before_each for all calls" do
before_each do
allow(foo).to receive(:call).and_return(nil)
allow(foo).to receive(:alt1_call).and_return(nil)
allow(foo).to receive(:alt2_call).and_return(nil)
end
it "should invoke Foo#call?" do
call
expect(foo).to have_received(:call)
end
it "should invoke Foo#alt1_call?" do
call
expect(foo).to have_received(:alt1_call)
end
it "should invoke Foo#alt2_call?" do
call
expect(foo).to have_received(:alt2_call)
end
end
describe "with returns set in before_each for alt calls only" do
before_each do
allow(foo).to receive(:alt1_call).and_return(nil)
allow(foo).to receive(:alt2_call).and_return(nil)
end
it "invokes Foo#alt1_call" do
call
expect(foo).to have_received(:alt1_call)
end
it "invokes Foo#alt2_call" do
call
expect(foo).to have_received(:alt2_call)
end
end
end
end

View file

@ -1,6 +0,0 @@
require "../spec_helper"
Spectator.describe "GitLab Issue #76" do
let(:value) { nil.as(Int32?) }
specify { expect(value).to be_nil }
end

View file

@ -1,10 +0,0 @@
require "../spec_helper"
# https://gitlab.com/arctic-fox/spectator/-/issues/77
Spectator.describe "GitLab Issue #77" do
it "fails" do
expect_raises do
raise "Error!"
end
end
end

View file

@ -1,30 +0,0 @@
require "../spec_helper"
# https://gitlab.com/arctic-fox/spectator/-/issues/80
class Item
end
class ItemUser
@item = Item.new
def item
@item
end
end
Spectator.describe "test1" do
it "without mock" do
item_user = ItemUser.new
item = item_user.item
item == item
end
end
Spectator.describe "test2" do
mock Item do
end
it "without mock" do
end
end

View file

@ -169,7 +169,7 @@ Spectator.describe Spectator::Matchers::ReceiveMatcher do
end end
context "with method calls" do context "with method calls" do
before do before_each do
dbl.test_method dbl.test_method
dbl.test_method(1, "wrong", :xyz, foo: "foobarbaz") dbl.test_method(1, "wrong", :xyz, foo: "foobarbaz")
dbl.irrelevant("foo") dbl.irrelevant("foo")
@ -289,14 +289,14 @@ Spectator.describe Spectator::Matchers::ReceiveMatcher do
pre_condition { expect(match_data).to be_a(failed_match) } pre_condition { expect(match_data).to be_a(failed_match) }
before do before_each do
dbl.test_method dbl.test_method
dbl.test_method(1, "test", :xyz, foo: "foobarbaz") dbl.test_method(1, "test", :xyz, foo: "foobarbaz")
dbl.irrelevant("foo") dbl.irrelevant("foo")
end end
it "has the expected call listed" do it "has the expected call listed" do
is_expected.to contain({:expected, "Not #{stub.message}"}) is_expected.to contain({:expected, "Not #{stub}"})
end end
it "has the list of called methods" do it "has the list of called methods" do

View file

@ -52,7 +52,7 @@ Spectator.describe "Explicit Subject" do
describe Array(Int32) do # TODO: Multiple arguments to describe/context. describe Array(Int32) do # TODO: Multiple arguments to describe/context.
subject { [] of Int32 } subject { [] of Int32 }
before { subject.push(1, 2, 3) } before_each { subject.push(1, 2, 3) }
it "has the prescribed elements" do it "has the prescribed elements" do
expect(subject).to eq([1, 2, 3]) expect(subject).to eq([1, 2, 3])

View file

@ -0,0 +1,58 @@
require "./spec_helper"
# This is a meta test that ensures specs can be compiled and run at runtime.
# The purpose of this is to report an error if this process fails.
# Other tests will fail, but display a different name/description of the test.
# This clearly indicates that runtime testing failed.
#
# Runtime compilation is used to get output of tests as well as check syntax.
# Some specs are too complex to be ran normally.
# Additionally, this allows examples to easily check specific failure cases.
# Plus, it makes testing user-reported issues easy.
Spectator.describe "Runtime compilation", :slow, :compile do
given_example passing_example do
it "does something" do
expect(true).to be_true
end
end
it "can compile and retrieve the result of an example" do
expect(passing_example).to be_successful
end
it "can retrieve expectations" do
expect(passing_example.expectations).to_not be_empty
end
given_example failing_example do
it "does something" do
expect(true).to be_false
end
it "doesn't run" do
expect(true).to be_false
end
end
it "detects failed examples" do
expect(failing_example).to be_failure
end
given_example malformed_example do
it "does something" do
asdf
end
end
it "raises on compilation errors" do
expect { malformed_example }.to raise_error(/compilation/i)
end
given_expectation satisfied_expectation do
expect(true).to be_true
end
it "can compile and retrieve expectations" do
expect(satisfied_expectation).to be_satisfied
end
end

View file

@ -15,3 +15,35 @@ end
macro specify_fails(description = nil, &block) macro specify_fails(description = nil, &block)
it_fails {{description}} {{block}} it_fails {{description}} {{block}}
end end
# Defines an example ("it" block) that is lazily compiled.
# When the example is referenced with *id*, it will be compiled and the results retrieved.
# The value returned by *id* will be a `Spectator::SpecHelpers::Result`.
# This allows the test result to be inspected.
macro given_example(id, &block)
let({{id}}) do
::Spectator::SpecHelpers::Example.new(
{{__FILE__}},
{{id.id.stringify}},
{{block.body.stringify}}
).result
end
end
# Defines an example ("it" block) that is lazily compiled.
# The "it" block must be omitted, as the block provided to this macro will be wrapped in one.
# When the expectation is referenced with *id*, it will be compiled and the result retrieved.
# The value returned by *id* will be a `Spectator::SpecHelpers::Expectation`.
# This allows an expectation to be inspected.
# Only the last expectation performed will be returned.
# An error is raised if no expectations ran.
macro given_expectation(id, &block)
let({{id}}) do
result = ::Spectator::SpecHelpers::Example.new(
{{__FILE__}},
{{id.id.stringify}},
{{"it do\n" + block.body.stringify + "\nend"}}
).result
result.expectations.last || raise("No expectations found from {{id.id}}")
end
end

View file

@ -1,188 +0,0 @@
require "../../../spec_helper"
Spectator.describe "Allow stub DSL" do
context "with a double" do
double(:dbl) do
# Ensure the original is never called.
stub abstract def foo : Nil
stub abstract def foo(arg) : Nil
stub abstract def value : Int32
end
let(dbl) { double(:dbl) }
# Ensure invocations don't leak between examples.
pre_condition { expect(dbl).to_not have_received(:foo), "Leaked method calls from previous examples" }
# Ensure stubs don't leak between examples.
pre_condition do
expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage)
end
it "matches when a message is received" do
allow(dbl).to receive(:foo)
expect { dbl.foo }.to_not raise_error
end
it "returns the correct value" do
allow(dbl).to receive(:value).and_return(42)
expect(dbl.value).to eq(42)
end
it "matches when a message is received with matching arguments" do
allow(dbl).to receive(:foo).with(:bar)
expect { dbl.foo(:bar) }.to_not raise_error
end
it "raises when a message without arguments is received" do
allow(dbl).to receive(:foo).with(:bar)
expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage, /foo/)
end
it "raises when a message with different arguments is received" do
allow(dbl).to receive(:foo).with(:baz)
expect { dbl.foo(:bar) }.to raise_error(Spectator::UnexpectedMessage, /foo/)
end
end
context "with a class double" do
double(:dbl) do
# Ensure the original is never called.
abstract_stub def self.foo : Nil
end
abstract_stub def self.foo(arg) : Nil
end
abstract_stub def self.value : Int32
42
end
end
let(dbl) { class_double(:dbl) }
# Ensure invocations don't leak between examples.
pre_condition { expect(dbl).to_not have_received(:foo), "Leaked method calls from previous examples" }
# Ensure stubs don't leak between examples.
pre_condition do
expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage)
end
it "matches when a message is received" do
allow(dbl).to receive(:foo)
expect { dbl.foo }.to_not raise_error
end
it "returns the correct value" do
allow(dbl).to receive(:value).and_return(42)
expect(dbl.value).to eq(42)
end
it "matches when a message is received with matching arguments" do
allow(dbl).to receive(:foo).with(:bar)
expect { dbl.foo(:bar) }.to_not raise_error
end
it "raises when a message without arguments is received" do
allow(dbl).to receive(:foo).with(:bar)
expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage, /foo/)
end
it "raises when a message with different arguments is received" do
allow(dbl).to receive(:foo).with(:baz)
expect { dbl.foo(:bar) }.to raise_error(Spectator::UnexpectedMessage, /foo/)
end
end
context "with a mock" do
abstract class MyClass
abstract def foo : Int32
abstract def foo(arg) : Int32
end
mock(MyClass)
let(fake) { mock(MyClass) }
# Ensure invocations don't leak between examples.
pre_condition { expect(fake).to_not have_received(:foo), "Leaked method calls from previous examples" }
# Ensure stubs don't leak between examples.
pre_condition do
expect { fake.foo }.to raise_error(Spectator::UnexpectedMessage)
end
it "matches when a message is received" do
allow(fake).to receive(:foo).and_return(42)
expect(fake.foo).to eq(42)
end
it "returns the correct value" do
allow(fake).to receive(:foo).and_return(42)
expect(fake.foo).to eq(42)
end
it "matches when a message is received with matching arguments" do
allow(fake).to receive(:foo).with(:bar).and_return(42)
expect(fake.foo(:bar)).to eq(42)
end
it "raises when a message without arguments is received" do
allow(fake).to receive(:foo).with(:bar)
expect { fake.foo }.to raise_error(Spectator::UnexpectedMessage, /foo/)
end
it "raises when a message with different arguments is received" do
allow(fake).to receive(:foo).with(:baz)
expect { fake.foo(:bar) }.to raise_error(Spectator::UnexpectedMessage, /foo/)
end
end
context "with a class mock" do
class MyClass
def self.foo : Int32
42
end
def self.foo(arg) : Int32
42
end
end
mock(MyClass)
let(fake) { class_mock(MyClass) }
# Ensure invocations don't leak between examples.
pre_condition { expect(fake).to_not have_received(:foo), "Leaked method calls from previous examples" }
# Ensure stubs don't leak between examples.
pre_condition { expect(fake.foo).to eq(42) }
it "matches when a message is received" do
allow(fake).to receive(:foo).and_return(0)
expect(fake.foo).to eq(0)
end
it "returns the correct value" do
allow(fake).to receive(:foo).and_return(0)
expect(fake.foo).to eq(0)
end
it "matches when a message is received with matching arguments" do
allow(fake).to receive(:foo).with(:bar).and_return(0)
expect(fake.foo(:bar)).to eq(0)
end
it "calls the original when a message without arguments is received" do
allow(fake).to receive(:foo).with(:bar)
expect(fake.foo).to eq(42)
end
it "calls the original when a message with different arguments is received" do
allow(fake).to receive(:foo).with(:baz)
expect(fake.foo(:bar)).to eq(42)
end
end
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
@ -312,7 +312,7 @@ Spectator.describe "Double DSL", :smoke do
let(override) { :override } let(override) { :override }
let(dbl) { double(:context_double, override: override) } let(dbl) { double(:context_double, override: override) }
before { allow(dbl).to receive(:memoize).and_return(memoize) } before_each { allow(dbl).to receive(:memoize).and_return(memoize) }
it "doesn't change predefined values" do it "doesn't change predefined values" do
expect(dbl.predefined).to eq(:predefined) expect(dbl.predefined).to eq(:predefined)

View file

@ -14,12 +14,6 @@ Spectator.describe "Deferred stub expectation DSL" do
# Ensure invocations don't leak between examples. # Ensure invocations don't leak between examples.
pre_condition { expect(dbl).to_not have_received(:foo), "Leaked method calls from previous examples" } pre_condition { expect(dbl).to_not have_received(:foo), "Leaked method calls from previous examples" }
# Ensure stubs don't leak between examples.
pre_condition do
expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage)
dbl._spectator_clear_calls # Don't include previous call in results.
end
it "matches when a message is received" do it "matches when a message is received" do
expect(dbl).to receive(:foo) expect(dbl).to receive(:foo)
dbl.foo dbl.foo
@ -73,12 +67,6 @@ Spectator.describe "Deferred stub expectation DSL" do
# Ensure invocations don't leak between examples. # Ensure invocations don't leak between examples.
pre_condition { expect(dbl).to_not have_received(:foo), "Leaked method calls from previous examples" } pre_condition { expect(dbl).to_not have_received(:foo), "Leaked method calls from previous examples" }
# Ensure stubs don't leak between examples.
pre_condition do
expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage)
dbl._spectator_clear_calls # Don't include previous call in results.
end
it "matches when a message is received" do it "matches when a message is received" do
expect(dbl).to receive(:foo) expect(dbl).to receive(:foo)
dbl.foo dbl.foo
@ -126,12 +114,6 @@ Spectator.describe "Deferred stub expectation DSL" do
# Ensure invocations don't leak between examples. # Ensure invocations don't leak between examples.
pre_condition { expect(fake).to_not have_received(:foo), "Leaked method calls from previous examples" } pre_condition { expect(fake).to_not have_received(:foo), "Leaked method calls from previous examples" }
# Ensure stubs don't leak between examples.
pre_condition do
expect { fake.foo }.to raise_error(Spectator::UnexpectedMessage)
fake._spectator_clear_calls # Don't include previous call in results.
end
it "matches when a message is received" do it "matches when a message is received" do
expect(fake).to receive(:foo).and_return(42) expect(fake).to receive(:foo).and_return(42)
fake.foo(:bar) fake.foo(:bar)
@ -184,20 +166,14 @@ Spectator.describe "Deferred stub expectation DSL" do
# Ensure invocations don't leak between examples. # Ensure invocations don't leak between examples.
pre_condition { expect(fake).to_not have_received(:foo), "Leaked method calls from previous examples" } pre_condition { expect(fake).to_not have_received(:foo), "Leaked method calls from previous examples" }
# Ensure stubs don't leak between examples.
pre_condition do
expect(fake.foo).to eq(42)
fake._spectator_clear_calls # Don't include previous call in results.
end
it "matches when a message is received" do it "matches when a message is received" do
expect(fake).to receive(:foo).and_return(0) expect(fake).to receive(:foo).and_return(42)
fake.foo(:bar) fake.foo(:bar)
end end
it "returns the correct value" do it "returns the correct value" do
expect(fake).to receive(:foo).and_return(0) expect(fake).to receive(:foo).and_return(42)
expect(fake.foo).to eq(0) expect(fake.foo).to eq(42)
end end
it "matches when a message isn't received" do it "matches when a message isn't received" do
@ -205,12 +181,12 @@ Spectator.describe "Deferred stub expectation DSL" do
end end
it "matches when a message is received with matching arguments" do it "matches when a message is received with matching arguments" do
expect(fake).to receive(:foo).with(:bar).and_return(0) expect(fake).to receive(:foo).with(:bar).and_return(42)
fake.foo(:bar) fake.foo(:bar)
end end
it "matches when a message without arguments is received" do it "matches when a message without arguments is received" do
expect(fake).to_not receive(:foo).with(:bar).and_return(0) expect(fake).to_not receive(:foo).with(:bar).and_return(42)
fake.foo fake.foo
end end
@ -219,7 +195,7 @@ Spectator.describe "Deferred stub expectation DSL" do
end end
it "matches when a message with arguments isn't received" do it "matches when a message with arguments isn't received" do
expect(fake).to_not receive(:foo).with(:baz).and_return(0) expect(fake).to_not receive(:foo).with(:baz).and_return(42)
fake.foo(:bar) fake.foo(:bar)
end end
end end

View file

@ -11,7 +11,7 @@ Spectator.describe "Mock DSL", :smoke do
args[1].as(Int32), args[1].as(Int32),
args[2].as(Int32), args[2].as(Int32),
}, },
args[:kwarg].as(Int32), args[3].as(Int32),
{ {
x: args[:x].as(Int32), x: args[:x].as(Int32),
y: args[:y].as(Int32), y: args[:y].as(Int32),
@ -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
@ -947,7 +947,7 @@ Spectator.describe "Mock DSL", :smoke do
let(override) { :override } let(override) { :override }
let(fake) { mock(Dummy, override: override) } let(fake) { mock(Dummy, override: override) }
before { allow(fake).to receive(:memoize).and_return(memoize) } before_each { allow(fake).to receive(:memoize).and_return(memoize) }
it "doesn't change predefined values" do it "doesn't change predefined values" do
expect(fake.predefined).to eq(:predefined) expect(fake.predefined).to eq(:predefined)
@ -1027,262 +1027,4 @@ Spectator.describe "Mock DSL", :smoke do
expect(fake.reference).to eq("reference") expect(fake.reference).to eq("reference")
end end
end end
describe "mock module" do
module Dummy
# `extend self` cannot be used.
# The Crystal compiler doesn't report the methods as class methods when doing so.
def self.abstract_method
:not_really_abstract
end
def self.default_method
:original
end
def self.args(arg)
arg
end
def self.method1
:original
end
def self.reference
method1.to_s
end
end
mock(Dummy) do
abstract_stub def self.abstract_method
:abstract
end
stub def self.default_method
:default
end
end
let(fake) { class_mock(Dummy) }
it "raises on abstract stubs" do
expect { fake.abstract_method }.to raise_error(Spectator::UnexpectedMessage, /abstract_method/)
end
it "can define default stubs" do
expect(fake.default_method).to eq(:default)
end
it "can define new stubs" do
expect { allow(fake).to receive(:args).and_return(42) }.to change { fake.args(5) }.from(5).to(42)
end
it "can override class method stubs" do
allow(fake).to receive(:method1).and_return(:override)
expect(fake.method1).to eq(:override)
end
xit "can reference stubs", pending: "Default stub of module class methods always refer to original" do
allow(fake).to receive(:method1).and_return(:reference)
expect(fake.reference).to eq("reference")
end
end
context "with a class including a mocked module" do
module Dummy
getter _spectator_invocations = [] of Symbol
def method1
@_spectator_invocations << :method1
"original"
end
def method2 : Symbol
@_spectator_invocations << :method2
:original
end
def method3(arg)
@_spectator_invocations << :method3
arg
end
def method4(&) : Symbol
@_spectator_invocations << :method4
yield
end
def method5(&)
@_spectator_invocations << :method5
yield.to_i
end
def method6(&)
@_spectator_invocations << :method6
yield
end
def method7(arg, *args, kwarg, **kwargs)
@_spectator_invocations << :method7
{arg, args, kwarg, kwargs}
end
def method8(arg, *args, kwarg, **kwargs, &)
@_spectator_invocations << :method8
yield
{arg, args, kwarg, kwargs}
end
end
# method1 stubbed via mock block
# method2 stubbed via keyword args
# method3 not stubbed (calls original)
# method4 stubbed via mock block (yields)
# method5 stubbed via keyword args (yields)
# method6 not stubbed (calls original and yields)
# method7 not stubbed (calls original) testing args
# method8 not stubbed (calls original and yields) testing args
mock(Dummy, method2: :stubbed, method5: 42) do
stub def method1
"stubbed"
end
stub def method4(&) : Symbol
yield
:block
end
end
subject(fake) { mock(Dummy) }
it "defines a subclass" do
expect(fake).to be_a(Dummy)
end
it "defines stubs in the block" do
expect(fake.method1).to eq("stubbed")
end
it "can stub methods defined in the block" do
stub = Spectator::ValueStub.new(:method1, "override")
expect { fake._spectator_define_stub(stub) }.to change { fake.method1 }.from("stubbed").to("override")
end
it "defines stubs from keyword arguments" do
expect(fake.method2).to eq(:stubbed)
end
it "can stub methods from keyword arguments" do
stub = Spectator::ValueStub.new(:method2, :override)
expect { fake._spectator_define_stub(stub) }.to change { fake.method2 }.from(:stubbed).to(:override)
end
it "calls the original implementation for methods not provided a stub" do
expect(fake.method3(:xyz)).to eq(:xyz)
end
it "can stub methods after declaration" do
stub = Spectator::ValueStub.new(:method3, :abc)
expect { fake._spectator_define_stub(stub) }.to change { fake.method3(:xyz) }.from(:xyz).to(:abc)
end
it "defines stubs with yield in the block" do
expect(fake.method4 { :wrong }).to eq(:block)
end
it "can stub methods with yield in the block" do
stub = Spectator::ValueStub.new(:method4, :override)
expect { fake._spectator_define_stub(stub) }.to change { fake.method4 { :wrong } }.from(:block).to(:override)
end
it "defines stubs with yield from keyword arguments" do
expect(fake.method5 { :wrong }).to eq(42)
end
it "can stub methods with yield from keyword arguments" do
stub = Spectator::ValueStub.new(:method5, 123)
expect { fake._spectator_define_stub(stub) }.to change { fake.method5 { "0" } }.from(42).to(123)
end
it "can stub yielding methods after declaration" do
stub = Spectator::ValueStub.new(:method6, :abc)
expect { fake._spectator_define_stub(stub) }.to change { fake.method6 { :xyz } }.from(:xyz).to(:abc)
end
it "handles arguments correctly" do
args1 = fake.method7(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7)
args2 = fake.method8(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) { :block }
aggregate_failures do
expect(args1).to eq({1, {2, 3}, 4, {x: 5, y: 6, z: 7}})
expect(args2).to eq({1, {2, 3}, 4, {x: 5, y: 6, z: 7}})
end
end
it "handles arguments correctly with stubs" do
stub1 = Spectator::ProcStub.new(:method7, args_proc)
stub2 = Spectator::ProcStub.new(:method8, args_proc)
fake._spectator_define_stub(stub1)
fake._spectator_define_stub(stub2)
args1 = fake.method7(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7)
args2 = fake.method8(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) { :block }
aggregate_failures do
expect(args1).to eq({1, {2, 3}, 4, {x: 5, y: 6, z: 7}})
expect(args2).to eq({1, {2, 3}, 4, {x: 5, y: 6, z: 7}})
end
end
it "compiles types without unions" do
aggregate_failures do
expect(fake.method1).to compile_as(String)
expect(fake.method2).to compile_as(Symbol)
expect(fake.method3(42)).to compile_as(Int32)
expect(fake.method4 { :foo }).to compile_as(Symbol)
expect(fake.method5 { "123" }).to compile_as(Int32)
expect(fake.method6 { "123" }).to compile_as(String)
end
end
def restricted(thing : Dummy)
thing.method1
end
it "can be used in type restricted methods" do
expect(restricted(fake)).to eq("stubbed")
end
it "does not call the original method when stubbed" do
fake.method1
fake.method2
fake.method3("foo")
fake.method4 { :foo }
fake.method5 { "42" }
fake.method6 { 42 }
fake.method7(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7)
fake.method8(1, 2, 3, kwarg: 4, x: 5, y: 6, z: 7) { :block }
expect(fake._spectator_invocations).to contain_exactly(:method3, :method6, :method7, :method8)
end
# Cannot test unexpected messages - will not compile due to missing methods.
describe "deferred default stubs" do
mock(Dummy)
let(fake2) do
mock(Dummy,
method1: "stubbed",
method3: 123,
method4: :xyz)
end
it "uses the keyword arguments as stubs" do
aggregate_failures do
expect(fake2.method1).to eq("stubbed")
expect(fake2.method2).to eq(:original)
expect(fake2.method3(42)).to eq(123)
expect(fake2.method4 { :foo }).to eq(:xyz)
end
end
end
end
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

@ -9,31 +9,5 @@ Spectator.describe Spectator::Allow do
it "applies a stub" do it "applies a stub" do
expect { alw.to(stub) }.to change { dbl.foo }.from(42).to(123) expect { alw.to(stub) }.to change { dbl.foo }.from(42).to(123)
end end
context "leak" do
class Thing
def foo
42
end
end
mock Thing
getter(thing : Thing) { mock(Thing) }
# Workaround type restrictions requiring a constant.
def fake
class_mock(Thing).cast(thing)
end
specify do
expect { allow(fake).to(stub) }.to change { fake.foo }.from(42).to(123)
end
# This example must be run after the previous (random order may break this).
it "clears the stub after the example completes" do
expect { fake.foo }.to eq(42)
end
end
end end
end end

View file

@ -1,15 +1,21 @@
require "../../spec_helper" require "../../spec_helper"
Spectator.describe Spectator::Arguments do Spectator.describe Spectator::Arguments do
subject(arguments) { Spectator::Arguments.new({42, "foo"}, {bar: "baz", qux: 123}) } subject(arguments) do
Spectator::Arguments.new(
it "stores the arguments" do
expect(arguments).to have_attributes(
args: {42, "foo"}, args: {42, "foo"},
kwargs: {bar: "baz", qux: 123} kwargs: {bar: "baz", qux: 123}
) )
end 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 describe ".capture" do
subject { Spectator::Arguments.capture(42, "foo", bar: "baz", qux: 123) } subject { Spectator::Arguments.capture(42, "foo", bar: "baz", qux: 123) }
@ -18,7 +24,8 @@ Spectator.describe Spectator::Arguments do
end end
end end
describe "#[](index)" do describe "#[]" do
context "with an index" do
it "returns a positional argument" do it "returns a positional argument" do
aggregate_failures do aggregate_failures do
expect(arguments[0]).to eq(42) expect(arguments[0]).to eq(42)
@ -27,14 +34,15 @@ Spectator.describe Spectator::Arguments do
end end
end end
describe "#[](symbol)" do context "with a symbol" do
it "returns a keyword argument" do it "returns a named argument" do
aggregate_failures do aggregate_failures do
expect(arguments[:bar]).to eq("baz") expect(arguments[:bar]).to eq("baz")
expect(arguments[:qux]).to eq(123) expect(arguments[:qux]).to eq(123)
end end
end end
end end
end
describe "#to_s" do describe "#to_s" do
subject { arguments.to_s } subject { arguments.to_s }
@ -55,79 +63,50 @@ Spectator.describe Spectator::Arguments do
describe "#==" do describe "#==" do
subject { arguments == other } subject { arguments == other }
context "with Arguments" do
context "with equal arguments" do context "with equal arguments" do
let(other) { arguments } let(other) { arguments }
it { is_expected.to be_true } it "returns true" do
is_expected.to be_true
end
end end
context "with different arguments" do context "with different arguments" do
let(other) { Spectator::Arguments.new({123, :foo, "bar"}, {opt: "foobar"}) } let(other) do
Spectator::Arguments.new(
args: {123, :foo, "bar"},
kwargs: {opt: "foobar"}
)
end
it { is_expected.to be_false } it "returns false" do
is_expected.to be_false
end
end end
context "with the same kwargs in a different order" do context "with the same kwargs in a different order" do
let(other) { Spectator::Arguments.new(arguments.args, {qux: 123, bar: "baz"}) } let(other) do
Spectator::Arguments.new(
args: arguments.args,
kwargs: {qux: 123, bar: "baz"}
)
end
it { is_expected.to be_true } it "returns true" do
is_expected.to be_true
end
end end
context "with a missing kwarg" do context "with a missing kwarg" do
let(other) { Spectator::Arguments.new(arguments.args, {bar: "baz"}) } let(other) do
Spectator::Arguments.new(
it { is_expected.to be_false } args: arguments.args,
kwargs: {bar: "baz"}
)
end end
context "with an extra kwarg" do it "returns false" do
let(other) { Spectator::Arguments.new(arguments.args, {bar: "baz", qux: 123, extra: 0}) } is_expected.to be_false
it { is_expected.to be_false }
end
end
context "with FormalArguments" do
context "with equal arguments" do
let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123}) }
it { is_expected.to be_true }
end
context "with different arguments" do
let(other) { Spectator::FormalArguments.new({arg1: 123, arg2: :foo, arg3: "bar"}, {opt: "foobar"}) }
it { is_expected.to be_false }
end
context "with the same kwargs in a different order" do
let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {qux: 123, bar: "baz"}) }
it { is_expected.to be_true }
end
context "with a missing kwarg" do
let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz"}) }
it { is_expected.to be_false }
end
context "with an extra kwarg" do
let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123, extra: 0}) }
it { is_expected.to be_false }
end
context "with different splat arguments" do
let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, {bar: "baz", qux: 123}) }
it { is_expected.to be_false }
end
context "with mixed positional tuple types" do
let(other) { Spectator::FormalArguments.new({arg1: 42}, :splat, {"foo"}, {bar: "baz", qux: 123}) }
it { is_expected.to be_true }
end end
end end
end end
@ -135,149 +114,76 @@ Spectator.describe Spectator::Arguments do
describe "#===" do describe "#===" do
subject { pattern === arguments } subject { pattern === arguments }
context "with Arguments" do
context "with equal arguments" do context "with equal arguments" do
let(pattern) { arguments } let(pattern) { arguments }
it { is_expected.to be_true } it "returns true" do
is_expected.to be_true
end end
context "with matching arguments" do
let(pattern) { Spectator::Arguments.new({Int32, /foo/}, {bar: /baz/, qux: Int32}) }
it { is_expected.to be_true }
end
context "with non-matching arguments" do
let(pattern) { Spectator::Arguments.new({Float64, /bar/}, {bar: /foo/, qux: "123"}) }
it { is_expected.to be_false }
end end
context "with different arguments" do context "with different arguments" do
let(pattern) { Spectator::Arguments.new({123, :foo, "bar"}, {opt: "foobar"}) } let(pattern) do
Spectator::Arguments.new(
args: {123, :foo, "bar"},
kwargs: {opt: "foobar"}
)
end
it { is_expected.to be_false } it "returns false" do
is_expected.to be_false
end
end end
context "with the same kwargs in a different order" do context "with the same kwargs in a different order" do
let(pattern) { Spectator::Arguments.new(arguments.args, {qux: Int32, bar: /baz/}) } let(pattern) do
Spectator::Arguments.new(
it { is_expected.to be_true } args: arguments.args,
kwargs: {qux: 123, bar: "baz"}
)
end end
context "with an additional kwarg" do it "returns true" do
let(pattern) { Spectator::Arguments.new(arguments.args, {bar: /baz/}) } is_expected.to be_true
end
it { is_expected.to be_true }
end end
context "with a missing kwarg" do context "with a missing kwarg" do
let(pattern) { Spectator::Arguments.new(arguments.args, {bar: /baz/, qux: Int32, extra: 0}) } let(pattern) do
Spectator::Arguments.new(
args: arguments.args,
kwargs: {bar: "baz"}
)
end
it { is_expected.to be_false } it "returns false" do
is_expected.to be_false
end end
end end
context "with FormalArguments" do context "with matching types and regex" do
let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123}) } let(pattern) do
Spectator::Arguments.new(
context "with equal arguments" do args: {Int32, /foo/},
let(pattern) { Spectator::Arguments.new({42, "foo"}, {bar: "baz", qux: 123}) } kwargs: {bar: String, qux: 123}
)
it { is_expected.to be_true }
end end
context "with matching arguments" do it "returns true" do
let(pattern) { Spectator::Arguments.new({Int32, /foo/}, {bar: /baz/, qux: Int32}) } is_expected.to be_true
end
it { is_expected.to be_true }
end end
context "with non-matching arguments" do context "with different types and regex" do
let(pattern) { Spectator::Arguments.new({Float64, /bar/}, {bar: /foo/, qux: "123"}) } let(pattern) do
Spectator::Arguments.new(
it { is_expected.to be_false } args: {Symbol, /bar/},
kwargs: {bar: String, qux: 42}
)
end end
context "with different arguments" do it "returns false" do
let(pattern) { Spectator::Arguments.new({123, :foo, "bar"}, {opt: "foobar"}) } is_expected.to be_false
it { is_expected.to be_false }
end
context "with the same kwargs in a different order" do
let(pattern) { Spectator::Arguments.new(arguments.positional, {qux: Int32, bar: /baz/}) }
it { is_expected.to be_true }
end
context "with an additional kwarg" do
let(pattern) { Spectator::Arguments.new(arguments.positional, {bar: /baz/}) }
it { is_expected.to be_true }
end
context "with a missing kwarg" do
let(pattern) { Spectator::Arguments.new(arguments.positional, {bar: /baz/, qux: Int32, extra: 0}) }
it { is_expected.to be_false }
end
context "with different splat arguments" do
let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, super.kwargs) }
let(pattern) { Spectator::Arguments.new({Int32, /foo/, 5}, arguments.kwargs) }
it { is_expected.to be_false }
end
context "with matching mixed positional tuple types" do
let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, super.kwargs) }
let(pattern) { Spectator::Arguments.new({Int32, /foo/, 1, 2, 3}, arguments.kwargs) }
it { is_expected.to be_true }
end
context "with non-matching mixed positional tuple types" do
let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, super.kwargs) }
let(pattern) { Spectator::Arguments.new({Float64, /bar/, 3, 2, Symbol}, 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::FormalArguments.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::FormalArguments.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::FormalArguments.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::FormalArguments.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::FormalArguments.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 end
end end

View file

@ -212,10 +212,14 @@ Spectator.describe Spectator::Double do
expect(dbl.hash).to be_a(UInt64) expect(dbl.hash).to be_a(UInt64)
expect(dbl.in?([42])).to be_false expect(dbl.in?([42])).to be_false
expect(dbl.in?(1, 2, 3)).to be_false expect(dbl.in?(1, 2, 3)).to be_false
expect(dbl.inspect).to contain("EmptyDouble")
expect(dbl.itself).to be(dbl) expect(dbl.itself).to be(dbl)
expect(dbl.not_nil!).to be(dbl) expect(dbl.not_nil!).to be(dbl)
expect(dbl.pretty_inspect).to contain("EmptyDouble")
expect(dbl.pretty_print(pp)).to be_nil expect(dbl.pretty_print(pp)).to be_nil
expect(dbl.tap { nil }).to be(dbl) expect(dbl.tap { nil }).to be(dbl)
expect(dbl.to_s).to contain("EmptyDouble")
expect(dbl.to_s(io)).to be_nil
expect(dbl.try { nil }).to be_nil expect(dbl.try { nil }).to be_nil
expect(dbl.object_id).to be_a(UInt64) expect(dbl.object_id).to be_a(UInt64)
expect(dbl.same?(dbl)).to be_true expect(dbl.same?(dbl)).to be_true
@ -297,7 +301,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
@ -305,7 +309,7 @@ Spectator.describe Spectator::Double do
subject(dbl) { ClassDouble } subject(dbl) { ClassDouble }
let(foo_stub) { Spectator::ValueStub.new(:foo, :override) } let(foo_stub) { Spectator::ValueStub.new(:foo, :override) }
after { dbl._spectator_clear_stubs } after_each { dbl._spectator_clear_stubs }
it "overrides an existing method" do it "overrides an existing method" do
expect { dbl._spectator_define_stub(foo_stub) }.to change { dbl.foo }.from(:stub).to(:override) expect { dbl._spectator_define_stub(foo_stub) }.to change { dbl.foo }.from(:stub).to(:override)
@ -353,7 +357,7 @@ Spectator.describe Spectator::Double do
end end
describe "._spectator_clear_stubs" do describe "._spectator_clear_stubs" do
before { dbl._spectator_define_stub(foo_stub) } before_each { dbl._spectator_define_stub(foo_stub) }
it "removes previously defined stubs" do it "removes previously defined stubs" do
expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(:override).to(:stub) expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(:override).to(:stub)
@ -361,7 +365,7 @@ Spectator.describe Spectator::Double do
end end
describe "._spectator_calls" do describe "._spectator_calls" do
before { dbl._spectator_clear_calls } before_each { dbl._spectator_clear_calls }
# Retrieves symbolic names of methods called on a double. # Retrieves symbolic names of methods called on a double.
def called_method_names(dbl) def called_method_names(dbl)
@ -436,7 +440,7 @@ Spectator.describe Spectator::Double do
subject(dbl) { FooBarDouble.new } subject(dbl) { FooBarDouble.new }
let(stub) { Spectator::ValueStub.new(:foo, 5) } let(stub) { Spectator::ValueStub.new(:foo, 5) }
before { dbl._spectator_define_stub(stub) } before_each { dbl._spectator_define_stub(stub) }
it "removes previously defined stubs" do it "removes previously defined stubs" do
expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(5).to(42) expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(5).to(42)
@ -447,7 +451,7 @@ Spectator.describe Spectator::Double do
subject(dbl) { FooBarDouble.new } subject(dbl) { FooBarDouble.new }
let(stub) { Spectator::ValueStub.new(:foo, 5) } let(stub) { Spectator::ValueStub.new(:foo, 5) }
before { dbl._spectator_define_stub(stub) } before_each { dbl._spectator_define_stub(stub) }
# Retrieves symbolic names of methods called on a double. # Retrieves symbolic names of methods called on a double.
def called_method_names(dbl) def called_method_names(dbl)
@ -465,7 +469,7 @@ Spectator.describe Spectator::Double do
it "stores calls to non-stubbed methods" do it "stores calls to non-stubbed methods" do
expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /baz/) expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /baz/)
expect(called_method_names(dbl)).to contain(:baz) expect(called_method_names(dbl)).to eq(%i[baz])
end end
it "stores arguments for a call" do it "stores arguments for a call" do
@ -475,68 +479,4 @@ Spectator.describe Spectator::Double do
expect(call.arguments).to eq(args) expect(call.arguments).to eq(args)
end end
end end
describe "#to_s" do
subject(string) { dbl.to_s }
context "with a name" do
let(dbl) { FooBarDouble.new }
it "indicates it's a double" do
expect(string).to contain("Double")
end
it "contains the double name" do
expect(string).to contain("dbl-name")
end
end
context "without a name" do
let(dbl) { EmptyDouble.new }
it "indicates it's a double" do
expect(string).to contain("Double")
end
it "contains \"Anonymous\"" do
expect(string).to contain("Anonymous")
end
end
end
describe "#inspect" do
subject(string) { dbl.inspect }
context "with a name" do
let(dbl) { FooBarDouble.new }
it "indicates it's a double" do
expect(string).to contain("Double")
end
it "contains the double name" do
expect(string).to contain("dbl-name")
end
it "contains the object ID" do
expect(string).to contain(dbl.object_id.to_s(16))
end
end
context "without a name" do
let(dbl) { EmptyDouble.new }
it "indicates it's a double" do
expect(string).to contain("Double")
end
it "contains \"Anonymous\"" do
expect(string).to contain("Anonymous")
end
it "contains the object ID" do
expect(string).to contain(dbl.object_id.to_s(16))
end
end
end
end end

View file

@ -1,325 +0,0 @@
require "../../spec_helper"
Spectator.describe Spectator::FormalArguments do
subject(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
it "stores the arguments" do
expect(arguments).to have_attributes(
args: {arg1: 42, arg2: "foo"},
splat_name: :splat,
splat: {:x, :y, :z},
kwargs: {bar: "baz", qux: 123}
)
end
describe ".build" do
subject { Spectator::FormalArguments.build({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, {bar: "baz", qux: 123}) }
it "stores the arguments and keyword arguments" do
is_expected.to have_attributes(
args: {arg1: 42, arg2: "foo"},
splat_name: :splat,
splat: {1, 2, 3},
kwargs: {bar: "baz", qux: 123}
)
end
context "without a splat" do
subject { Spectator::FormalArguments.build({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123}) }
it "stores the arguments and keyword arguments" do
is_expected.to have_attributes(
args: {arg1: 42, arg2: "foo"},
splat: nil,
kwargs: {bar: "baz", qux: 123}
)
end
end
end
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::FormalArguments.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 named positional arguments" do
subject(arguments) { Spectator::FormalArguments.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)
end
end
end
end
describe "#to_s" do
subject { arguments.to_s }
it "formats the arguments" do
is_expected.to eq("(arg1: 42, arg2: \"foo\", *splat: {:x, :y, :z}, bar: \"baz\", qux: 123)")
end
context "when empty" do
let(arguments) { Spectator::FormalArguments.none }
it "returns (no args)" do
is_expected.to eq("(no args)")
end
end
context "with a splat and no arguments" do
let(arguments) { Spectator::FormalArguments.build(NamedTuple.new, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
it "omits the splat name" do
is_expected.to eq("(:x, :y, :z, bar: \"baz\", qux: 123)")
end
end
end
describe "#==" do
subject { arguments == other }
context "with Arguments" do
context "with equal arguments" do
let(other) { Spectator::Arguments.new(arguments.positional, arguments.kwargs) }
it { is_expected.to be_true }
end
context "with different arguments" do
let(other) { Spectator::Arguments.new({123, :foo, "bar"}, {opt: "foobar"}) }
it { is_expected.to be_false }
end
context "with the same kwargs in a different order" do
let(other) { Spectator::Arguments.new(arguments.positional, {qux: 123, bar: "baz"}) }
it { is_expected.to be_true }
end
context "with a missing kwarg" do
let(other) { Spectator::Arguments.new(arguments.positional, {bar: "baz"}) }
it { is_expected.to be_false }
end
context "with an extra kwarg" do
let(other) { Spectator::Arguments.new(arguments.positional, {bar: "baz", qux: 123, extra: 0}) }
it { is_expected.to be_false }
end
end
context "with FormalArguments" do
context "with equal arguments" do
let(other) { arguments }
it { is_expected.to be_true }
end
context "with different arguments" do
let(other) { Spectator::FormalArguments.new({arg1: 123, arg2: :foo, arg3: "bar"}, :splat, {1, 2, 3}, {opt: "foobar"}) }
it { is_expected.to be_false }
end
context "with the same kwargs in a different order" do
let(other) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {qux: 123, bar: "baz"}) }
it { is_expected.to be_true }
end
context "with a missing kwarg" do
let(other) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: "baz"}) }
it { is_expected.to be_false }
end
context "with an extra kwarg" do
let(other) { Spectator::FormalArguments.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::FormalArguments.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::FormalArguments.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::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
it { is_expected.to be_true }
end
end
end
describe "#===" do
subject { pattern === arguments }
context "with Arguments" do
let(arguments) { Spectator::Arguments.new({42, "foo"}, {bar: "baz", qux: 123}) }
context "with equal arguments" do
let(pattern) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123}) }
it { is_expected.to be_true }
end
context "with matching arguments" do
let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, {bar: /baz/, qux: Int32}) }
it { is_expected.to be_true }
end
context "with non-matching arguments" do
let(pattern) { Spectator::FormalArguments.new({arg1: Float64, arg2: /bar/}, {bar: /foo/, qux: "123"}) }
it { is_expected.to be_false }
end
context "with different arguments" do
let(pattern) { Spectator::FormalArguments.new({arg1: 123, arg2: :foo, arg3: "bar"}, {opt: "foobar"}) }
it { is_expected.to be_false }
end
context "with the same kwargs in a different order" do
let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, {qux: Int32, bar: /baz/}) }
it { is_expected.to be_true }
end
context "with an additional kwarg" do
let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, {bar: /baz/}) }
it { is_expected.to be_true }
end
context "with a missing kwarg" do
let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, {bar: /baz/, qux: Int32, extra: 0}) }
it { is_expected.to be_false }
end
end
context "with FormalArguments" do
context "with equal arguments" do
let(pattern) { arguments }
it { is_expected.to be_true }
end
context "with matching arguments" do
let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /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::FormalArguments.new({arg1: Float64, arg2: /bar/}, :splat, {String, Int32, :x}, {bar: /foo/, qux: "123"}) }
it { is_expected.to be_false }
end
context "with different arguments" do
let(pattern) { Spectator::FormalArguments.new({arg1: 123, arg2: :foo, arg3: "bar"}, :splat, {1, 2, 3}, {opt: "foobar"}) }
it { is_expected.to be_false }
end
context "with the same kwargs in a different order" do
let(pattern) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {qux: Int32, bar: /baz/}) }
it { is_expected.to be_true }
end
context "with an additional kwarg" do
let(pattern) { Spectator::FormalArguments.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::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: /baz/, qux: Int32, extra: 0}) }
it { is_expected.to be_false }
end
context "with different splat arguments" do
let(pattern) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, {1, 2, 3}, arguments.kwargs) }
it { is_expected.to be_false }
end
context "with matching mixed positional tuple types" do
let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, arguments.splat_name, arguments.splat, arguments.kwargs) }
it { is_expected.to be_true }
end
context "with non-matching mixed positional tuple types" do
let(pattern) { Spectator::FormalArguments.new({arg1: Float64, arg2: /bar/}, arguments.splat_name, arguments.splat, arguments.kwargs) }
it { is_expected.to be_false }
end
end
end
end

View file

@ -235,9 +235,16 @@ Spectator.describe Spectator::LazyDouble do
end end
context "with previously undefined methods" do context "with previously undefined methods" do
it "raises an error" do it "can stub methods" do
stub = Spectator::ValueStub.new(:baz, :xyz) stub = Spectator::ValueStub.new(:baz, :xyz)
expect { dbl._spectator_define_stub(stub) }.to raise_error(/stub/) dbl._spectator_define_stub(stub)
expect(dbl.baz).to eq(:xyz)
end
it "uses a stub only if an argument constraint is met" do
stub = Spectator::ValueStub.new(:baz, :xyz, Spectator::Arguments.capture(:right))
dbl._spectator_define_stub(stub)
expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /baz/)
end end
end end
end end
@ -246,18 +253,27 @@ Spectator.describe Spectator::LazyDouble do
subject(dbl) { Spectator::LazyDouble.new(foo: 42, bar: "baz") } subject(dbl) { Spectator::LazyDouble.new(foo: 42, bar: "baz") }
let(stub) { Spectator::ValueStub.new(:foo, 5) } let(stub) { Spectator::ValueStub.new(:foo, 5) }
before { dbl._spectator_define_stub(stub) } before_each { dbl._spectator_define_stub(stub) }
it "removes previously defined stubs" do it "removes previously defined stubs" do
expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(5).to(42) expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(5).to(42)
end end
it "raises on methods without an implementation" do
stub = Spectator::ValueStub.new(:baz, :xyz)
dbl._spectator_define_stub(stub)
expect(dbl.baz).to eq(:xyz)
dbl._spectator_clear_stubs
expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /baz/)
end
end end
describe "#_spectator_calls" do describe "#_spectator_calls" do
subject(dbl) { Spectator::LazyDouble.new(foo: 42, bar: "baz") } subject(dbl) { Spectator::LazyDouble.new(foo: 42, bar: "baz") }
let(stub) { Spectator::ValueStub.new(:foo, 5) } let(stub) { Spectator::ValueStub.new(:foo, 5) }
before { dbl._spectator_define_stub(stub) } before_each { dbl._spectator_define_stub(stub) }
# Retrieves symbolic names of methods called on a double. # Retrieves symbolic names of methods called on a double.
def called_method_names(dbl) def called_method_names(dbl)
@ -275,7 +291,7 @@ Spectator.describe Spectator::LazyDouble do
it "stores calls to non-stubbed methods" do it "stores calls to non-stubbed methods" do
expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /baz/) expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /baz/)
expect(called_method_names(dbl)).to contain(:baz) expect(called_method_names(dbl)).to eq(%i[baz])
end end
it "stores arguments for a call" do it "stores arguments for a call" do
@ -285,68 +301,4 @@ Spectator.describe Spectator::LazyDouble do
expect(call.arguments).to eq(args) expect(call.arguments).to eq(args)
end end
end end
describe "#to_s" do
subject(string) { dbl.to_s }
context "with a name" do
let(dbl) { Spectator::LazyDouble.new("dbl-name") }
it "indicates it's a double" do
expect(string).to contain("LazyDouble")
end
it "contains the double name" do
expect(string).to contain("dbl-name")
end
end
context "without a name" do
let(dbl) { Spectator::LazyDouble.new }
it "contains the double type" do
expect(string).to contain("LazyDouble")
end
it "contains \"Anonymous\"" do
expect(string).to contain("Anonymous")
end
end
end
describe "#inspect" do
subject(string) { dbl.inspect }
context "with a name" do
let(dbl) { Spectator::LazyDouble.new("dbl-name") }
it "contains the double type" do
expect(string).to contain("LazyDouble")
end
it "contains the double name" do
expect(string).to contain("dbl-name")
end
it "contains the object ID" do
expect(string).to contain(dbl.object_id.to_s(16))
end
end
context "without a name" do
let(dbl) { Spectator::LazyDouble.new }
it "contains the double type" do
expect(string).to contain("LazyDouble")
end
it "contains \"Anonymous\"" do
expect(string).to contain("Anonymous")
end
it "contains the object ID" do
expect(string).to contain(dbl.object_id.to_s(16))
end
end
end
end end

View file

@ -29,18 +29,8 @@ Spectator.describe Spectator::Mock do
@_spectator_invocations << :method3 @_spectator_invocations << :method3
"original" "original"
end end
def method4 : Thing
self
end end
def method5 : OtherThing
OtherThing.new
end
end
class OtherThing; end
Spectator::Mock.define_subtype(:class, Thing, MockThing, :mock_name, method1: 123) do Spectator::Mock.define_subtype(:class, Thing, MockThing, :mock_name, method1: 123) do
stub def method2 stub def method2
:stubbed :stubbed
@ -114,20 +104,6 @@ Spectator.describe Spectator::Mock do
mock.method3 mock.method3
expect(mock._spectator_invocations).to contain_exactly(:method3) expect(mock._spectator_invocations).to contain_exactly(:method3)
end end
it "can reference its own type" do
new_mock = MockThing.new
stub = Spectator::ValueStub.new(:method4, new_mock)
mock._spectator_define_stub(stub)
expect(mock.method4).to be(new_mock)
end
it "can reference other types in the original namespace" do
other = OtherThing.new
stub = Spectator::ValueStub.new(:method5, other)
mock._spectator_define_stub(stub)
expect(mock.method5).to be(other)
end
end end
context "with an abstract class" do context "with an abstract class" do
@ -144,14 +120,8 @@ Spectator.describe Spectator::Mock do
end end
abstract def method4 abstract def method4
abstract def method4 : Thing
abstract def method5 : OtherThing
end end
class OtherThing; end
Spectator::Mock.define_subtype(:class, Thing, MockThing, :mock_name, method2: :stubbed) do Spectator::Mock.define_subtype(:class, Thing, MockThing, :mock_name, method2: :stubbed) do
stub def method1 : Int32 # NOTE: Return type is required since one wasn't provided in the parent. stub def method1 : Int32 # NOTE: Return type is required since one wasn't provided in the parent.
123 123
@ -229,20 +199,6 @@ Spectator.describe Spectator::Mock do
mock.method3 mock.method3
expect(mock._spectator_invocations).to contain_exactly(:method3) expect(mock._spectator_invocations).to contain_exactly(:method3)
end end
it "can reference its own type" do
new_mock = MockThing.new
stub = Spectator::ValueStub.new(:method4, new_mock)
mock._spectator_define_stub(stub)
expect(mock.method4).to be(new_mock)
end
it "can reference other types in the original namespace" do
other = OtherThing.new
stub = Spectator::ValueStub.new(:method5, other)
mock._spectator_define_stub(stub)
expect(mock.method5).to be(other)
end
end end
context "with an abstract struct" do context "with an abstract struct" do
@ -259,14 +215,8 @@ Spectator.describe Spectator::Mock do
end end
abstract def method4 abstract def method4
abstract def method4 : Thing
abstract def method5 : OtherThing
end end
class OtherThing; end
Spectator::Mock.define_subtype(:struct, Thing, MockThing, :mock_name, method2: :stubbed) do Spectator::Mock.define_subtype(:struct, Thing, MockThing, :mock_name, method2: :stubbed) do
stub def method1 : Int32 # NOTE: Return type is required since one wasn't provided in the parent. stub def method1 : Int32 # NOTE: Return type is required since one wasn't provided in the parent.
123 123
@ -336,22 +286,6 @@ Spectator.describe Spectator::Mock do
mock.method3 mock.method3
expect(mock._spectator_invocations).to contain_exactly(:method3) expect(mock._spectator_invocations).to contain_exactly(:method3)
end end
it "can reference its own type" do
mock = self.mock # FIXME: Workaround for passing by value messing with stubs.
new_mock = MockThing.new
stub = Spectator::ValueStub.new(:method4, new_mock)
mock._spectator_define_stub(stub)
expect(mock.method4).to be_a(Thing)
end
it "can reference other types in the original namespace" do
mock = self.mock # FIXME: Workaround for passing by value messing with stubs.
other = OtherThing.new
stub = Spectator::ValueStub.new(:method5, other)
mock._spectator_define_stub(stub)
expect(mock.method5).to be(other)
end
end end
context "class method stubs" do context "class method stubs" do
@ -364,21 +298,11 @@ Spectator.describe Spectator::Mock do
arg arg
end end
def self.baz(arg, &) def self.baz(arg)
yield yield
end end
def self.thing : Thing
new
end end
def self.other : OtherThing
OtherThing.new
end
end
class OtherThing; end
Spectator::Mock.define_subtype(:class, Thing, MockThing) do Spectator::Mock.define_subtype(:class, Thing, MockThing) do
stub def self.foo stub def self.foo
:stub :stub
@ -388,7 +312,7 @@ Spectator.describe Spectator::Mock do
let(mock) { MockThing } let(mock) { MockThing }
let(foo_stub) { Spectator::ValueStub.new(:foo, :override) } let(foo_stub) { Spectator::ValueStub.new(:foo, :override) }
after { mock._spectator_clear_stubs } after_each { mock._spectator_clear_stubs }
it "overrides an existing method" do it "overrides an existing method" do
expect { mock._spectator_define_stub(foo_stub) }.to change { mock.foo }.from(:stub).to(:override) expect { mock._spectator_define_stub(foo_stub) }.to change { mock.foo }.from(:stub).to(:override)
@ -443,22 +367,8 @@ Spectator.describe Spectator::Mock do
expect(restricted(mock)).to eq(:stub) expect(restricted(mock)).to eq(:stub)
end end
it "can reference its own type" do
new_mock = MockThing.new
stub = Spectator::ValueStub.new(:thing, new_mock)
mock._spectator_define_stub(stub)
expect(mock.thing).to be(new_mock)
end
it "can reference other types in the original namespace" do
other = OtherThing.new
stub = Spectator::ValueStub.new(:other, other)
mock._spectator_define_stub(stub)
expect(mock.other).to be(other)
end
describe "._spectator_clear_stubs" do describe "._spectator_clear_stubs" do
before { mock._spectator_define_stub(foo_stub) } before_each { mock._spectator_define_stub(foo_stub) }
it "removes previously defined stubs" do it "removes previously defined stubs" do
expect { mock._spectator_clear_stubs }.to change { mock.foo }.from(:override).to(:stub) expect { mock._spectator_clear_stubs }.to change { mock.foo }.from(:override).to(:stub)
@ -466,7 +376,7 @@ Spectator.describe Spectator::Mock do
end end
describe "._spectator_calls" do describe "._spectator_calls" do
before { mock._spectator_clear_calls } before_each { mock._spectator_clear_calls }
# Retrieves symbolic names of methods called on a mock. # Retrieves symbolic names of methods called on a mock.
def called_method_names(mock) def called_method_names(mock)
@ -491,203 +401,6 @@ Spectator.describe Spectator::Mock do
end end
end end
context "with a module" do
module Thing
# `extend self` cannot be used.
# The Crystal compiler doesn't report the methods as class methods when doing so.
def self.original_method
:original
end
def self.default_method
:original
end
def self.stubbed_method(_value = 42)
:original
end
end
Spectator::Mock.define_subtype(:module, Thing, MockThing) do
stub def self.stubbed_method(_value = 42)
:stubbed
end
end
let(mock) { MockThing }
after { mock._spectator_clear_stubs }
it "overrides an existing method" do
stub = Spectator::ValueStub.new(:original_method, :override)
expect { mock._spectator_define_stub(stub) }.to change { mock.original_method }.from(:original).to(:override)
end
it "doesn't affect other methods" do
stub = Spectator::ValueStub.new(:stubbed_method, :override)
expect { mock._spectator_define_stub(stub) }.to_not change { mock.original_method }
end
it "replaces an existing default stub" do
stub = Spectator::ValueStub.new(:default_method, :override)
expect { mock._spectator_define_stub(stub) }.to change { mock.default_method }.to(:override)
end
it "replaces an existing stubbed method" do
stub = Spectator::ValueStub.new(:stubbed_method, :override)
expect { mock._spectator_define_stub(stub) }.to change { mock.stubbed_method }.to(:override)
end
def restricted(thing : Thing.class)
thing.stubbed_method
end
it "can be used in type restricted methods" do
expect(restricted(mock)).to eq(:stubbed)
end
describe "._spectator_clear_stubs" do
before do
stub = Spectator::ValueStub.new(:original_method, :override)
mock._spectator_define_stub(stub)
end
it "removes previously defined stubs" do
expect { mock._spectator_clear_stubs }.to change { mock.original_method }.from(:override).to(:original)
end
end
describe "._spectator_calls" do
before { mock._spectator_clear_calls }
# Retrieves symbolic names of methods called on a mock.
def called_method_names(mock)
mock._spectator_calls.map(&.method)
end
it "stores calls to original methods" do
expect { mock.original_method }.to change { called_method_names(mock) }.from(%i[]).to(%i[original_method])
end
it "stores calls to default methods" do
expect { mock.default_method }.to change { called_method_names(mock) }.from(%i[]).to(%i[default_method])
end
it "stores calls to stubbed methods" do
expect { mock.stubbed_method }.to change { called_method_names(mock) }.from(%i[]).to(%i[stubbed_method])
end
it "stores multiple calls to the same stub" do
mock.stubbed_method
expect { mock.stubbed_method }.to change { called_method_names(mock) }.from(%i[stubbed_method]).to(%i[stubbed_method stubbed_method])
end
it "stores arguments for a call" do
mock.stubbed_method(5)
args = Spectator::Arguments.capture(5)
call = mock._spectator_calls.first
expect(call.arguments).to eq(args)
end
end
end
context "with a mocked module included in a class" do
module Thing
def original_method
:original
end
def default_method
:original
end
def stubbed_method(_value = 42)
:original
end
end
Spectator::Mock.define_subtype(:module, Thing, MockThing, default_method: :default) do
stub def stubbed_method(_value = 42)
:stubbed
end
end
class IncludedMock
include MockThing
end
let(mock) { IncludedMock.new }
it "overrides an existing method" do
stub = Spectator::ValueStub.new(:original_method, :override)
expect { mock._spectator_define_stub(stub) }.to change { mock.original_method }.from(:original).to(:override)
end
it "doesn't affect other methods" do
stub = Spectator::ValueStub.new(:stubbed_method, :override)
expect { mock._spectator_define_stub(stub) }.to_not change { mock.original_method }
end
it "replaces an existing default stub" do
stub = Spectator::ValueStub.new(:default_method, :override)
expect { mock._spectator_define_stub(stub) }.to change { mock.default_method }.to(:override)
end
it "replaces an existing stubbed method" do
stub = Spectator::ValueStub.new(:stubbed_method, :override)
expect { mock._spectator_define_stub(stub) }.to change { mock.stubbed_method }.to(:override)
end
def restricted(thing : Thing.class)
thing.default_method
end
describe "#_spectator_clear_stubs" do
before do
stub = Spectator::ValueStub.new(:original_method, :override)
mock._spectator_define_stub(stub)
end
it "removes previously defined stubs" do
expect { mock._spectator_clear_stubs }.to change { mock.original_method }.from(:override).to(:original)
end
end
describe "#_spectator_calls" do
before { mock._spectator_clear_calls }
# Retrieves symbolic names of methods called on a mock.
def called_method_names(mock)
mock._spectator_calls.map(&.method)
end
it "stores calls to original methods" do
expect { mock.original_method }.to change { called_method_names(mock) }.from(%i[]).to(%i[original_method])
end
it "stores calls to default methods" do
expect { mock.default_method }.to change { called_method_names(mock) }.from(%i[]).to(%i[default_method])
end
it "stores calls to stubbed methods" do
expect { mock.stubbed_method }.to change { called_method_names(mock) }.from(%i[]).to(%i[stubbed_method])
end
it "stores multiple calls to the same stub" do
mock.stubbed_method
expect { mock.stubbed_method }.to change { called_method_names(mock) }.from(%i[stubbed_method]).to(%i[stubbed_method stubbed_method])
end
it "stores arguments for a call" do
mock.stubbed_method(5)
args = Spectator::Arguments.capture(5)
call = mock._spectator_calls.first
expect(call.arguments).to eq(args)
end
end
end
context "with a method that uses NoReturn" do context "with a method that uses NoReturn" do
abstract class Thing abstract class Thing
abstract def oops : NoReturn abstract def oops : NoReturn
@ -697,7 +410,7 @@ Spectator.describe Spectator::Mock do
let(mock) { MockThing.new } let(mock) { MockThing.new }
after { mock._spectator_clear_stubs } after_each { mock._spectator_clear_stubs }
it "raises a TypeCastError when using a value-based stub" do it "raises a TypeCastError when using a value-based stub" do
stub = Spectator::ValueStub.new(:oops, nil).as(Spectator::Stub) stub = Spectator::ValueStub.new(:oops, nil).as(Spectator::Stub)
@ -748,7 +461,7 @@ Spectator.describe Spectator::Mock do
let(mock) { MockedClass.new } let(mock) { MockedClass.new }
# Necessary to clear stubs to prevent leakages between tests. # Necessary to clear stubs to prevent leakages between tests.
after { mock._spectator_clear_stubs } after_each { mock._spectator_clear_stubs }
it "overrides responses from methods with keyword arguments" do it "overrides responses from methods with keyword arguments" do
expect(mock.method1).to eq(123) expect(mock.method1).to eq(123)
@ -858,8 +571,8 @@ Spectator.describe Spectator::Mock do
let(mock) { MockedStruct.new } let(mock) { MockedStruct.new }
# Necessary to clear stubs to prevent leakages between tests. # Necessary to clear stubs to prevent leakages between tests.
after { mock._spectator_clear_stubs } after_each { mock._spectator_clear_stubs }
after { MockedStruct._spectator_invocations.clear } after_each { MockedStruct._spectator_invocations.clear }
it "overrides responses from methods with keyword arguments" do it "overrides responses from methods with keyword arguments" do
expect(mock.method1).to eq(123) expect(mock.method1).to eq(123)
@ -929,7 +642,7 @@ Spectator.describe Spectator::Mock do
arg arg
end end
def self.baz(arg, &) def self.baz(arg)
yield yield
end end
end end
@ -943,7 +656,7 @@ Spectator.describe Spectator::Mock do
let(mock) { Thing } let(mock) { Thing }
let(foo_stub) { Spectator::ValueStub.new(:foo, :override) } let(foo_stub) { Spectator::ValueStub.new(:foo, :override) }
after { mock._spectator_clear_stubs } after_each { mock._spectator_clear_stubs }
it "overrides an existing method" do it "overrides an existing method" do
expect { mock._spectator_define_stub(foo_stub) }.to change { mock.foo }.from(:stub).to(:override) expect { mock._spectator_define_stub(foo_stub) }.to change { mock.foo }.from(:stub).to(:override)
@ -999,7 +712,7 @@ Spectator.describe Spectator::Mock do
end end
describe "._spectator_clear_stubs" do describe "._spectator_clear_stubs" do
before { mock._spectator_define_stub(foo_stub) } before_each { mock._spectator_define_stub(foo_stub) }
it "removes previously defined stubs" do it "removes previously defined stubs" do
expect { mock._spectator_clear_stubs }.to change { mock.foo }.from(:override).to(:stub) expect { mock._spectator_clear_stubs }.to change { mock.foo }.from(:override).to(:stub)
@ -1007,7 +720,7 @@ Spectator.describe Spectator::Mock do
end end
describe "._spectator_calls" do describe "._spectator_calls" do
before { mock._spectator_clear_calls } before_each { mock._spectator_clear_calls }
# Retrieves symbolic names of methods called on a mock. # Retrieves symbolic names of methods called on a mock.
def called_method_names(mock) def called_method_names(mock)
@ -1043,7 +756,7 @@ Spectator.describe Spectator::Mock do
let(mock) { NoReturnThing.new } let(mock) { NoReturnThing.new }
after { mock._spectator_clear_stubs } after_each { mock._spectator_clear_stubs }
it "raises a TypeCastError when using a value-based stub" do it "raises a TypeCastError when using a value-based stub" do
stub = Spectator::ValueStub.new(:oops, nil).as(Spectator::Stub) stub = Spectator::ValueStub.new(:oops, nil).as(Spectator::Stub)

View file

@ -186,9 +186,12 @@ Spectator.describe Spectator::NullDouble do
expect(dbl.hash).to be_a(UInt64) expect(dbl.hash).to be_a(UInt64)
expect(dbl.in?([42])).to be_false expect(dbl.in?([42])).to be_false
expect(dbl.in?(1, 2, 3)).to be_false expect(dbl.in?(1, 2, 3)).to be_false
expect(dbl.inspect).to contain("EmptyDouble")
expect(dbl.itself).to be(dbl) expect(dbl.itself).to be(dbl)
expect(dbl.not_nil!).to be(dbl) expect(dbl.not_nil!).to be(dbl)
expect(dbl.pretty_inspect).to contain("EmptyDouble")
expect(dbl.tap { nil }).to be(dbl) expect(dbl.tap { nil }).to be(dbl)
expect(dbl.to_s).to contain("EmptyDouble")
expect(dbl.try { nil }).to be_nil expect(dbl.try { nil }).to be_nil
expect(dbl.object_id).to be_a(UInt64) expect(dbl.object_id).to be_a(UInt64)
expect(dbl.same?(dbl)).to be_true expect(dbl.same?(dbl)).to be_true
@ -259,7 +262,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
@ -267,7 +270,7 @@ Spectator.describe Spectator::NullDouble do
subject(dbl) { ClassDouble } subject(dbl) { ClassDouble }
let(foo_stub) { Spectator::ValueStub.new(:foo, :override) } let(foo_stub) { Spectator::ValueStub.new(:foo, :override) }
after { dbl._spectator_clear_stubs } after_each { dbl._spectator_clear_stubs }
it "overrides an existing method" do it "overrides an existing method" do
expect { dbl._spectator_define_stub(foo_stub) }.to change { dbl.foo }.from(:stub).to(:override) expect { dbl._spectator_define_stub(foo_stub) }.to change { dbl.foo }.from(:stub).to(:override)
@ -315,7 +318,7 @@ Spectator.describe Spectator::NullDouble do
end end
describe "._spectator_clear_stubs" do describe "._spectator_clear_stubs" do
before { dbl._spectator_define_stub(foo_stub) } before_each { dbl._spectator_define_stub(foo_stub) }
it "removes previously defined stubs" do it "removes previously defined stubs" do
expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(:override).to(:stub) expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(:override).to(:stub)
@ -323,7 +326,7 @@ Spectator.describe Spectator::NullDouble do
end end
describe "._spectator_calls" do describe "._spectator_calls" do
before { dbl._spectator_clear_calls } before_each { dbl._spectator_clear_calls }
# Retrieves symbolic names of methods called on a double. # Retrieves symbolic names of methods called on a double.
def called_method_names(dbl) def called_method_names(dbl)
@ -398,7 +401,7 @@ Spectator.describe Spectator::NullDouble do
subject(dbl) { FooBarDouble.new } subject(dbl) { FooBarDouble.new }
let(stub) { Spectator::ValueStub.new(:foo, 5) } let(stub) { Spectator::ValueStub.new(:foo, 5) }
before { dbl._spectator_define_stub(stub) } before_each { dbl._spectator_define_stub(stub) }
it "removes previously defined stubs" do it "removes previously defined stubs" do
expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(5).to(42) expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(5).to(42)
@ -409,7 +412,7 @@ Spectator.describe Spectator::NullDouble do
subject(dbl) { FooBarDouble.new } subject(dbl) { FooBarDouble.new }
let(stub) { Spectator::ValueStub.new(:foo, 5) } let(stub) { Spectator::ValueStub.new(:foo, 5) }
before { dbl._spectator_define_stub(stub) } before_each { dbl._spectator_define_stub(stub) }
# Retrieves symbolic names of methods called on a double. # Retrieves symbolic names of methods called on a double.
def called_method_names(dbl) def called_method_names(dbl)
@ -436,68 +439,4 @@ Spectator.describe Spectator::NullDouble do
expect(call.arguments).to eq(args) expect(call.arguments).to eq(args)
end end
end end
describe "#to_s" do
subject(string) { dbl.to_s }
context "with a name" do
let(dbl) { FooBarDouble.new }
it "indicates it's a double" do
expect(string).to contain("NullDouble")
end
it "contains the double name" do
expect(string).to contain("dbl-name")
end
end
context "without a name" do
let(dbl) { EmptyDouble.new }
it "contains the double type" do
expect(string).to contain("NullDouble")
end
it "contains \"Anonymous\"" do
expect(string).to contain("Anonymous")
end
end
end
describe "#inspect" do
subject(string) { dbl.inspect }
context "with a name" do
let(dbl) { FooBarDouble.new }
it "contains the double type" do
expect(string).to contain("NullDouble")
end
it "contains the double name" do
expect(string).to contain("dbl-name")
end
it "contains the object ID" do
expect(string).to contain(dbl.object_id.to_s(16))
end
end
context "without a name" do
let(dbl) { EmptyDouble.new }
it "contains the double type" do
expect(string).to contain("NullDouble")
end
it "contains \"Anonymous\"" do
expect(string).to contain("Anonymous")
end
it "contains the object ID" do
expect(string).to contain(dbl.object_id.to_s(16))
end
end
end
end end

View file

@ -34,7 +34,7 @@ module Spectator
# Produces a string representation of the expression. # Produces a string representation of the expression.
# This consists of the label (if one is available) and the value. # This consists of the label (if one is available) and the value.
def to_s(io : IO) : Nil def to_s(io)
if (label = @label) if (label = @label)
io << label << ": " io << label << ": "
end end
@ -43,7 +43,7 @@ module Spectator
# Produces a detailed string representation of the expression. # Produces a detailed string representation of the expression.
# This consists of the label (if one is available) and the value. # This consists of the label (if one is available) and the value.
def inspect(io : IO) : Nil def inspect(io)
if (label = @label) if (label = @label)
io << label << ": " io << label << ": "
end end

View file

@ -13,12 +13,12 @@ module Spectator
end end
# Displays "anything". # Displays "anything".
def to_s(io : IO) : Nil def to_s(io)
io << "anything" io << "anything"
end end
# Displays "<anything>". # Displays "<anything>".
def inspect(io : IO) : Nil def inspect(io)
io << "<anything>" io << "<anything>"
end end
end end

View file

@ -112,7 +112,7 @@ module Spectator
# Adds the example filter option to the parser. # Adds the example filter option to the parser.
private def example_option(parser, builder) private def example_option(parser, builder)
parser.on("-e", "--example STRING", "Run examples whose full nested names include STRING") do |pattern| parser.on("-e", "--example STRING", "Run examples whose full nested names include STRING") do |pattern|
Log.debug { "Filtering for examples containing '#{pattern}' (-e '#{pattern}')" } Log.debug { "Filtering for examples named '#{pattern}' (-e '#{pattern}')" }
filter = NameNodeFilter.new(pattern) filter = NameNodeFilter.new(pattern)
builder.add_node_filter(filter) builder.add_node_filter(filter)
end end

View file

@ -4,23 +4,18 @@
# This type is intentionally outside the `Spectator` module. # This type is intentionally outside the `Spectator` module.
# 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.
def eval(&)
with self yield
end
# Produces a dummy string to represent the context as a string. # Produces a dummy string to represent the context as a string.
# This prevents the default behavior, which normally stringifies instance variables. # This prevents the default behavior, which normally stringifies instance variables.
# Due to the sheer amount of types Spectator can create # Due to the sheer amount of types Spectator can create
# and that the Crystal compiler instantiates a `#to_s` and/or `#inspect` for each of those types, # and that the Crystal compiler instantiates a `#to_s` and/or `#inspect` for each of those types,
# an explosion in method instances can be created. # an explosion in method instances can be created.
# The compile time is drastically reduced by using a dummy string instead. # The compile time is drastically reduced by using a dummy string instead.
def to_s(io : IO) : Nil def to_s(io)
io << "Context" io << "Context"
end end
# :ditto: # :ditto:
def inspect(io : IO) : Nil def inspect(io)
io << "Context<" << self.class << '>' io << "Context<" << self.class << '>'
end end
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

@ -137,11 +137,7 @@ module Spectator::DSL
what.is_a?(NilLiteral) %} what.is_a?(NilLiteral) %}
{{what}} {{what}}
{% elsif what.is_a?(StringInterpolation) %} {% elsif what.is_a?(StringInterpolation) %}
{{@type.name}}.new.eval do {% raise "String interpolation isn't supported for example group names" %}
{{what}}
rescue e
"<Failed to evaluate context label - #{e.class}: #{e}>"
end
{% else %} {% else %}
{{what.stringify}} {{what.stringify}}
{% end %} {% end %}

View file

@ -124,21 +124,11 @@ module Spectator::DSL
# This means that values defined by `let` and `subject` are available. # This means that values defined by `let` and `subject` are available.
define_example_hook :before_each define_example_hook :before_each
# :ditto:
macro before(&block)
before_each {{block}}
end
# Defines a block of code that will be invoked after every example in the group. # Defines a block of code that will be invoked after every example in the group.
# The block will be run in the context of the current running example. # The block will be run in the context of the current running example.
# This means that values defined by `let` and `subject` are available. # This means that values defined by `let` and `subject` are available.
define_example_hook :after_each define_example_hook :after_each
# :ditto:
macro after(&block)
after_each {{block}}
end
# Defines a block of code that will be invoked around every example in the group. # Defines a block of code that will be invoked around every example in the group.
# The block will be run in the context of the current running example. # The block will be run in the context of the current running example.
# This means that values defined by `let` and `subject` are available. # This means that values defined by `let` and `subject` are available.
@ -149,11 +139,6 @@ module Spectator::DSL
# More code can run afterwards (in the block). # More code can run afterwards (in the block).
define_example_hook :around_each define_example_hook :around_each
# :ditto:
macro around(&block)
around_each {{block}}
end
# Defines a block of code that will be invoked before every example in the group. # Defines a block of code that will be invoked before every example in the group.
# The block will be run in the context of the current running example. # The block will be run in the context of the current running example.
# This means that values defined by `let` and `subject` are available. # This means that values defined by `let` and `subject` are available.

View file

@ -790,7 +790,7 @@ module Spectator::DSL
# ``` # ```
# expect_raises { raise "foobar" } # expect_raises { raise "foobar" }
# ``` # ```
macro expect_raises(&block) macro expect_raises
expect {{block}}.to raise_error expect {{block}}.to raise_error
end end

View file

@ -6,9 +6,6 @@ module Spectator::DSL
private macro _spectator_metadata(name, source, *tags, **metadata) private macro _spectator_metadata(name, source, *tags, **metadata)
private def self.{{name.id}} private def self.{{name.id}}
%metadata = {{source.id}}.dup %metadata = {{source.id}}.dup
{% unless tags.empty? && metadata.empty? %}
%metadata ||= ::Spectator::Metadata.new
{% end %}
{% for k in tags %} {% for k in tags %}
%metadata[{{k.id.symbolize}}] = nil %metadata[{{k.id.symbolize}}] = nil
{% end %} {% end %}

View file

@ -31,19 +31,19 @@ module Spectator::DSL
::Spectator::DSL::Mocks::TYPES << {name.id.symbolize, @type.name(generic_args: false).symbolize, double_type_name.symbolize} %} ::Spectator::DSL::Mocks::TYPES << {name.id.symbolize, @type.name(generic_args: false).symbolize, double_type_name.symbolize} %}
# Define the plain double type. # Define the plain double type.
::Spectator::Double.define({{double_type_name}}, {{name}}, {{value_methods.double_splat}}) do ::Spectator::Double.define({{double_type_name}}, {{name}}, {{**value_methods}}) do
# Returns a new double that responds to undefined methods with itself. # Returns a new double that responds to undefined methods with itself.
# See: `NullDouble` # See: `NullDouble`
def as_null_object def as_null_object
{{null_double_type_name}}.new(@stubs) {{null_double_type_name}}.new(@stubs)
end end
{{block.body if block}} {% if block %}{{block.body}}{% end %}
end end
{% begin %} {% begin %}
# Define a matching null double type. # Define a matching null double type.
::Spectator::NullDouble.define({{null_double_type_name}}, {{name}}, {{value_methods.double_splat}}) {{block}} ::Spectator::NullDouble.define({{null_double_type_name}}, {{name}}, {{**value_methods}}) {{block}}
{% end %} {% end %}
end end
@ -94,9 +94,9 @@ module Spectator::DSL
begin begin
%double = {% if found_tuple %} %double = {% if found_tuple %}
{{found_tuple[2].id}}.new({{value_methods.double_splat}}) {{found_tuple[2].id}}.new({{**value_methods}})
{% else %} {% else %}
::Spectator::LazyDouble.new({{name}}, {{value_methods.double_splat}}) ::Spectator::LazyDouble.new({{name}}, {{**value_methods}})
{% end %} {% end %}
::Spectator::Harness.current?.try(&.cleanup { %double._spectator_reset }) ::Spectator::Harness.current?.try(&.cleanup { %double._spectator_reset })
%double %double
@ -176,7 +176,7 @@ module Spectator::DSL
# See `#def_double`. # See `#def_double`.
macro double(name, **value_methods, &block) macro double(name, **value_methods, &block)
{% begin %} {% begin %}
{% if @def %}new_double{% else %}def_double{% end %}({{name}}, {{value_methods.double_splat}}) {{block}} {% if @def %}new_double{% else %}def_double{% end %}({{name}}, {{**value_methods}}) {{block}}
{% end %} {% end %}
end end
@ -189,7 +189,7 @@ module Spectator::DSL
# expect(dbl.foo).to eq(42) # expect(dbl.foo).to eq(42)
# ``` # ```
macro double(**value_methods) macro double(**value_methods)
::Spectator::LazyDouble.new({{value_methods.double_splat}}) ::Spectator::LazyDouble.new({{**value_methods}})
end end
# Defines a new mock type. # Defines a new mock type.
@ -218,16 +218,15 @@ module Spectator::DSL
# end # end
# ``` # ```
private macro def_mock(type, name = nil, **value_methods, &block) private macro def_mock(type, name = nil, **value_methods, &block)
{% resolved = type.resolve {% # Construct a unique type name for the mock by using the number of defined types.
# Construct a unique type name for the mock by using the number of defined types.
index = ::Spectator::DSL::Mocks::TYPES.size index = ::Spectator::DSL::Mocks::TYPES.size
# The type is nested under the original so that any type names from the original can be resolved.
mock_type_name = "Mock#{index}".id mock_type_name = "Mock#{index}".id
# Store information about how the mock is defined and its context. # Store information about how the mock is defined and its context.
# This is important for constructing an instance of the mock later. # This is important for constructing an instance of the mock later.
::Spectator::DSL::Mocks::TYPES << {type.id.symbolize, @type.name(generic_args: false).symbolize, "#{"::".id unless resolved.name.starts_with?("::")}#{resolved.name}::#{mock_type_name}".id.symbolize} ::Spectator::DSL::Mocks::TYPES << {type.id.symbolize, @type.name(generic_args: false).symbolize, mock_type_name.symbolize}
resolved = type.resolve
base = if resolved.class? base = if resolved.class?
:class :class
elsif resolved.struct? elsif resolved.struct?
@ -236,11 +235,7 @@ module Spectator::DSL
:module :module
end %} end %}
{% begin %} ::Spectator::Mock.define_subtype({{base}}, {{type.id}}, {{mock_type_name}}, {{name}}, {{**value_methods}}) {{block}}
{{base.id}} {{"::".id unless resolved.name.starts_with?("::")}}{{resolved.name}}
::Spectator::Mock.define_subtype({{base}}, {{type.id}}, {{mock_type_name}}, {{name}}, {{value_methods.double_splat}}) {{block}}
end
{% end %}
end end
# Instantiates a mock. # Instantiates a mock.
@ -321,7 +316,7 @@ module Spectator::DSL
macro mock(type, **value_methods, &block) macro mock(type, **value_methods, &block)
{% raise "First argument of `mock` must be a type name, not #{type}" unless type.is_a?(Path) || type.is_a?(Generic) || type.is_a?(Union) || type.is_a?(Metaclass) || type.is_a?(TypeNode) %} {% raise "First argument of `mock` must be a type name, not #{type}" unless type.is_a?(Path) || type.is_a?(Generic) || type.is_a?(Union) || type.is_a?(Metaclass) || type.is_a?(TypeNode) %}
{% begin %} {% begin %}
{% if @def %}new_mock{% else %}def_mock{% end %}({{type}}, {{value_methods.double_splat}}) {{block}} {% if @def %}new_mock{% else %}def_mock{% end %}({{type}}, {{**value_methods}}) {{block}}
{% end %} {% end %}
end end
@ -431,7 +426,7 @@ module Spectator::DSL
# This isn't required, but new_mock() should still find this type. # This isn't required, but new_mock() should still find this type.
::Spectator::DSL::Mocks::TYPES << {type.id.symbolize, @type.name(generic_args: false).symbolize, resolved.name.symbolize} %} ::Spectator::DSL::Mocks::TYPES << {type.id.symbolize, @type.name(generic_args: false).symbolize, resolved.name.symbolize} %}
::Spectator::Mock.inject({{base}}, {{resolved.name}}, {{value_methods.double_splat}}) {{block}} ::Spectator::Mock.inject({{base}}, ::{{resolved.name}}, {{**value_methods}}) {{block}}
end end
# Targets a stubbable object (such as a mock or double) for operations. # Targets a stubbable object (such as a mock or double) for operations.

View file

@ -11,12 +11,12 @@ 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
# One-word description of the result. # One-word description of the result.
def to_s(io : IO) : Nil def to_s(io)
io << "error" io << "error"
end end

View file

@ -40,7 +40,7 @@ module Spectator
# Note: The metadata will not be merged with the parent metadata. # Note: The metadata will not be merged with the parent metadata.
def initialize(@context : Context, @entrypoint : self ->, def initialize(@context : Context, @entrypoint : self ->,
name : String? = nil, location : Location? = nil, name : String? = nil, location : Location? = nil,
@group : ExampleGroup? = nil, metadata = nil) @group : ExampleGroup? = nil, metadata = Metadata.new)
super(name, location, metadata) super(name, location, metadata)
# Ensure group is linked. # Ensure group is linked.
@ -58,7 +58,7 @@ module Spectator
# Note: The metadata will not be merged with the parent metadata. # Note: The metadata will not be merged with the parent metadata.
def initialize(@context : Context, @entrypoint : self ->, def initialize(@context : Context, @entrypoint : self ->,
@name_proc : Example -> String, location : Location? = nil, @name_proc : Example -> String, location : Location? = nil,
@group : ExampleGroup? = nil, metadata = nil) @group : ExampleGroup? = nil, metadata = Metadata.new)
super(nil, location, metadata) super(nil, location, metadata)
# Ensure group is linked. # Ensure group is linked.
@ -75,7 +75,7 @@ module Spectator
# A set of *metadata* can be used for filtering and modifying example behavior. # A set of *metadata* can be used for filtering and modifying example behavior.
# Note: The metadata will not be merged with the parent metadata. # Note: The metadata will not be merged with the parent metadata.
def initialize(name : String? = nil, location : Location? = nil, def initialize(name : String? = nil, location : Location? = nil,
@group : ExampleGroup? = nil, metadata = nil, &block : self ->) @group : ExampleGroup? = nil, metadata = Metadata.new, &block : self ->)
super(name, location, metadata) super(name, location, metadata)
@context = NullContext.new @context = NullContext.new
@ -93,10 +93,9 @@ module Spectator
# A set of *metadata* can be used for filtering and modifying example behavior. # A set of *metadata* can be used for filtering and modifying example behavior.
# Note: The metadata will not be merged with the parent metadata. # Note: The metadata will not be merged with the parent metadata.
def self.pending(name : String? = nil, location : Location? = nil, def self.pending(name : String? = nil, location : Location? = nil,
group : ExampleGroup? = nil, metadata = nil, reason = nil) group : ExampleGroup? = nil, metadata = Metadata.new, reason = nil)
# Add pending tag and reason if they don't exist. # Add pending tag and reason if they don't exist.
tags = {:pending => nil, :reason => reason} metadata = metadata.merge({:pending => nil, :reason => reason}) { |_, v, _| v }
metadata = metadata ? metadata.merge(tags) { |_, v, _| v } : tags
new(name, location, group, metadata) { nil } new(name, location, group, metadata) { nil }
end end
@ -104,8 +103,8 @@ module Spectator
# Returns the result of the execution. # Returns the result of the execution.
# The result will also be stored in `#result`. # The result will also be stored in `#result`.
def run : Result def run : Result
Log.debug { "Running example: #{self}" } Log.debug { "Running example #{self}" }
Log.warn { "Example already ran: #{self}" } if @finished Log.warn { "Example #{self} already ran" } if @finished
if pending? if pending?
Log.debug { "Skipping example #{self} - marked pending" } Log.debug { "Skipping example #{self} - marked pending" }
@ -118,7 +117,7 @@ module Spectator
begin begin
@result = Harness.run do @result = Harness.run do
if proc = @name_proc if proc = @name_proc.as?(Proc(Example, String))
self.name = proc.call(self) self.name = proc.call(self)
end end
@ -143,10 +142,8 @@ module Spectator
group.call_before_each(self) group.call_before_each(self)
group.call_pre_condition(self) group.call_pre_condition(self)
end end
Log.trace { "Running example code for: #{self}" }
@entrypoint.call(self) @entrypoint.call(self)
@finished = true @finished = true
Log.trace { "Finished running example code for: #{self}" }
if group = @group if group = @group
group.call_post_condition(self) group.call_post_condition(self)
group.call_after_each(self) group.call_after_each(self)
@ -164,7 +161,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 +181,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
@ -194,7 +191,7 @@ module Spectator
# Constructs the full name or description of the example. # Constructs the full name or description of the example.
# This prepends names of groups this example is part of. # This prepends names of groups this example is part of.
def to_s(io : IO) : Nil def to_s(io)
name = @name name = @name
# Prefix with group's full name if the node belongs to a group. # Prefix with group's full name if the node belongs to a group.
@ -213,9 +210,9 @@ module Spectator
end end
# Exposes information about the example useful for debugging. # Exposes information about the example useful for debugging.
def inspect(io : IO) : Nil def inspect(io)
super super
io << " - " << result io << ' ' << result
end end
# Creates the JSON representation of the example, # Creates the JSON representation of the example,
@ -279,7 +276,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
@ -289,7 +286,7 @@ module Spectator
# Constructs the full name or description of the example. # Constructs the full name or description of the example.
# This prepends names of groups this example is part of. # This prepends names of groups this example is part of.
def to_s(io : IO) : Nil def to_s(io) : Nil
@example.to_s(io) @example.to_s(io)
end end
end end

View file

@ -15,7 +15,7 @@ module Spectator
# The *entrypoint* indicates the proc used to invoke the test code in the example. # The *entrypoint* indicates the proc used to invoke the test code in the example.
# The *name*, *location*, and *metadata* will be applied to the `Example` produced by `#build`. # The *name*, *location*, and *metadata* will be applied to the `Example` produced by `#build`.
def initialize(@context_builder : -> Context, @entrypoint : Example ->, def initialize(@context_builder : -> Context, @entrypoint : Example ->,
@name : String? = nil, @location : Location? = nil, @metadata : Metadata? = nil) @name : String? = nil, @location : Location? = nil, @metadata : Metadata = Metadata.new)
end end
# Creates the builder. # Creates the builder.
@ -24,7 +24,7 @@ module Spectator
# The *name* is an interpolated string that runs in the context of the example. # The *name* is an interpolated string that runs in the context of the example.
# *location*, and *metadata* will be applied to the `Example` produced by `#build`. # *location*, and *metadata* will be applied to the `Example` produced by `#build`.
def initialize(@context_builder : -> Context, @entrypoint : Example ->, def initialize(@context_builder : -> Context, @entrypoint : Example ->,
@name : Example -> String, @location : Location? = nil, @metadata : Metadata? = nil) @name : Example -> String, @location : Location? = nil, @metadata : Metadata = Metadata.new)
end end
# Constructs an example with previously defined attributes and context. # Constructs an example with previously defined attributes and context.

View file

@ -19,14 +19,14 @@ module Spectator
protected setter group : ExampleGroup? protected setter group : ExampleGroup?
define_hook before_all : ExampleGroupHook do define_hook before_all : ExampleGroupHook do
Log.trace { "Processing before_all hooks for: #{self}" } Log.trace { "Processing before_all hooks for #{self}" }
@group.try &.call_before_all @group.try &.call_before_all
before_all_hooks.each &.call_once before_all_hooks.each &.call_once
end end
define_hook after_all : ExampleGroupHook, :prepend do define_hook after_all : ExampleGroupHook, :prepend do
Log.trace { "Processing after_all hooks for: #{self}" } Log.trace { "Processing after_all hooks for #{self}" }
after_all_hooks.each &.call_once if finished? after_all_hooks.each &.call_once if finished?
if group = @group if group = @group
@ -35,21 +35,21 @@ module Spectator
end end
define_hook before_each : ExampleHook do |example| define_hook before_each : ExampleHook do |example|
Log.trace { "Processing before_each hooks for: #{self}" } Log.trace { "Processing before_each hooks for #{self}" }
@group.try &.call_before_each(example) @group.try &.call_before_each(example)
before_each_hooks.each &.call(example) before_each_hooks.each &.call(example)
end end
define_hook after_each : ExampleHook, :prepend do |example| define_hook after_each : ExampleHook, :prepend do |example|
Log.trace { "Processing after_each hooks for: #{self}" } Log.trace { "Processing after_each hooks for #{self}" }
after_each_hooks.each &.call(example) after_each_hooks.each &.call(example)
@group.try &.call_after_each(example) @group.try &.call_after_each(example)
end end
define_hook around_each : ExampleProcsyHook do |procsy| define_hook around_each : ExampleProcsyHook do |procsy|
Log.trace { "Processing around_each hooks for: #{self}" } Log.trace { "Processing around_each hooks for #{self}" }
around_each_hooks.reverse_each { |hook| procsy = hook.wrap(procsy) } around_each_hooks.reverse_each { |hook| procsy = hook.wrap(procsy) }
if group = @group if group = @group
@ -59,14 +59,14 @@ module Spectator
end end
define_hook pre_condition : ExampleHook do |example| define_hook pre_condition : ExampleHook do |example|
Log.trace { "Processing pre_condition hooks for: #{self}" } Log.trace { "Processing pre_condition hooks for #{self}" }
@group.try &.call_pre_condition(example) @group.try &.call_pre_condition(example)
pre_condition_hooks.each &.call(example) pre_condition_hooks.each &.call(example)
end end
define_hook post_condition : ExampleHook, :prepend do |example| define_hook post_condition : ExampleHook, :prepend do |example|
Log.trace { "Processing post_condition hooks for: #{self}" } Log.trace { "Processing post_condition hooks for #{self}" }
post_condition_hooks.each &.call(example) post_condition_hooks.each &.call(example)
@group.try &.call_post_condition(example) @group.try &.call_post_condition(example)
@ -79,7 +79,7 @@ module Spectator
# This group will be assigned to the parent *group* if it is provided. # This group will be assigned to the parent *group* if it is provided.
# A set of *metadata* can be used for filtering and modifying example behavior. # A set of *metadata* can be used for filtering and modifying example behavior.
def initialize(@name : Label = nil, @location : Location? = nil, def initialize(@name : Label = nil, @location : Location? = nil,
@group : ExampleGroup? = nil, @metadata : Metadata? = nil) @group : ExampleGroup? = nil, @metadata : Metadata = Metadata.new)
# Ensure group is linked. # Ensure group is linked.
group << self if group group << self if group
end end
@ -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
@ -112,15 +112,11 @@ module Spectator
# Constructs the full name or description of the example group. # Constructs the full name or description of the example group.
# This prepends names of groups this group is part of. # This prepends names of groups this group is part of.
def to_s(io : IO, *, nested = false) : Nil def to_s(io)
unless parent = @group
# Display special string when called directly.
io << "<root>" unless nested
return
end
# Prefix with group's full name if the node belongs to a group. # Prefix with group's full name if the node belongs to a group.
parent.to_s(io, nested: true) return unless parent = @group
parent.to_s(io)
name = @name name = @name
# Add padding between the node names # Add padding between the node names
@ -130,7 +126,7 @@ module Spectator
(parent.name?.is_a?(Symbol) && name.is_a?(String) && (parent.name?.is_a?(Symbol) && name.is_a?(String) &&
(name.starts_with?('#') || name.starts_with?('.'))) (name.starts_with?('#') || name.starts_with?('.')))
super(io) super
end end
# Adds the specified *node* to the group. # Adds the specified *node* to the group.

View file

@ -28,7 +28,7 @@ module Spectator
# Creates the builder. # Creates the builder.
# Initially, the builder will have no children and no hooks. # Initially, the builder will have no children and no hooks.
# The *name*, *location*, and *metadata* will be applied to the `ExampleGroup` produced by `#build`. # The *name*, *location*, and *metadata* will be applied to the `ExampleGroup` produced by `#build`.
def initialize(@name : Label = nil, @location : Location? = nil, @metadata : Metadata? = nil) def initialize(@name : Label = nil, @location : Location? = nil, @metadata : Metadata = Metadata.new)
end end
# Constructs an example group with previously defined attributes, children, and hooks. # Constructs an example group with previously defined attributes, children, and hooks.

View file

@ -42,7 +42,7 @@ module Spectator
# Produces the string representation of the hook. # Produces the string representation of the hook.
# Includes the location and label if they're not nil. # Includes the location and label if they're not nil.
def to_s(io : IO) : Nil def to_s(io)
io << "example group hook" io << "example group hook"
if (label = @label) if (label = @label)

View file

@ -18,7 +18,7 @@ module Spectator
# This group will be assigned to the parent *group* if it is provided. # This group will be assigned to the parent *group* if it is provided.
# A set of *metadata* can be used for filtering and modifying example behavior. # A set of *metadata* can be used for filtering and modifying example behavior.
def initialize(@item : T, name : Label = nil, location : Location? = nil, def initialize(@item : T, name : Label = nil, location : Location? = nil,
group : ExampleGroup? = nil, metadata : Metadata? = nil) group : ExampleGroup? = nil, metadata : Metadata = Metadata.new)
super(name, location, group, metadata) super(name, location, group, metadata)
end end
end end

View file

@ -37,7 +37,7 @@ module Spectator
# Produces the string representation of the hook. # Produces the string representation of the hook.
# Includes the location and label if they're not nil. # Includes the location and label if they're not nil.
def to_s(io : IO) : Nil def to_s(io)
io << "example hook" io << "example hook"
if (label = @label) if (label = @label)

View file

@ -39,7 +39,7 @@ module Spectator
# Produces the string representation of the hook. # Produces the string representation of the hook.
# Includes the location and label if they're not nil. # Includes the location and label if they're not nil.
def to_s(io : IO) : Nil def to_s(io)
io << "example hook" io << "example hook"
if (label = @label) if (label = @label)

View file

@ -114,21 +114,6 @@ module Spectator
report(match_data, message) report(match_data, message)
end end
# Asserts that some criteria defined by the matcher is satisfied.
# Allows a custom message to be used.
# Returns the expected value cast as the expected type, if the matcher is satisfied.
def to(matcher : Matchers::TypeMatcher(U), message = nil) forall U
match_data = matcher.match(@expression)
value = @expression.value
if report(match_data, message)
return value if value.is_a?(U)
raise "Spectator bug: expected value should have cast to #{U}"
else
raise TypeCastError.new("#{@expression.label} is expected to be a #{U}, but was actually #{value.class}")
end
end
# Asserts that a method is not called before the example completes. # Asserts that a method is not called before the example completes.
@[AlwaysInline] @[AlwaysInline]
def to_not(stub : Stub, message = nil) : Nil def to_not(stub : Stub, message = nil) : Nil
@ -151,36 +136,6 @@ module Spectator
report(match_data, message) report(match_data, message)
end end
# Asserts that some criteria defined by the matcher is not satisfied.
# Allows a custom message to be used.
# Returns the expected value cast without the unexpected type, if the matcher is satisfied.
def to_not(matcher : Matchers::TypeMatcher(U), message = nil) forall U
match_data = matcher.negated_match(@expression)
value = @expression.value
if report(match_data, message)
return value unless value.is_a?(U)
raise "Spectator bug: expected value should not be #{U}"
else
raise TypeCastError.new("#{@expression.label} is not expected to be a #{U}, but was actually #{value.class}")
end
end
# Asserts that some criteria defined by the matcher is not satisfied.
# Allows a custom message to be used.
# Returns the expected value cast as a non-nillable type, if the matcher is satisfied.
def to_not(matcher : Matchers::NilMatcher, message = nil)
match_data = matcher.negated_match(@expression)
if report(match_data, message)
value = @expression.value
return value unless value.nil?
raise "Spectator bug: expected value should not be nil"
else
raise NilAssertionError.new("#{@expression.label} is not expected to be nil.")
end
end
# :ditto: # :ditto:
@[AlwaysInline] @[AlwaysInline]
def not_to(matcher, message = nil) : Nil def not_to(matcher, message = nil) : Nil
@ -205,15 +160,9 @@ module Spectator
stubbable._spectator_define_stub(unconstrained_stub) stubbable._spectator_define_stub(unconstrained_stub)
end end
# Apply the stub that is expected to be called.
stubbable._spectator_define_stub(stub) stubbable._spectator_define_stub(stub)
# Check if the stub was invoked after the test completes.
matcher = Matchers::ReceiveMatcher.new(stub) matcher = Matchers::ReceiveMatcher.new(stub)
Harness.current.defer { to(matcher, message) } to_eventually(matcher, message)
# Prevent leaking stubs between tests.
Harness.current.cleanup { stubbable._spectator_remove_stub(stub) }
end end
# Asserts that some criteria defined by the matcher is eventually satisfied. # Asserts that some criteria defined by the matcher is eventually satisfied.
@ -241,15 +190,9 @@ module Spectator
stubbable._spectator_define_stub(unconstrained_stub) stubbable._spectator_define_stub(unconstrained_stub)
end end
# Apply the stub that could be called in case it is.
stubbable._spectator_define_stub(stub) stubbable._spectator_define_stub(stub)
# Check if the stub was invoked after the test completes.
matcher = Matchers::ReceiveMatcher.new(stub) matcher = Matchers::ReceiveMatcher.new(stub)
Harness.current.defer { to_not(matcher, message) } to_never(matcher, message)
# Prevent leaking stubs between tests.
Harness.current.cleanup { stubbable._spectator_remove_stub(stub) }
end end
# :ditto: # :ditto:

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
@ -55,7 +55,7 @@ module Spectator
end end
# One-word description of the result. # One-word description of the result.
def to_s(io : IO) : Nil def to_s(io)
io << "fail" io << "fail"
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

@ -16,7 +16,7 @@ module Spectator::Formatting::Components
end end
# Writes the comment to the output. # Writes the comment to the output.
def to_s(io : IO) : Nil def to_s(io)
io << "# " << @content io << "# " << @content
end end
end end

View file

@ -7,38 +7,36 @@ module Spectator::Formatting::Components
# Displays information about an error result. # Displays information about an error result.
struct ErrorResultBlock < ResultBlock struct ErrorResultBlock < ResultBlock
# Creates the component. # Creates the component.
def initialize(example : Example, index : Int32, @error : Exception, subindex = 0) def initialize(example : Example, index : Int32, @result : ErrorResult, subindex = 0)
super(example, index, subindex) super(example, index, subindex)
end end
# Content displayed on the second line of the block after the label. # Content displayed on the second line of the block after the label.
private def subtitle private def subtitle
@error.message.try(&.each_line.first) @result.error.message.try(&.each_line.first)
end end
# Prefix for the second line of the block. # Prefix for the second line of the block.
private def subtitle_label private def subtitle_label
case @error "Error: ".colorize(:red)
when ExampleFailed then "Failure: "
else "Error: "
end.colorize(:red)
end end
# Display error information. # Display error information.
private def content(io) private def content(io)
# Fetch the error and message. # Fetch the error and message.
lines = @error.message.try(&.lines) error = @result.error
lines = error.message.try(&.lines)
# Write the error and message if available. # Write the error and message if available.
case case
when lines.nil? then write_error_class(io) when lines.nil? then write_error_class(io, error)
when lines.size == 1 then write_error_message(io, lines.first) when lines.size == 1 then write_error_message(io, error, lines.first)
when lines.size > 1 then write_multiline_error_message(io, lines) when lines.size > 1 then write_multiline_error_message(io, error, lines)
else write_error_class(io) else write_error_class(io, error)
end end
# Display the backtrace if it's available. # Display the backtrace if it's available.
if backtrace = @error.backtrace? if backtrace = error.backtrace?
indent { write_backtrace(io, backtrace) } indent { write_backtrace(io, backtrace) }
end end
@ -46,24 +44,24 @@ module Spectator::Formatting::Components
end end
# Display just the error type. # Display just the error type.
private def write_error_class(io) private def write_error_class(io, error)
line(io) do line(io) do
io << @error.class.colorize(:red) io << error.class.colorize(:red)
end end
end end
# Display the error type and first line of the message. # Display the error type and first line of the message.
private def write_error_message(io, message) private def write_error_message(io, error, message)
line(io) do line(io) do
io << "#{@error.class}: ".colorize(:red) io << "#{error.class}: ".colorize(:red)
io << message io << message
end end
end end
# Display the error type and its multi-line message. # Display the error type and its multi-line message.
private def write_multiline_error_message(io, lines) private def write_multiline_error_message(io, error, lines)
# Use the normal formatting for the first line. # Use the normal formatting for the first line.
write_error_message(io, lines.first) write_error_message(io, error, lines.first)
# Display additional lines after the first. # Display additional lines after the first.
lines.skip(1).each do |entry| lines.skip(1).each do |entry|

View file

@ -9,7 +9,7 @@ module Spectator::Formatting::Components
end end
# Produces output for running the previously specified example. # Produces output for running the previously specified example.
def to_s(io : IO) : Nil def to_s(io)
io << "crystal spec " io << "crystal spec "
# Use location for argument if it's available, since it's simpler. # Use location for argument if it's available, since it's simpler.

View file

@ -10,7 +10,7 @@ module Spectator::Formatting::Components
end end
# Produces the list of commands to run failed examples. # Produces the list of commands to run failed examples.
def to_s(io : IO) : Nil def to_s(io)
io.puts "Failed examples:" io.puts "Failed examples:"
io.puts io.puts
@failures.each do |failure| @failures.each do |failure|

View file

@ -9,7 +9,7 @@ module Spectator::Formatting::Components
end end
# Produces the output containing the profiling information. # Produces the output containing the profiling information.
def to_s(io : IO) : Nil def to_s(io)
io << "Top " io << "Top "
io << @profile.size io << @profile.size
io << " slowest examples (" io << " slowest examples ("

View file

@ -41,7 +41,7 @@ module Spectator::Formatting::Components
private abstract def content(io) private abstract def content(io)
# Writes the component's output to the specified stream. # Writes the component's output to the specified stream.
def to_s(io : IO) : Nil def to_s(io)
title_line(io) title_line(io)
# Ident over to align with the spacing used by the index. # Ident over to align with the spacing used by the index.
indent(index_digit_count + 2) do indent(index_digit_count + 2) do

View file

@ -15,7 +15,7 @@ module Spectator::Formatting::Components
# #:##:## # #:##:##
# # days #:##:## # # days #:##:##
# ``` # ```
def to_s(io : IO) : Nil def to_s(io)
millis = @span.total_milliseconds millis = @span.total_milliseconds
return format_micro(io, millis * 1000) if millis < 1 return format_micro(io, millis * 1000) if millis < 1

View file

@ -11,7 +11,7 @@ module Spectator::Formatting::Components
end end
# Displays the stats. # Displays the stats.
def to_s(io : IO) : Nil def to_s(io)
runtime(io) runtime(io)
totals(io) totals(io)
if seed = @report.random_seed? if seed = @report.random_seed?

View file

@ -10,7 +10,7 @@ module Spectator::Formatting::Components
end end
# Produces the output containing the profiling information. # Produces the output containing the profiling information.
def to_s(io : IO) : Nil def to_s(io)
io << "# Top " io << "# Top "
io << @profile.size io << @profile.size
io << " slowest examples (" io << " slowest examples ("

View file

@ -31,7 +31,7 @@ module Spectator::Formatting::Components
end end
# Writes the counts to the output. # Writes the counts to the output.
def to_s(io : IO) : Nil def to_s(io)
io << @examples << " examples, " << @failures << " failures" io << @examples << " examples, " << @failures << " failures"
if @errors > 0 if @errors > 0

View file

@ -63,22 +63,15 @@ module Spectator::Formatting
# Displays one or more blocks for a failed example. # Displays one or more blocks for a failed example.
# Each block is a failed expectation or error raised in the example. # Each block is a failed expectation or error raised in the example.
private def dump_failed_example(example, index) private def dump_failed_example(example, index)
# Retrieve the ultimate reason for failing. result = example.result.as?(ErrorResult)
error = example.result.as?(FailResult).try(&.error)
# Prevent displaying duplicated output from expectation.
# Display `ExampleFailed` but not `ExpectationFailed`.
error = nil if error.responds_to?(:expectation)
# Gather all failed expectations.
failed_expectations = example.result.expectations.select(&.failed?) failed_expectations = example.result.expectations.select(&.failed?)
block_count = failed_expectations.size block_count = failed_expectations.size
block_count += 1 if error # Add an extra block for final error if it's significant. block_count += 1 if result
# Don't use sub-index if there was only one problem. # Don't use sub-index if there was only one problem.
if block_count == 1 if block_count == 1
if error if result
io.puts Components::ErrorResultBlock.new(example, index, error) io.puts Components::ErrorResultBlock.new(example, index, result)
else else
io.puts Components::FailResultBlock.new(example, index, failed_expectations.first) io.puts Components::FailResultBlock.new(example, index, failed_expectations.first)
end end
@ -86,7 +79,7 @@ module Spectator::Formatting
failed_expectations.each_with_index(1) do |expectation, subindex| failed_expectations.each_with_index(1) do |expectation, subindex|
io.puts Components::FailResultBlock.new(example, index, expectation, subindex) io.puts Components::FailResultBlock.new(example, index, expectation, subindex)
end end
io.puts Components::ErrorResultBlock.new(example, index, error, block_count) if error io.puts Components::ErrorResultBlock.new(example, index, result, block_count) if result
end end
end end
end end

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

@ -15,7 +15,7 @@ module Spectator
# The *collection* is the set of items to create sub-nodes for. # The *collection* is the set of items to create sub-nodes for.
# The *iterators* is a list of optional names given to items in the collection. # The *iterators* is a list of optional names given to items in the collection.
def initialize(@collection : Enumerable(T), name : String? = nil, @iterators : Array(String) = [] of String, def initialize(@collection : Enumerable(T), name : String? = nil, @iterators : Array(String) = [] of String,
location : Location? = nil, metadata : Metadata? = nil) location : Location? = nil, metadata : Metadata = Metadata.new)
super(name, location, metadata) super(name, location, metadata)
end end

View file

@ -59,7 +59,7 @@ module Spectator
# ```text # ```text
# FILE:LINE # FILE:LINE
# ``` # ```
def to_s(io : IO) : Nil def to_s(io)
io << path << ':' << line io << path << ':' << line
end end
end end

View file

@ -15,7 +15,7 @@ module Spectator::Matchers
extend self extend self
# Text displayed when a method is undefined. # Text displayed when a method is undefined.
def inspect(io : IO) : Nil def inspect(io)
io << "<Method undefined>" io << "<Method undefined>"
end end
end end

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

@ -1,4 +1,3 @@
require "../value"
require "./match_data" require "./match_data"
module Spectator::Matchers module Spectator::Matchers
@ -23,19 +22,6 @@ module Spectator::Matchers
# A successful match with `#match` should normally fail for this method, and vice-versa. # A successful match with `#match` should normally fail for this method, and vice-versa.
abstract def negated_match(actual : Expression(T)) : MatchData forall T abstract def negated_match(actual : Expression(T)) : MatchData forall T
# Compares a matcher against a value.
# Enables composable matchers.
def ===(actual : Expression(T)) : Bool
match(actual).matched?
end
# Compares a matcher against a value.
# Enables composable matchers.
def ===(other) : Bool
expression = Value.new(other)
match(expression).matched?
end
private def match_data_description(actual : Expression(T)) : String forall T private def match_data_description(actual : Expression(T)) : String forall T
match_data_description(actual.label) match_data_description(actual.label)
end end

View file

@ -29,26 +29,7 @@ module Spectator::Matchers
# Checks whether the matcher is satisfied with the expression given to it. # Checks whether the matcher is satisfied with the expression given to it.
private def match?(actual : Expression(T)) : Bool forall T private def match?(actual : Expression(T)) : Bool forall T
actual_value = actual.value expected.value.includes?(actual.value)
expected_value = expected.value
if expected_value.is_a?(Range) && actual_value.is_a?(Comparable)
return match_impl?(expected_value, actual_value)
end
return false unless actual_value.is_a?(Comparable(typeof(expected_value.begin)))
expected_value.includes?(actual_value)
end
private def match_impl?(expected_value : Range(B, E), actual_value : Comparable(B)) : Bool forall B, E
expected_value.includes?(actual_value)
end
private def match_impl?(expected_value : Range(B, E), actual_value : T) : Bool forall B, E, T
return false unless actual_value.is_a?(B) || actual_value.is_a?(Comparable(B))
expected_value.includes?(actual_value)
end
private def match_impl?(expected_value : Range(Number, Number), actual_value : Number) : Bool
expected_value.includes?(actual_value)
end end
# Message displayed when the matcher isn't satisfied. # Message displayed when the matcher isn't satisfied.

View file

@ -81,7 +81,7 @@ module Spectator::Matchers
# Short text about the matcher's purpose. # Short text about the matcher's purpose.
def description : String def description : String
"received #{@stub.message} #{humanize_count}" "received #{@stub} #{humanize_count}"
end end
# Actually performs the test against the expression (value or block). # Actually performs the test against the expression (value or block).
@ -89,10 +89,10 @@ module Spectator::Matchers
stubbed = actual.value stubbed = actual.value
calls = relevant_calls(stubbed) calls = relevant_calls(stubbed)
if @count.includes?(calls.size) if @count.includes?(calls.size)
SuccessfulMatchData.new("#{actual.label} received #{@stub.message} #{humanize_count}") SuccessfulMatchData.new("#{actual.label} received #{@stub} #{humanize_count}")
else else
FailedMatchData.new("#{actual.label} received #{@stub.message} #{humanize_count}", FailedMatchData.new("#{actual.label} received #{@stub} #{humanize_count}",
"#{actual.label} did not receive #{@stub.message}", values(actual).to_a) "#{actual.label} did not receive #{@stub}", values(actual).to_a)
end end
end end
@ -106,9 +106,9 @@ module Spectator::Matchers
stubbed = actual.value stubbed = actual.value
calls = relevant_calls(stubbed) calls = relevant_calls(stubbed)
if @count.includes?(calls.size) if @count.includes?(calls.size)
FailedMatchData.new("#{actual.label} did not receive #{@stub.message}", "#{actual.label} received #{@stub.message}", negated_values(actual).to_a) FailedMatchData.new("#{actual.label} did not receive #{@stub}", "#{actual.label} received #{@stub}", negated_values(actual).to_a)
else else
SuccessfulMatchData.new("#{actual.label} did not receive #{@stub.message} #{humanize_count}") SuccessfulMatchData.new("#{actual.label} did not receive #{@stub} #{humanize_count}")
end end
end end
@ -120,7 +120,7 @@ module Spectator::Matchers
# Additional information about the match failure. # Additional information about the match failure.
private def values(actual : Expression(T)) forall T private def values(actual : Expression(T)) forall T
{ {
expected: @stub.message, expected: @stub.to_s,
actual: method_call_list(actual.value), actual: method_call_list(actual.value),
} }
end end
@ -128,7 +128,7 @@ module Spectator::Matchers
# Additional information about the match failure when negated. # Additional information about the match failure when negated.
private def negated_values(actual : Expression(T)) forall T private def negated_values(actual : Expression(T)) forall T
{ {
expected: "Not #{@stub.message}", expected: "Not #{@stub}",
actual: method_call_list(actual.value), actual: method_call_list(actual.value),
} }
end end

View file

@ -1,5 +1,3 @@
require "../expression"
require "../value"
require "./standard_matcher" require "./standard_matcher"
module Spectator::Matchers module Spectator::Matchers
@ -24,7 +22,7 @@ module Spectator::Matchers
# Creates the value matcher. # Creates the value matcher.
# The expected value is stored for later use. # The expected value is stored for later use.
def initialize(@expected : ::Spectator::Value(ExpectedType)) def initialize(@expected : Value(ExpectedType))
end end
# Additional information about the match failure. # Additional information about the match failure.

View file

@ -1,61 +1,5 @@
module Spectator module Spectator
# Untyped arguments to a method call (message). # Untyped arguments to a method call (message).
abstract class AbstractArguments abstract class AbstractArguments
# Use the string representation to avoid over complicating debug output.
def inspect(io : IO) : Nil
to_s(io)
end
# Utility method for comparing two tuples considering special types.
private def compare_tuples(a : Tuple | Array, b : Tuple | Array)
return false if a.size != b.size
a.zip(b) do |a_value, b_value|
return false unless compare_values(a_value, b_value)
end
true
end
# Utility method for comparing two tuples considering special types.
# Supports nilable tuples (ideal for splats).
private def compare_tuples(a : Tuple? | Array?, b : Tuple? | Array?)
return false if a.nil? ^ b.nil?
compare_tuples(a.not_nil!, b.not_nil!)
end
# Utility method for comparing two named tuples ignoring order.
private def compare_named_tuples(a : NamedTuple | Hash, b : NamedTuple | Hash)
a.each do |k, v1|
v2 = b.fetch(k) { return false }
return false unless compare_values(v1, v2)
end
true
end
# Utility method for comparing two arguments considering special types.
# Some types used for case-equality don't work well with unexpected right-hand types.
# This can happen when the right side is a massive union of types.
private def compare_values(a, b)
case a
when Proc
# Using procs as argument matchers isn't supported currently.
# Compare directly instead.
a == b
when Range
# Ranges can only be matched against if their right side is comparable.
# Ensure the right side is comparable, otherwise compare directly.
return a === b if b.is_a?(Comparable(typeof(b)))
a == b
when Tuple, Array
return compare_tuples(a, b) if b.is_a?(Tuple) || b.is_a?(Array)
a === b
when NamedTuple, Hash
return compare_named_tuples(a, b) if b.is_a?(NamedTuple) || b.is_a?(Hash)
a === b
else
a === b
end
end
end end
end end

View file

@ -1,4 +1,3 @@
require "../harness"
require "./stub" require "./stub"
require "./stubbable" require "./stubbable"
require "./stubbed_type" require "./stubbed_type"
@ -22,7 +21,6 @@ module Spectator
# Applies a stub to the targeted stubbable object. # Applies a stub to the targeted stubbable object.
def to(stub : Stub) : Nil def to(stub : Stub) : Nil
@target._spectator_define_stub(stub) @target._spectator_define_stub(stub)
Harness.current?.try &.cleanup { @target._spectator_remove_stub(stub) }
end end
end end
end end

View file

@ -4,19 +4,22 @@ module Spectator
# Arguments used in a method call. # Arguments used in a method call.
# #
# Can also be used to match arguments. # Can also be used to match arguments.
# *Args* must be a `Tuple` representing the standard arguments. # *T* must be a `Tuple` type representing the positional arguments.
# *KWArgs* must be a `NamedTuple` type representing extra keyword arguments. # *NT* must be a `NamedTuple` type representing the keyword arguments.
class Arguments(Args, KWArgs) < AbstractArguments class Arguments(T, NT) < AbstractArguments
# Positional arguments. # Positional arguments.
getter args : Args getter args : T
# Keyword arguments. # Keyword arguments.
getter kwargs : KWArgs getter kwargs : NT
# Creates arguments used in a method call. # Creates arguments used in a method call.
def initialize(@args : Args, @kwargs : KWArgs) def initialize(@args : T, @kwargs : NT)
{% raise "Positional arguments (generic type Args) must be a Tuple" unless Args <= Tuple %} end
{% raise "Keyword arguments (generic type KWArgs) must be a NamedTuple" unless KWArgs <= NamedTuple %}
# Constructs an instance from literal arguments.
def self.capture(*args, **kwargs) : AbstractArguments
new(args, kwargs).as(AbstractArguments)
end end
# Instance of empty arguments. # Instance of empty arguments.
@ -27,14 +30,9 @@ module Spectator
nil.as(AbstractArguments?) nil.as(AbstractArguments?)
end end
# Friendlier constructor for capturing arguments.
def self.capture(*args, **kwargs)
new(args, kwargs)
end
# Returns the positional argument at the specified index. # Returns the positional argument at the specified index.
def [](index : Int) def [](index : Int)
args[index] @args[index]
end end
# Returns the specified named argument. # Returns the specified named argument.
@ -42,16 +40,6 @@ module Spectator
@kwargs[arg] @kwargs[arg]
end end
# Returns all arguments and splatted arguments as a tuple.
def positional : Tuple
args
end
# Returns all named positional and keyword arguments as a named tuple.
def named : NamedTuple
kwargs
end
# Constructs a string representation of the arguments. # Constructs a string representation of the arguments.
def to_s(io : IO) : Nil def to_s(io : IO) : Nil
return io << "(no args)" if args.empty? && kwargs.empty? return io << "(no args)" if args.empty? && kwargs.empty?
@ -65,46 +53,39 @@ module Spectator
end end
# Add the keyword arguments. # Add the keyword arguments.
kwargs.each_with_index(args.size) do |key, value, i| size = args.size + kwargs.size
io << ", " if i > 0 kwargs.each_with_index(args.size) do |k, v, i|
io << key << ": " io << ", " if 0 < i < size
value.inspect(io) io << k << ": "
v.inspect(io)
end end
io << ')' io << ')'
end end
# Checks if this set of arguments and another are equal. # Checks if this set of arguments and another are equal.
def ==(other : AbstractArguments) def ==(other : Arguments)
positional == other.positional && kwargs == other.kwargs args == other.args && kwargs == other.kwargs
end end
# Checks if another set of arguments matches this set of arguments. # Checks if another set of arguments matches this set of arguments.
def ===(other : Arguments) def ===(other : Arguments)
compare_tuples(positional, other.positional) && compare_named_tuples(kwargs, other.kwargs) args === other.args && named_tuples_match?(kwargs, other.kwargs)
end end
# :ditto: # Checks if two named tuples match.
def ===(other : FormalArguments) #
return false unless compare_named_tuples(kwargs, other.named) # 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
i = 0 a.each do |k, v|
other.args.each do |k, v2| return false unless b.has_key?(k)
break if i >= positional.size return false unless v === b[k]
next if kwargs.has_key?(k) # Covered by named arguments.
v1 = positional[i]
i += 1
return false unless compare_values(v1, v2)
end end
other.splat.try &.each do |v2| true
v1 = positional.fetch(i) { return false }
i += 1
return false unless compare_values(v1, v2)
end
i == positional.size
end end
end end
end end

View file

@ -61,7 +61,7 @@ module Spectator
end end
{% end %} {% end %}
{{block.body if block}} {% if block %}{{block.body}}{% end %}
end end
end end
@ -95,38 +95,16 @@ module Spectator
false false
end end
# Simplified string representation of a double.
# Avoids displaying nested content and bloating method instantiation.
def to_s(io : IO) : Nil
io << "#<" + {{@type.name(generic_args: false).stringify}} + " "
io << _spectator_stubbed_name << '>'
end
# :ditto:
def inspect(io : IO) : Nil
io << "#<" + {{@type.name(generic_args: false).stringify}} + " "
io << _spectator_stubbed_name
io << ":0x"
object_id.to_s(io, 16)
io << '>'
end
# Defines a stub to change the behavior of a method in this double. # Defines a stub to change the behavior of a method in this double.
# #
# NOTE: Defining a stub for a method not defined in the double's type has no effect. # NOTE: Defining a stub for a method not defined in the double's type has no effect.
protected def _spectator_define_stub(stub : Stub) : Nil protected def _spectator_define_stub(stub : Stub) : Nil
Log.debug { "Defined stub for #{inspect} #{stub}" } Log.debug { "Defined stub for #{_spectator_stubbed_name} #{stub}" }
@stubs.unshift(stub) @stubs.unshift(stub)
end end
protected def _spectator_remove_stub(stub : Stub) : Nil
Log.debug { "Removing stub #{stub} from #{inspect}" }
@stubs.delete(stub)
end
protected def _spectator_clear_stubs : Nil protected def _spectator_clear_stubs : Nil
Log.debug { "Clearing stubs for #{inspect}" } Log.debug { "Clearing stubs for #{_spectator_stubbed_name}" }
@stubs.clear @stubs.clear
end end
@ -156,17 +134,17 @@ module Spectator
# Returns the double's name formatted for user output. # Returns the double's name formatted for user output.
private def _spectator_stubbed_name : String private def _spectator_stubbed_name : String
{% if anno = @type.annotation(StubbedName) %} {% if anno = @type.annotation(StubbedName) %}
{{(anno[0] || :Anonymous.id).stringify}} "#<Double " + {{(anno[0] || :Anonymous.id).stringify}} + ">"
{% else %} {% else %}
"Anonymous" "#<Double Anonymous>"
{% end %} {% end %}
end end
private def self._spectator_stubbed_name : String private def self._spectator_stubbed_name : String
{% if anno = @type.annotation(StubbedName) %} {% if anno = @type.annotation(StubbedName) %}
{{(anno[0] || :Anonymous.id).stringify}} "#<Class Double " + {{(anno[0] || :Anonymous.id).stringify}} + ">"
{% else %} {% else %}
"Anonymous" "#<Class Double Anonymous>"
{% end %} {% end %}
end end
@ -186,7 +164,7 @@ module Spectator
"Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)."
end end
raise UnexpectedMessage.new("#{inspect} received unexpected message #{call}") raise UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}")
end end
private def _spectator_abstract_stub_fallback(call : MethodCall, type) private def _spectator_abstract_stub_fallback(call : MethodCall, type)
@ -201,13 +179,12 @@ module Spectator
# Handle all methods but only respond to configured messages. # Handle all methods but only respond to configured messages.
# Raises an `UnexpectedMessage` error for non-configures messages. # Raises an `UnexpectedMessage` error for non-configures messages.
macro method_missing(call) macro method_missing(call)
args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{{call.named_args.splat if call.named_args}}) Log.trace { "Got undefined method `{{call.name}}({{*call.args}}{% if call.named_args %}{% unless call.args.empty? %}, {% end %}{{*call.named_args}}{% end %}){% if call.block %} { ... }{% end %}`" }
args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{% if call.named_args %}{{*call.named_args}}{% end %})
call = ::Spectator::MethodCall.new({{call.name.symbolize}}, args) call = ::Spectator::MethodCall.new({{call.name.symbolize}}, args)
_spectator_record_call(call) _spectator_record_call(call)
Log.trace { "#{inspect} got undefined method `#{call}{% if call.block %} { ... }{% end %}`" } raise ::Spectator::UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}")
raise ::Spectator::UnexpectedMessage.new("#{inspect} received unexpected message #{call}")
nil # Necessary for compiler to infer return type as nil. Avoids runtime "can't execute ... `x` has no type errors". nil # Necessary for compiler to infer return type as nil. Avoids runtime "can't execute ... `x` has no type errors".
end end
end end

View file

@ -20,12 +20,6 @@ module Spectator
def initialize(method : Symbol, @exception : Exception, constraint : AbstractArguments? = nil, location : Location? = nil) def initialize(method : Symbol, @exception : Exception, constraint : AbstractArguments? = nil, location : Location? = nil)
super(method, constraint, location) super(method, constraint, location)
end end
# String representation of the stub, formatted as a method call.
def to_s(io : IO) : Nil
super
io << " # raises " << @exception
end
end end
module StubModifiers module StubModifiers

View file

@ -1,133 +0,0 @@
require "./abstract_arguments"
module Spectator
# Arguments passed into a method.
#
# *Args* must be a `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 FormalArguments(Args, Splat, DoubleSplat) < AbstractArguments
# Positional arguments.
getter args : Args
# Additional positional arguments.
getter splat : Splat
# Keyword arguments.
getter kwargs : DoubleSplat
# Name of the splat argument, if used.
getter splat_name : Symbol?
# Creates arguments used in a method call.
def initialize(@args : Args, @splat_name : Symbol?, @splat : Splat, @kwargs : DoubleSplat)
{% raise "Positional arguments (generic type Args) must be a NamedTuple" unless Args <= NamedTuple %}
{% raise "Splat arguments (generic type Splat) must be a Tuple" unless Splat <= Tuple || Splat <= Nil %}
{% raise "Keyword arguments (generic type DoubleSplat) must be a NamedTuple" unless DoubleSplat <= NamedTuple %}
end
# Creates arguments used in a method call.
def self.new(args : Args, kwargs : DoubleSplat)
new(args, nil, nil, kwargs)
end
# Captures arguments passed to a call.
def self.build(args = NamedTuple.new, kwargs = NamedTuple.new)
new(args, nil, nil, kwargs)
end
# :ditto:
def self.build(args : NamedTuple, splat_name : Symbol, splat : Tuple, kwargs = NamedTuple.new)
new(args, splat_name, splat, kwargs)
end
# Instance of empty arguments.
class_getter none : AbstractArguments = build
# Returns the positional argument at the specified index.
def [](index : Int)
positional[index]
end
# Returns the specified named argument.
def [](arg : Symbol)
return @args[arg] if @args.has_key?(arg)
@kwargs[arg]
end
# Returns all arguments and splatted arguments as a tuple.
def positional : Tuple
if (splat = @splat)
args.values + splat
else
args.values
end
end
# Returns all named positional and keyword arguments as a named tuple.
def named : NamedTuple
args.merge(kwargs)
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?
io << '('
# Add the positional arguments.
{% if Args < NamedTuple %}
# Include argument names.
args.each_with_index do |name, value, i|
io << ", " if i > 0
io << name << ": "
value.inspect(io)
end
{% else %}
args.each_with_index do |arg, i|
io << ", " if i > 0
arg.inspect(io)
end
{% end %}
# Add the splat arguments.
if (splat = @splat) && !splat.empty?
io << ", " unless args.empty?
if splat_name = !args.empty? && @splat_name
io << '*' << splat_name << ": {"
end
splat.each_with_index do |arg, i|
io << ", " if i > 0
arg.inspect(io)
end
io << '}' if splat_name
end
# Add the keyword arguments.
offset = args.size
offset += splat.size if (splat = @splat)
kwargs.each_with_index(offset) do |key, value, i|
io << ", " if i > 0
io << key << ": "
value.inspect(io)
end
io << ')'
end
# Checks if this set of arguments and another are equal.
def ==(other : AbstractArguments)
positional == other.positional && kwargs == other.kwargs
end
# Checks if another set of arguments matches this set of arguments.
def ===(other : Arguments)
compare_tuples(positional, other.positional) && compare_named_tuples(kwargs, other.kwargs)
end
# :ditto:
def ===(other : FormalArguments)
compare_named_tuples(args, other.args) && compare_tuples(splat, other.splat) && compare_named_tuples(kwargs, other.kwargs)
end
end
end

View file

@ -26,24 +26,15 @@ module Spectator
super(_spectator_double_stubs + message_stubs) super(_spectator_double_stubs + message_stubs)
end end
# Defines a stub to change the behavior of a method in this double.
#
# NOTE: Defining a stub for a method not defined in the double's type raises an error.
protected def _spectator_define_stub(stub : Stub) : Nil
return super if Messages.types.has_key?(stub.method)
raise "Can't define stub #{stub} on lazy double because it wasn't initially defined."
end
# Returns the double's name formatted for user output. # Returns the double's name formatted for user output.
private def _spectator_stubbed_name : String private def _spectator_stubbed_name : String
@name || "Anonymous" "#<LazyDouble #{@name || "Anonymous"}>"
end end
private def _spectator_stub_fallback(call : MethodCall, &) private def _spectator_stub_fallback(call : MethodCall, &)
if _spectator_stub_for_method?(call.method) if _spectator_stub_for_method?(call.method)
Log.info { "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." } Log.info { "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." }
raise UnexpectedMessage.new("#{inspect} received unexpected message #{call}") raise UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}")
else else
Log.trace { "Fallback for #{call} - call original" } Log.trace { "Fallback for #{call} - call original" }
yield yield
@ -52,13 +43,13 @@ module Spectator
# Handles all messages. # Handles all messages.
macro method_missing(call) macro method_missing(call)
Log.trace { "Got undefined method `{{call.name}}({{*call.args}}{% if call.named_args %}{% unless call.args.empty? %}, {% end %}{{*call.named_args}}{% end %}){% if call.block %} { ... }{% end %}`" }
# Capture information about the call. # Capture information about the call.
%args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{{call.named_args.splat if call.named_args}}) %args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{% if call.named_args %}{{*call.named_args}}{% end %})
%call = ::Spectator::MethodCall.new({{call.name.symbolize}}, %args) %call = ::Spectator::MethodCall.new({{call.name.symbolize}}, %args)
_spectator_record_call(%call) _spectator_record_call(%call)
Log.trace { "#{inspect} got undefined method `#{%call}{% if call.block %} { ... }{% end %}`" }
# Attempt to find a stub that satisfies the method call and arguments. # Attempt to find a stub that satisfies the method call and arguments.
if %stub = _spectator_find_stub(%call) if %stub = _spectator_find_stub(%call)
# Cast the stub or return value to the expected type. # Cast the stub or return value to the expected type.

View file

@ -1,6 +1,5 @@
require "./abstract_arguments" require "./abstract_arguments"
require "./arguments" require "./arguments"
require "./formal_arguments"
module Spectator module Spectator
# Stores information about a call to a method. # Stores information about a call to a method.
@ -17,26 +16,13 @@ module Spectator
# Creates a method call by splatting its arguments. # Creates a method call by splatting its arguments.
def self.capture(method : Symbol, *args, **kwargs) def self.capture(method : Symbol, *args, **kwargs)
arguments = Arguments.capture(*args, **kwargs).as(AbstractArguments) arguments = Arguments.new(args, kwargs).as(AbstractArguments)
new(method, arguments)
end
# Creates a method call from within a method.
# Takes the same arguments as `FormalArguments.build` but with the method name first.
def self.build(method : Symbol, *args, **kwargs)
arguments = FormalArguments.build(*args, **kwargs).as(AbstractArguments)
new(method, arguments) new(method, arguments)
end end
# Constructs a string containing the method name and arguments. # Constructs a string containing the method name and arguments.
def to_s(io : IO) : Nil def to_s(io : IO) : Nil
io << '#' << method io << '#' << method << arguments
arguments.inspect(io)
end
# :ditto:
def inspect(io : IO) : Nil
to_s(io)
end end
end end
end end

View file

@ -1,6 +1,5 @@
require "./method_call" require "./method_call"
require "./mocked" require "./mocked"
require "./mock_registry"
require "./reference_mock_registry" require "./reference_mock_registry"
require "./stub" require "./stub"
require "./stubbed_name" require "./stubbed_name"
@ -37,35 +36,7 @@ module Spectator
macro define_subtype(base, mocked_type, type_name, name = nil, **value_methods, &block) macro define_subtype(base, mocked_type, type_name, name = nil, **value_methods, &block)
{% begin %} {% begin %}
{% if name %}@[::Spectator::StubbedName({{name}})]{% end %} {% if name %}@[::Spectator::StubbedName({{name}})]{% end %}
{% if base.id == :module.id %}
{{base.id}} {{type_name.id}}
include {{mocked_type.id}}
# Mock class that includes the mocked module {{mocked_type.id}}
{% if name %}@[::Spectator::StubbedName({{name}})]{% end %}
private class ClassIncludingMock{{type_name.id}}
include {{type_name.id}}
end
# Returns a mock class that includes the mocked module {{mocked_type.id}}.
def self.new(*args, **kwargs) : ClassIncludingMock{{type_name.id}}
# FIXME: Creating the instance normally with `.new` causing infinite recursion.
inst = ClassIncludingMock{{type_name.id}}.allocate
inst.initialize(*args, **kwargs)
inst
end
# Returns a mock class that includes the mocked module {{mocked_type.id}}.
def self.new(*args, **kwargs) : ClassIncludingMock{{type_name.id}}
# FIXME: Creating the instance normally with `.new` causing infinite recursion.
inst = ClassIncludingMock{{type_name.id}}.allocate
inst.initialize(*args, **kwargs) { |*yargs| yield *yargs }
inst
end
{% else %}
{{base.id}} {{type_name.id}} < {{mocked_type.id}} {{base.id}} {{type_name.id}} < {{mocked_type.id}}
{% end %}
include ::Spectator::Mocked include ::Spectator::Mocked
extend ::Spectator::StubbedType extend ::Spectator::StubbedType
@ -79,22 +50,18 @@ module Spectator
end end
{% end %} {% end %}
def _spectator_remove_stub(stub : ::Spectator::Stub) : ::Nil def _spectator_clear_stubs : Nil
@_spectator_stubs.try &.delete(stub)
end
def _spectator_clear_stubs : ::Nil
@_spectator_stubs = nil @_spectator_stubs = nil
end end
private class_getter _spectator_stubs : ::Array(::Spectator::Stub) = [] of ::Spectator::Stub private class_getter _spectator_stubs : Array(::Spectator::Stub) = [] of ::Spectator::Stub
class_getter _spectator_calls : ::Array(::Spectator::MethodCall) = [] of ::Spectator::MethodCall class_getter _spectator_calls : Array(::Spectator::MethodCall) = [] of ::Spectator::MethodCall
getter _spectator_calls = [] of ::Spectator::MethodCall getter _spectator_calls = [] of ::Spectator::MethodCall
# Returns the mock's name formatted for user output. # Returns the mock's name formatted for user output.
private def _spectator_stubbed_name : ::String private def _spectator_stubbed_name : String
\{% if anno = @type.annotation(::Spectator::StubbedName) %} \{% if anno = @type.annotation(::Spectator::StubbedName) %}
"#<Mock {{mocked_type.id}} \"" + \{{(anno[0] || :Anonymous.id).stringify}} + "\">" "#<Mock {{mocked_type.id}} \"" + \{{(anno[0] || :Anonymous.id).stringify}} + "\">"
\{% else %} \{% else %}
@ -102,7 +69,7 @@ module Spectator
\{% end %} \{% end %}
end end
private def self._spectator_stubbed_name : ::String private def self._spectator_stubbed_name : String
\{% if anno = @type.annotation(::Spectator::StubbedName) %} \{% if anno = @type.annotation(::Spectator::StubbedName) %}
"#<Class Mock {{mocked_type.id}} \"" + \{{(anno[0] || :Anonymous.id).stringify}} + "\">" "#<Class Mock {{mocked_type.id}} \"" + \{{(anno[0] || :Anonymous.id).stringify}} + "\">"
\{% else %} \{% else %}
@ -113,7 +80,7 @@ module Spectator
macro finished macro finished
stub_type {{mocked_type.id}} stub_type {{mocked_type.id}}
{{block.body if block}} {% if block %}{{block.body}}{% end %}
end end
end end
{% end %} {% end %}
@ -149,7 +116,7 @@ module Spectator
macro inject(base, type_name, name = nil, **value_methods, &block) macro inject(base, type_name, name = nil, **value_methods, &block)
{% begin %} {% begin %}
{% if name %}@[::Spectator::StubbedName({{name}})]{% end %} {% if name %}@[::Spectator::StubbedName({{name}})]{% end %}
{{base.id}} {{"::".id unless type_name.id.starts_with?("::")}}{{type_name.id}} {{base.id}} ::{{type_name.id}}
include ::Spectator::Mocked include ::Spectator::Mocked
extend ::Spectator::StubbedType extend ::Spectator::StubbedType
@ -158,12 +125,12 @@ module Spectator
{% elsif base == :struct %} {% elsif base == :struct %}
@@_spectator_mock_registry = ::Spectator::ValueMockRegistry(self).new @@_spectator_mock_registry = ::Spectator::ValueMockRegistry(self).new
{% else %} {% else %}
@@_spectator_mock_registry = ::Spectator::MockRegistry.new {% raise "Unsupported base type #{base} for injecting mock" %}
{% end %} {% end %}
private class_getter _spectator_stubs : ::Array(::Spectator::Stub) = [] of ::Spectator::Stub private class_getter _spectator_stubs : Array(::Spectator::Stub) = [] of ::Spectator::Stub
class_getter _spectator_calls : ::Array(::Spectator::MethodCall) = [] of ::Spectator::MethodCall class_getter _spectator_calls : Array(::Spectator::MethodCall) = [] of ::Spectator::MethodCall
private def _spectator_stubs private def _spectator_stubs
entry = @@_spectator_mock_registry.fetch(self) do entry = @@_spectator_mock_registry.fetch(self) do
@ -172,11 +139,7 @@ module Spectator
entry.stubs entry.stubs
end end
def _spectator_remove_stub(stub : ::Spectator::Stub) : ::Nil def _spectator_clear_stubs : Nil
@@_spectator_mock_registry[self]?.try &.stubs.delete(stub)
end
def _spectator_clear_stubs : ::Nil
@@_spectator_mock_registry.delete(self) @@_spectator_mock_registry.delete(self)
end end
@ -198,7 +161,7 @@ module Spectator
end end
# Returns the mock's name formatted for user output. # Returns the mock's name formatted for user output.
private def _spectator_stubbed_name : ::String private def _spectator_stubbed_name : String
\{% if anno = @type.annotation(::Spectator::StubbedName) %} \{% if anno = @type.annotation(::Spectator::StubbedName) %}
"#<Mock {{type_name.id}} \"" + \{{(anno[0] || :Anonymous.id).stringify}} + "\">" "#<Mock {{type_name.id}} \"" + \{{(anno[0] || :Anonymous.id).stringify}} + "\">"
\{% else %} \{% else %}
@ -207,7 +170,7 @@ module Spectator
end end
# Returns the mock's name formatted for user output. # Returns the mock's name formatted for user output.
private def self._spectator_stubbed_name : ::String private def self._spectator_stubbed_name : String
\{% if anno = @type.annotation(::Spectator::StubbedName) %} \{% if anno = @type.annotation(::Spectator::StubbedName) %}
"#<Class Mock {{type_name.id}} \"" + \{{(anno[0] || :Anonymous.id).stringify}} + "\">" "#<Class Mock {{type_name.id}} \"" + \{{(anno[0] || :Anonymous.id).stringify}} + "\">"
\{% else %} \{% else %}
@ -218,7 +181,7 @@ module Spectator
macro finished macro finished
stub_type {{type_name.id}} stub_type {{type_name.id}}
{{block.body if block}} {% if block %}{{block.body}}{% end %}
end end
end end
{% end %} {% end %}

View file

@ -1,43 +0,0 @@
require "./mock_registry_entry"
require "./stub"
module Spectator
# Stores collections of stubs for mocked types.
#
# This type is intended for all mocked modules that have functionality "injected."
# That is, the type itself has mock functionality bolted on.
# Adding instance members should be avoided, for instance, it could mess up serialization.
class MockRegistry
@entry : MockRegistryEntry?
# Retrieves all stubs.
def [](_object = nil)
@entry.not_nil!
end
# Retrieves all stubs.
def []?(_object = nil)
@entry
end
# Retrieves all stubs.
#
# Yields to the block on the first retrieval.
# This allows a mock to populate the registry with initial stubs.
def fetch(object : Reference, & : -> Array(Stub))
entry = @entry
if entry.nil?
entry = MockRegistryEntry.new
entry.stubs = yield
@entry = entry
else
entry
end
end
# Clears all stubs defined for a mocked object.
def delete(object : Reference) : Nil
@entry = nil
end
end
end

View file

@ -26,10 +26,6 @@ module Spectator
_spectator_stubs.unshift(stub) _spectator_stubs.unshift(stub)
end end
def _spectator_remove_stub(stub : Stub) : Nil
_spectator_stubs.delete(stub)
end
def _spectator_clear_stubs : Nil def _spectator_clear_stubs : Nil
_spectator_stubs.clear _spectator_stubs.clear
end end

View file

@ -26,7 +26,7 @@ module Spectator
private def _spectator_abstract_stub_fallback(call : MethodCall) private def _spectator_abstract_stub_fallback(call : MethodCall)
if _spectator_stub_for_method?(call.method) if _spectator_stub_for_method?(call.method)
Log.info { "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." } Log.info { "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." }
raise UnexpectedMessage.new("#{inspect} received unexpected message #{call}") raise UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}")
else else
Log.trace { "Fallback for #{call} - return self" } Log.trace { "Fallback for #{call} - return self" }
self self
@ -42,22 +42,22 @@ module Spectator
private def _spectator_abstract_stub_fallback(call : MethodCall, type) private def _spectator_abstract_stub_fallback(call : MethodCall, type)
if _spectator_stub_for_method?(call.method) if _spectator_stub_for_method?(call.method)
Log.info { "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." } Log.info { "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." }
raise UnexpectedMessage.new("#{inspect} received unexpected message #{call}") raise UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}")
else else
raise TypeCastError.new("#{inspect} received message #{call} and is attempting to return `self`, but returned type must be `#{type}`.") raise TypeCastError.new("#{_spectator_stubbed_name} received message #{call} and is attempting to return `self`, but returned type must be `#{type}`.")
end end
end end
# Handles all undefined messages. # Handles all undefined messages.
# Returns stubbed values if available, otherwise delegates to `#_spectator_abstract_stub_fallback`. # Returns stubbed values if available, otherwise delegates to `#_spectator_abstract_stub_fallback`.
macro method_missing(call) macro method_missing(call)
Log.trace { "Got undefined method `{{call.name}}({{*call.args}}{% if call.named_args %}{% unless call.args.empty? %}, {% end %}{{*call.named_args}}{% end %}){% if call.block %} { ... }{% end %}`" }
# Capture information about the call. # Capture information about the call.
%args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{{call.named_args.splat if call.named_args}}) %args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{% if call.named_args %}{{*call.named_args}}{% end %})
%call = ::Spectator::MethodCall.new({{call.name.symbolize}}, %args) %call = ::Spectator::MethodCall.new({{call.name.symbolize}}, %args)
_spectator_record_call(%call) _spectator_record_call(%call)
Log.trace { "#{inspect} got undefined method `#{%call}{% if call.block %} { ... }{% end %}`" }
self self
end end
end end

View file

@ -25,12 +25,6 @@ module Spectator
@entries[key] @entries[key]
end end
# Retrieves all stubs defined for a mocked object or nil if the object isn't mocked yet.
def []?(object : Reference)
key = Box.box(object)
@entries[key]?
end
# Retrieves all stubs defined for a mocked object. # Retrieves all stubs defined for a mocked object.
# #
# Yields to the block on the first retrieval. # Yields to the block on the first retrieval.

View file

@ -22,23 +22,6 @@ module Spectator
def initialize(@method : Symbol, @constraint : AbstractArguments? = nil, @location : Location? = nil) def initialize(@method : Symbol, @constraint : AbstractArguments? = nil, @location : Location? = nil)
end end
# String representation of the stub, formatted as a method call.
def message(io : IO) : Nil
io << "#" << method << (constraint || "(any args)")
end
# String representation of the stub, formatted as a method call.
def message
String.build do |str|
message(str)
end
end
# String representation of the stub, formatted as a method definition.
def to_s(io : IO) : Nil
message(io)
end
# Checks if a method call should receive the response from this stub. # Checks if a method call should receive the response from this stub.
def ===(call : MethodCall) def ===(call : MethodCall)
return false if method != call.method return false if method != call.method
@ -46,5 +29,10 @@ module Spectator
constraint === call.arguments constraint === call.arguments
end end
# String representation of the stub, formatted as a method call.
def to_s(io : IO) : Nil
io << "#" << method << (constraint || "(any args)")
end
end end
end end

View file

@ -1,5 +1,5 @@
require "../dsl/reserved" require "../dsl/reserved"
require "./formal_arguments" require "./arguments"
require "./method_call" require "./method_call"
require "./stub" require "./stub"
require "./typed_stub" require "./typed_stub"
@ -28,9 +28,6 @@ module Spectator
# Defines a stub to change the behavior of a method. # Defines a stub to change the behavior of a method.
abstract def _spectator_define_stub(stub : Stub) : Nil abstract def _spectator_define_stub(stub : Stub) : Nil
# Removes a specific, previously defined stub.
abstract def _spectator_remove_stub(stub : Stub) : Nil
# Clears all previously defined stubs. # Clears all previously defined stubs.
abstract def _spectator_clear_stubs : Nil abstract def _spectator_clear_stubs : Nil
@ -118,7 +115,7 @@ module Spectator
{% raise "Default stub cannot be an abstract method" if method.abstract? %} {% raise "Default stub cannot be an abstract method" if method.abstract? %}
{% raise "Cannot stub method with reserved keyword as name - #{method.name}" if method.name.starts_with?("_spectator") || ::Spectator::DSL::RESERVED_KEYWORDS.includes?(method.name.symbolize) %} {% raise "Cannot stub method with reserved keyword as name - #{method.name}" if method.name.starts_with?("_spectator") || ::Spectator::DSL::RESERVED_KEYWORDS.includes?(method.name.symbolize) %}
{{visibility.id if visibility != :public}} def {{"#{method.receiver}.".id if method.receiver}}{{method.name}}( {{visibility.id if visibility != :public}} def {{method.receiver && "#{method.receiver}.".id}}{{method.name}}(
{% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %} {% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %}
{% if method.double_splat %}**{{method.double_splat}}, {% end %} {% if method.double_splat %}**{{method.double_splat}}, {% end %}
{% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %} {% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %}
@ -126,64 +123,25 @@ module Spectator
{{method.body}} {{method.body}}
end end
{% original = "previous_def" {% original = "previous_def#{" { |*_spectator_yargs| yield *_spectator_yargs }".id if method.accepts_block?}".id %}
# Workaround for Crystal not propagating block with previous_def/super.
if method.accepts_block?
original += "("
if method.splat_index
method.args.each_with_index do |arg, i|
if i == method.splat_index
if arg.internal_name && arg.internal_name.size > 0
original += "*#{arg.internal_name}, "
end
original += "**#{method.double_splat}, " if method.double_splat
elsif i > method.splat_index
original += "#{arg.name}: #{arg.internal_name}, "
else
original += "#{arg.internal_name}, "
end
end
else
method.args.each do |arg|
original += "#{arg.internal_name}, "
end
original += "**#{method.double_splat}, " if method.double_splat
end
# If the block is captured (i.e. `&block` syntax), it must be passed along as an argument.
# Otherwise, use `yield` to forward the block.
captured_block = if method.block_arg && method.block_arg.name && method.block_arg.name.size > 0
method.block_arg.name
else
nil
end
original += "&#{captured_block}" if captured_block
original += ")"
original += " { |*_spectator_yargs| yield *_spectator_yargs }" unless captured_block
end
original = original.id %}
{% # Reconstruct the method signature. {% # Reconstruct the method signature.
# I wish there was a better way of doing this, but there isn't (at least not that I'm aware of). # I wish there was a better way of doing this, but there isn't (at least not that I'm aware of).
# This chunk of code must reconstruct the method signature exactly as it was originally. # This chunk of code must reconstruct the method signature exactly as it was originally.
# If it doesn't match, it doesn't override the method and the stubbing won't work. # If it doesn't match, it doesn't override the method and the stubbing won't work.
%} %}
{{visibility.id if visibility != :public}} def {{"#{method.receiver}.".id if method.receiver}}{{method.name}}( {{visibility.id if visibility != :public}} def {{method.receiver && "#{method.receiver}.".id}}{{method.name}}(
{% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %} {% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %}
{% if method.double_splat %}**{{method.double_splat}}, {% end %} {% if method.double_splat %}**{{method.double_splat}}, {% end %}
{% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %} {% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %}
){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %} ){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %}
# Capture information about the call. # Capture information about the call.
%call = ::Spectator::MethodCall.build( %args = ::Spectator::Arguments.capture(
{{method.name.symbolize}}, {% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg.internal_name}}, {% end %}
::NamedTuple.new( {% if method.double_splat %}**{{method.double_splat}}{% end %}
{% 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).empty? %}{{splat.symbolize}}, {{splat}},{% end %}
::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 %}
).merge({{method.double_splat}})
) )
%call = ::Spectator::MethodCall.new({{method.name.symbolize}}, %args)
_spectator_record_call(%call) _spectator_record_call(%call)
# Attempt to find a stub that satisfies the method call and arguments. # Attempt to find a stub that satisfies the method call and arguments.
@ -192,26 +150,12 @@ module Spectator
# Cast the stub or return value to the expected type. # Cast the stub or return value to the expected type.
# This is necessary to match the expected return type of the original method. # This is necessary to match the expected return type of the original method.
_spectator_cast_stub_value(%stub, %call, typeof({{original}}), _spectator_cast_stub_value(%stub, %call, typeof({{original}}),
{{ if rt = method.return_type {{ if method.return_type && method.return_type.resolve == NoReturn
if rt.is_a?(Path) && (resolved = rt.resolve?).is_a?(TypeNode) && resolved <= NoReturn
:no_return :no_return
else elsif method.return_type && method.return_type.resolve <= Nil || method.return_type.is_a?(Union) && method.return_type.types.map(&.resolve).includes?(Nil)
# Process as an enumerable type to reduce code repetition.
rt = rt.is_a?(Union) ? rt.types : [rt]
# Check if any types are nilable.
nilable = rt.any? do |t|
# These are all macro types that have the `resolve?` method.
(t.is_a?(TypeNode) || t.is_a?(Path) || t.is_a?(Generic) || t.is_a?(MetaClass)) &&
(resolved = t.resolve?).is_a?(TypeNode) && resolved <= Nil
end
if nilable
:nil :nil
else else
:raise :raise
end
end
else
:raise
end }}) end }})
else else
# Delegate missing stub behavior to concrete type. # Delegate missing stub behavior to concrete type.
@ -267,7 +211,7 @@ module Spectator
%} %}
{% unless method.abstract? %} {% unless method.abstract? %}
{{visibility.id if visibility != :public}} def {{"#{method.receiver}.".id if method.receiver}}{{method.name}}( {{visibility.id if visibility != :public}} def {{method.receiver && "#{method.receiver}.".id}}{{method.name}}(
{% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %} {% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %}
{% if method.double_splat %}**{{method.double_splat}}, {% end %} {% if method.double_splat %}**{{method.double_splat}}, {% end %}
{% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %} {% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %}
@ -275,42 +219,7 @@ module Spectator
{{method.body}} {{method.body}}
end end
{% original = "previous_def" {% original = "previous_def#{" { |*_spectator_yargs| yield *_spectator_yargs }".id if method.accepts_block?}".id %}
# Workaround for Crystal not propagating block with previous_def/super.
if method.accepts_block?
original += "("
if method.splat_index
method.args.each_with_index do |arg, i|
if i == method.splat_index
if arg.internal_name && arg.internal_name.size > 0
original += "*#{arg.internal_name}, "
end
original += "**#{method.double_splat}, " if method.double_splat
elsif i > method.splat_index
original += "#{arg.name}: #{arg.internal_name}"
else
original += "#{arg.internal_name}, "
end
end
else
method.args.each do |arg|
original += "#{arg.internal_name}, "
end
original += "**#{method.double_splat}, " if method.double_splat
end
# If the block is captured (i.e. `&block` syntax), it must be passed along as an argument.
# Otherwise, use `yield` to forward the block.
captured_block = if method.block_arg && method.block_arg.name && method.block_arg.name.size > 0
method.block_arg.name
else
nil
end
original += "&#{captured_block}" if captured_block
original += ")"
original += " { |*_spectator_yargs| yield *_spectator_yargs }" unless captured_block
end
original = original.id %}
{% end %} {% end %}
{% # Reconstruct the method signature. {% # Reconstruct the method signature.
@ -318,23 +227,18 @@ module Spectator
# This chunk of code must reconstruct the method signature exactly as it was originally. # This chunk of code must reconstruct the method signature exactly as it was originally.
# If it doesn't match, it doesn't override the method and the stubbing won't work. # If it doesn't match, it doesn't override the method and the stubbing won't work.
%} %}
{{visibility.id if visibility != :public}} def {{"#{method.receiver}.".id if method.receiver}}{{method.name}}( {{visibility.id if visibility != :public}} def {{method.receiver && "#{method.receiver}.".id}}{{method.name}}(
{% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %} {% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %}
{% if method.double_splat %}**{{method.double_splat}}, {% end %} {% if method.double_splat %}**{{method.double_splat}}, {% end %}
{% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %} {% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %}
){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %} ){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %}
# Capture information about the call. # Capture information about the call.
%call = ::Spectator::MethodCall.build( %args = ::Spectator::Arguments.capture(
{{method.name.symbolize}}, {% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg.internal_name}}, {% end %}
::NamedTuple.new( {% if method.double_splat %}**{{method.double_splat}}{% end %}
{% 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).empty? %}{{splat.symbolize}}, {{splat}},{% end %}
::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 %}
).merge({{method.double_splat}})
) )
%call = ::Spectator::MethodCall.new({{method.name.symbolize}}, %args)
_spectator_record_call(%call) _spectator_record_call(%call)
# Attempt to find a stub that satisfies the method call and arguments. # Attempt to find a stub that satisfies the method call and arguments.
@ -342,25 +246,15 @@ module Spectator
if %stub = _spectator_find_stub(%call) if %stub = _spectator_find_stub(%call)
# Cast the stub or return value to the expected type. # Cast the stub or return value to the expected type.
# This is necessary to match the expected return type of the original method. # This is necessary to match the expected return type of the original method.
{% if rt = method.return_type %} {% if method.return_type %}
# Return type restriction takes priority since it can be a superset of the original implementation. # Return type restriction takes priority since it can be a superset of the original implementation.
_spectator_cast_stub_value(%stub, %call, {{method.return_type}}, _spectator_cast_stub_value(%stub, %call, {{method.return_type}},
{{ if rt.is_a?(Path) && (resolved = rt.resolve?).is_a?(TypeNode) && resolved <= NoReturn {{ if method.return_type.resolve == NoReturn
:no_return :no_return
else elsif method.return_type.resolve <= Nil || method.return_type.is_a?(Union) && method.return_type.types.map(&.resolve).includes?(Nil)
# Process as an enumerable type to reduce code repetition.
rt = rt.is_a?(Union) ? rt.types : [rt]
# Check if any types are nilable.
nilable = rt.any? do |t|
# These are all macro types that have the `resolve?` method.
(t.is_a?(TypeNode) || t.is_a?(Path) || t.is_a?(Generic) || t.is_a?(MetaClass)) &&
(resolved = t.resolve?).is_a?(TypeNode) && resolved <= Nil
end
if nilable
:nil :nil
else else
:raise :raise
end
end }}) end }})
{% elsif !method.abstract? %} {% elsif !method.abstract? %}
# The method isn't abstract, infer the type it returns without calling it. # The method isn't abstract, infer the type it returns without calling it.
@ -431,96 +325,67 @@ module Spectator
# Redefines all methods and ones inherited from its parents and mixins to support stubs. # Redefines all methods and ones inherited from its parents and mixins to support stubs.
private macro stub_type(type_name = @type) private macro stub_type(type_name = @type)
{% type = type_name.resolve {% type = type_name.resolve
definitions = [] of Nil # Reverse order of ancestors (there's currently no reverse method for ArrayLiteral).
scope = if type == @type count = type.ancestors.size
:previous_def ancestors = type.ancestors.map_with_index { |_, i| type.ancestors[count - i - 1] } %}
elsif type.module? {% for ancestor in ancestors %}
type.name {% for method in ancestor.methods.reject do |meth|
else meth.name.starts_with?("_spectator") ||
:super ::Spectator::DSL::RESERVED_KEYWORDS.includes?(meth.name.symbolize)
end.id
# Add entries for methods in the target type and its class type.
[[:self.id, type.class], [nil, type]].each do |(receiver, t)|
t.methods.each do |method|
definitions << {
type: t,
method: method,
scope: scope,
receiver: receiver,
}
end
end
# Iterate through all ancestors and add their methods.
type.ancestors.each do |ancestor|
[[:self.id, ancestor.class], [nil, ancestor]].each do |(receiver, t)|
t.methods.each do |method|
# Skip methods already found to prevent redefining them multiple times.
unless definitions.any? do |d|
m = d[:method]
m.name == method.name &&
m.args == method.args &&
m.splat_index == method.splat_index &&
m.double_splat == method.double_splat &&
m.block_arg == method.block_arg
end
definitions << {
type: t,
method: method,
scope: :super.id,
receiver: receiver,
}
end
end
end
end
definitions = definitions.reject do |definition|
name = definition[:method].name
name.starts_with?("_spectator") || ::Spectator::DSL::RESERVED_KEYWORDS.includes?(name.symbolize)
end %} end %}
{{(method.abstract? ? :abstract_stub : :default_stub).id}} {{method.visibility.id if method.visibility != :public}} def {{method.receiver && "#{method.receiver}.".id}}{{method.name}}(
{% for definition in definitions %} {% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %}
{% original_type = definition[:type] {% if method.double_splat %}**{{method.double_splat}}, {% end %}
method = definition[:method] {% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %}
scope = definition[:scope] ){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %}
receiver = definition[:receiver] super{% if method.accepts_block? %} { |*%yargs| yield *%yargs }{% end %}
rewrite_args = method.accepts_block?
# Handle calling methods on other objects (primarily for mock modules).
if scope != :super.id && scope != :previous_def.id
if receiver == :self.id
scope = "#{scope}.#{method.name}".id
rewrite_args = true
else
scope = :super.id
end end
{% end %}
{% for method in ancestor.class.methods.reject do |meth|
meth.name.starts_with?("_spectator") ||
::Spectator::DSL::RESERVED_KEYWORDS.includes?(meth.name.symbolize)
end %} end %}
# Redefinition of {{original_type}}{{"#".id}}{{method.name}} default_stub {{method.visibility.id if method.visibility != :public}} def self.{{method.name}}(
{{(method.abstract? ? "abstract_stub abstract" : "default_stub").id}} {{method.visibility.id if method.visibility != :public}} def {{"#{receiver}.".id if receiver}}{{method.name}}( {% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %}
{% if method.double_splat %}**{{method.double_splat}}, {% end %}
{% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %}
){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %}
super{% if method.accepts_block? %} { |*%yargs| yield *%yargs }{% end %}
end
{% end %}
{% end %}
{% for method in type.methods.reject do |meth|
meth.name.starts_with?("_spectator") ||
::Spectator::DSL::RESERVED_KEYWORDS.includes?(meth.name.symbolize)
end %}
{{(method.abstract? ? :"abstract_stub abstract" : :default_stub).id}} {{method.visibility.id if method.visibility != :public}} def {{method.receiver && "#{method.receiver}.".id}}{{method.name}}(
{% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %} {% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %}
{% if method.double_splat %}**{{method.double_splat}}, {% end %} {% if method.double_splat %}**{{method.double_splat}}, {% end %}
{% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %} {% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %}
){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %} ){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %}
{% unless method.abstract? %} {% unless method.abstract? %}
{{scope}}{% if rewrite_args %}({% for arg, i in method.args %} {% if type == @type %}previous_def{% else %}super{% end %}{% if method.accepts_block? %} { |*%yargs| yield *%yargs }{% end %}
{% if i == method.splat_index && arg.internal_name && arg.internal_name.size > 0 %}*{{arg.internal_name}}, {% if method.double_splat %}**{{method.double_splat}}, {% end %}{% end %}
{% if method.splat_index && i > method.splat_index %}{{arg.name}}: {{arg.internal_name}}, {% end %}
{% if !method.splat_index || i < method.splat_index %}{{arg.internal_name}}, {% end %}{% end %}
{% if !method.splat_index && method.double_splat %}**{{method.double_splat}}, {% end %}
{% captured_block = if method.block_arg && method.block_arg.name && method.block_arg.name.size > 0
method.block_arg.name
else
nil
end %}
{% if captured_block %}&{{captured_block}}{% end %}
){% if !captured_block && method.accepts_block? %} { |*%yargs| yield *%yargs }{% end %}{% end %}
end end
{% end %} {% end %}
{% end %} {% end %}
{% for method in type.class.methods.reject do |meth|
meth.name.starts_with?("_spectator") ||
::Spectator::DSL::RESERVED_KEYWORDS.includes?(meth.name.symbolize)
end %}
default_stub {{method.visibility.id if method.visibility != :public}} def self.{{method.name}}(
{% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %}
{% if method.double_splat %}**{{method.double_splat}}, {% end %}
{% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %}
){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %}
{% if type == @type %}previous_def{% else %}super{% end %}{% if method.accepts_block? %} { |*%yargs| yield *%yargs }{% end %}
end
{% end %}
end end
# Utility macro for casting a stub (and its return value) to the correct type. # Utility macro for casting a stub (and it's return value) to the correct type.
# #
# *stub* is the variable holding the stub. # *stub* is the variable holding the stub.
# *call* is the variable holding the captured method call. # *call* is the variable holding the captured method call.
@ -530,6 +395,18 @@ module Spectator
# - `:raise` - raise a `TypeCastError`. # - `:raise` - raise a `TypeCastError`.
# - `:no_return` - raise as no value should be returned. # - `:no_return` - raise as no value should be returned.
private macro _spectator_cast_stub_value(stub, call, type, fail_cast = :nil) private macro _spectator_cast_stub_value(stub, call, type, fail_cast = :nil)
# Attempt to cast the stub to the method's return type.
# If successful, return the value of the stub.
# This is a common usage where the return type is simple and matches the stub type exactly.
if %typed = {{stub}}.as?(::Spectator::TypedStub({{type}}))
%typed.call({{call}})
else
# The stub couldn't be easily cast to match the return type.
# Even though all stubs will have a `#call` method, the compiler doesn't seem to agree.
# Assert that it will (this should never fail).
raise TypeCastError.new("Stub has no value") unless {{stub}}.responds_to?(:call)
{% if fail_cast == :no_return %} {% if fail_cast == :no_return %}
{{stub}}.call({{call}}) {{stub}}.call({{call}})
raise TypeCastError.new("#{_spectator_stubbed_name} received message #{ {{call}} } and is attempting to return a value, but it shouldn't have returned (`NoReturn`).") raise TypeCastError.new("#{_spectator_stubbed_name} received message #{ {{call}} } and is attempting to return a value, but it shouldn't have returned (`NoReturn`).")
@ -537,31 +414,30 @@ 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.
# The caller will receive a properly typed value without unions or other side-effects. # The caller will receive a properly typed value without unions or other side-effects.
%cast = %value.as?({{type}}) if %cast = %value.as?({{type}})
{% if fail_cast == :nil %}
%cast %cast
{% elsif fail_cast == :raise %}
# Check if nil was returned by the stub and if its okay to return it.
if %value.nil? && %type.nilable?
# Value was nil and nil is allowed to be returned.
%type.cast(%cast)
elsif %cast.nil?
# 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}`.")
else else
# Types match and value can be returned as cast type. {% if fail_cast == :nil %}
%cast nil
{% elsif fail_cast == :raise %}
# The stubbed value was something else entirely and cannot be cast to the return type.
# There's something weird going on (compiler bug?) that sometimes causes this class lookup to fail.
%type = begin
%value.class.to_s
rescue
"<Unknown>"
end end
raise TypeCastError.new("#{_spectator_stubbed_name} received message #{ {{call}} } and is attempting to return a `#{%type}`, but returned type must be `#{ {{type}} }`.")
{% else %} {% else %}
{% raise "fail_cast must be :nil, :raise, or :no_return, but got: #{fail_cast}" %} {% raise "fail_cast must be :nil, :raise, or :no_return, but got: #{fail_cast}" %}
{% end %} {% end %}
end
{% end %} {% end %}
end end
end end
end
end end

View file

@ -20,10 +20,6 @@ module Spectator
_spectator_stubs.unshift(stub) _spectator_stubs.unshift(stub)
end end
def _spectator_remove_stub(stub : Stub) : Nil
_spectator_stubs.delete(stub)
end
def _spectator_clear_stubs : Nil def _spectator_clear_stubs : Nil
_spectator_stubs.clear _spectator_stubs.clear
end end

View file

@ -9,11 +9,5 @@ module Spectator
abstract class TypedStub(T) < Stub abstract class TypedStub(T) < Stub
# Invokes the stubbed implementation. # Invokes the stubbed implementation.
abstract def call(call : MethodCall) : T abstract def call(call : MethodCall) : T
# String representation of the stub, formatted as a method call.
def to_s(io : IO) : Nil
super
io << " : " << T
end
end end
end end

View file

@ -29,12 +29,6 @@ module Spectator
@entries[key] @entries[key]
end end
# Retrieves all stubs defined for a mocked object or nil if the object isn't mocked yet.
def []?(object : T)
key = value_bytes(object)
@entries[key]?
end
# Retrieves all stubs defined for a mocked object. # Retrieves all stubs defined for a mocked object.
# #
# Yields to the block on the first retrieval. # Yields to the block on the first retrieval.

Some files were not shown because too many files have changed in this diff Show more