mirror of
https://gitea.invidious.io/iv-org/shard-spectator.git
synced 2024-08-15 00:53:35 +00:00
Compare commits
70 commits
Author | SHA1 | Date | |
---|---|---|---|
|
dcaa05531a | ||
|
a93908d507 | ||
|
a634046a86 | ||
|
12f3f35958 | ||
|
73e1686219 | ||
|
287758e6af | ||
|
f39ceb8eba | ||
|
9b1d400ee1 | ||
|
edb20e5b2f | ||
|
526a998e41 | ||
|
556d4783bf | ||
|
b5fbc96195 | ||
|
5520999b6d | ||
|
4a630b1ebf | ||
|
d72895fe10 | ||
|
04f151fddf | ||
|
9cbb5d2cf7 | ||
|
3852606b28 | ||
|
726a2e1515 | ||
|
5c08427ca0 | ||
|
735122a94b | ||
|
9ea5c261b1 | ||
|
24a860ea11 | ||
|
528ad7257d | ||
|
7149ef7df5 | ||
|
cb89589155 | ||
|
a5e8f11e11 | ||
|
abbd6ffd71 | ||
|
fd372226ab | ||
|
6a5e5b8f7a | ||
|
4a0bfc1cb2 | ||
|
d46698d81a | ||
|
8c3900adcb | ||
|
30602663fe | ||
|
b8901f522a | ||
|
c4bcf54b98 | ||
|
acf810553a | ||
|
faff2933e6 | ||
|
0f8c46d6ef | ||
|
7620f58fb8 | ||
|
feaf1c6015 | ||
|
8f80b10fc1 | ||
|
a3c55dfa47 | ||
|
fa99987780 | ||
|
d378583054 | ||
|
6255cc85c4 | ||
|
e6584c9f04 | ||
|
f55c60e01f | ||
|
4b68b8e3de | ||
|
c3e7edc700 | ||
|
149c0e6e4b | ||
|
9f54a9e542 | ||
|
65a4b8e756 | ||
|
b52593dbde | ||
|
7e2ec4ee37 | ||
|
952e949307 | ||
|
293faccd5c | ||
|
2985ef5919 | ||
|
bd44b5562e | ||
|
47a62ece78 | ||
|
7ffa63718b | ||
|
275b217c6c | ||
|
fbe877690d | ||
|
a967dce241 | ||
|
1f98bf9ff1 | ||
|
5f499336ac | ||
|
df10c8e75b | ||
|
a585ef0996 | ||
|
2d6c8844d4 | ||
|
321c15407d |
61 changed files with 1931 additions and 287 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -10,3 +10,5 @@
|
||||||
|
|
||||||
# Ignore JUnit output
|
# Ignore JUnit output
|
||||||
output.xml
|
output.xml
|
||||||
|
|
||||||
|
/test.cr
|
||||||
|
|
58
CHANGELOG.md
58
CHANGELOG.md
|
@ -4,7 +4,54 @@ 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.11.4]
|
## [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
|
### 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 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.
|
- Add `before`, `after`, and `around` as aliases for `before_each`, `after_each`, and `around_each` respectively.
|
||||||
|
@ -416,8 +463,13 @@ This has been changed so that it compiles and raises an error at runtime with a
|
||||||
First version ready for public use.
|
First version ready for public use.
|
||||||
|
|
||||||
|
|
||||||
[Unreleased]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.4...master
|
[Unreleased]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.12.1...master
|
||||||
[0.11.3]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.3...v0.11.4
|
[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.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.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.1]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.0...v0.11.1
|
||||||
|
|
|
@ -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.11.0
|
version: ~> 0.12.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(thing)
|
expect(interface).to have_received(:invoke).with(dbl)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
name: spectator
|
name: spectator
|
||||||
version: 0.11.4
|
version: 0.12.1
|
||||||
description: |
|
description: |
|
||||||
Feature-rich testing framework for Crystal inspired by RSpec.
|
Feature-rich testing framework for Crystal inspired by RSpec.
|
||||||
|
|
||||||
|
|
|
@ -123,6 +123,109 @@ 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
|
||||||
|
|
|
@ -1,26 +1,28 @@
|
||||||
require "../spec_helper"
|
require "../spec_helper"
|
||||||
|
|
||||||
private abstract class Interface
|
module Readme
|
||||||
abstract def invoke(thing) : String
|
abstract class Interface
|
||||||
end
|
abstract def invoke(thing) : String
|
||||||
|
end
|
||||||
|
|
||||||
# Type being tested.
|
# Type being tested.
|
||||||
private class Driver
|
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 Driver do
|
Spectator.describe Readme::Driver do
|
||||||
# Define a mock for Interface.
|
# Define a mock for Interface.
|
||||||
mock Interface
|
mock Readme::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(Interface)
|
interface = mock(Readme::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")
|
||||||
|
|
||||||
|
|
70
spec/features/expect_type_spec.cr
Normal file
70
spec/features/expect_type_spec.cr
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
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
|
22
spec/features/interpolated_label_spec.cr
Normal file
22
spec/features/interpolated_label_spec.cr
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
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
|
|
@ -26,12 +26,15 @@ Spectator.describe "GitHub Issue #44" do
|
||||||
# Original issue uses keyword arguments in place of positional arguments.
|
# Original issue uses keyword arguments in place of positional arguments.
|
||||||
context "keyword arguments in place of positional arguments" do
|
context "keyword arguments in place of positional arguments" do
|
||||||
before_each do
|
before_each do
|
||||||
expect(Process).to receive(:run).with(command, shell: true, output: :pipe).and_raise(exception)
|
pipe = Process::Redirect::Pipe
|
||||||
|
expect(Process).to receive(:run).with(command, shell: true, output: pipe).and_raise(exception)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "must stub Process.run", skip: "Keyword arguments in place of positional arguments not supported with expect-receive" do
|
it "must stub Process.run" do
|
||||||
Process.run(command, shell: true, output: :pipe) do |_process|
|
expect do
|
||||||
end
|
Process.run(command, shell: true, output: :pipe) do |_process|
|
||||||
|
end
|
||||||
|
end.to raise_error(File::NotFoundError, "File not found")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
135
spec/issues/github_issue_48_spec.cr
Normal file
135
spec/issues/github_issue_48_spec.cr
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
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
|
6
spec/issues/github_issue_49_spec.cr
Normal file
6
spec/issues/github_issue_49_spec.cr
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
require "../spec_helper"
|
||||||
|
|
||||||
|
# https://github.com/icy-arctic-fox/spectator/issues/49
|
||||||
|
Spectator.describe "GitHub Issue #49" do
|
||||||
|
# mock File
|
||||||
|
end
|
48
spec/issues/github_issue_55_spec.cr
Normal file
48
spec/issues/github_issue_55_spec.cr
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
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
|
|
@ -1,31 +1,33 @@
|
||||||
require "../spec_helper"
|
require "../spec_helper"
|
||||||
|
|
||||||
private class Foo
|
module GitLabIssue51
|
||||||
def call(str : String) : String?
|
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
|
end
|
||||||
|
|
||||||
def alt1_call(str : String) : String?
|
class Bar
|
||||||
nil
|
def call(a_foo) : Nil # Must add nil restriction here, otherwise a segfault occurs from returning the result of #alt2_call.
|
||||||
end
|
a_foo.call("")
|
||||||
|
a_foo.alt1_call("")
|
||||||
def alt2_call(str : String) : String?
|
a_foo.alt2_call("")
|
||||||
[str, nil].sample
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private class Bar
|
Spectator.describe GitLabIssue51::Bar do
|
||||||
def call(a_foo) : Nil # Must add nil restriction here, otherwise a segfault occurs from returning the result of #alt2_call.
|
mock GitLabIssue51::Foo, call: "", alt1_call: "", alt2_call: ""
|
||||||
a_foo.call("")
|
|
||||||
a_foo.alt1_call("")
|
|
||||||
a_foo.alt2_call("")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Spectator.describe Bar do
|
let(:foo) { mock(GitLabIssue51::Foo) }
|
||||||
mock Foo, call: "", alt1_call: "", alt2_call: ""
|
|
||||||
|
|
||||||
let(:foo) { mock(Foo) }
|
|
||||||
subject(:call) { described_class.new.call(foo) }
|
subject(:call) { described_class.new.call(foo) }
|
||||||
|
|
||||||
describe "#call" do
|
describe "#call" do
|
||||||
|
|
30
spec/issues/gitlab_issue_80_spec.cr
Normal file
30
spec/issues/gitlab_issue_80_spec.cr
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
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
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
@ -1027,4 +1027,262 @@ 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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -212,14 +212,10 @@ 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
|
||||||
|
@ -301,7 +297,7 @@ Spectator.describe Spectator::Double do
|
||||||
arg
|
arg
|
||||||
end
|
end
|
||||||
|
|
||||||
stub def self.baz(arg)
|
stub def self.baz(arg, &)
|
||||||
yield
|
yield
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -469,7 +465,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 eq(%i[baz])
|
expect(called_method_names(dbl)).to contain(:baz)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "stores arguments for a call" do
|
it "stores arguments for a call" do
|
||||||
|
@ -479,4 +475,68 @@ 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
|
||||||
|
|
|
@ -275,7 +275,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 eq(%i[baz])
|
expect(called_method_names(dbl)).to contain(:baz)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "stores arguments for a call" do
|
it "stores arguments for a call" do
|
||||||
|
@ -285,4 +285,68 @@ 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
|
||||||
|
|
|
@ -29,8 +29,18 @@ Spectator.describe Spectator::Mock do
|
||||||
@_spectator_invocations << :method3
|
@_spectator_invocations << :method3
|
||||||
"original"
|
"original"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def method4 : Thing
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def method5 : OtherThing
|
||||||
|
OtherThing.new
|
||||||
|
end
|
||||||
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
|
||||||
|
@ -104,6 +114,20 @@ 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
|
||||||
|
@ -120,8 +144,14 @@ 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
|
||||||
|
@ -199,6 +229,20 @@ 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
|
||||||
|
@ -215,8 +259,14 @@ 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
|
||||||
|
@ -286,6 +336,22 @@ 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
|
||||||
|
@ -298,11 +364,21 @@ 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
|
||||||
|
|
||||||
|
def self.other : OtherThing
|
||||||
|
OtherThing.new
|
||||||
|
end
|
||||||
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
|
||||||
|
@ -367,6 +443,20 @@ 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 { mock._spectator_define_stub(foo_stub) }
|
||||||
|
|
||||||
|
@ -401,6 +491,203 @@ 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
|
||||||
|
@ -642,7 +929,7 @@ Spectator.describe Spectator::Mock do
|
||||||
arg
|
arg
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.baz(arg)
|
def self.baz(arg, &)
|
||||||
yield
|
yield
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -186,12 +186,9 @@ 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
|
||||||
|
@ -262,7 +259,7 @@ Spectator.describe Spectator::NullDouble do
|
||||||
arg
|
arg
|
||||||
end
|
end
|
||||||
|
|
||||||
stub def self.baz(arg)
|
stub def self.baz(arg, &)
|
||||||
yield
|
yield
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -439,4 +436,68 @@ 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
|
||||||
|
|
|
@ -4,6 +4,11 @@
|
||||||
# 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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -137,7 +137,11 @@ module Spectator::DSL
|
||||||
what.is_a?(NilLiteral) %}
|
what.is_a?(NilLiteral) %}
|
||||||
{{what}}
|
{{what}}
|
||||||
{% elsif what.is_a?(StringInterpolation) %}
|
{% elsif what.is_a?(StringInterpolation) %}
|
||||||
{% raise "String interpolation isn't supported for example group names" %}
|
{{@type.name}}.new.eval do
|
||||||
|
{{what}}
|
||||||
|
rescue e
|
||||||
|
"<Failed to evaluate context label - #{e.class}: #{e}>"
|
||||||
|
end
|
||||||
{% else %}
|
{% else %}
|
||||||
{{what.stringify}}
|
{{what.stringify}}
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
|
@ -6,6 +6,9 @@ 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 %}
|
||||||
|
|
|
@ -31,7 +31,7 @@ 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}}) do
|
::Spectator::Double.define({{double_type_name}}, {{name}}, {{value_methods.double_splat}}) 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
|
||||||
|
@ -43,7 +43,7 @@ module Spectator::DSL
|
||||||
|
|
||||||
{% begin %}
|
{% begin %}
|
||||||
# Define a matching null double type.
|
# Define a matching null double type.
|
||||||
::Spectator::NullDouble.define({{null_double_type_name}}, {{name}}, {{**value_methods}}) {{block}}
|
::Spectator::NullDouble.define({{null_double_type_name}}, {{name}}, {{value_methods.double_splat}}) {{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}})
|
{{found_tuple[2].id}}.new({{value_methods.double_splat}})
|
||||||
{% else %}
|
{% else %}
|
||||||
::Spectator::LazyDouble.new({{name}}, {{**value_methods}})
|
::Spectator::LazyDouble.new({{name}}, {{value_methods.double_splat}})
|
||||||
{% 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}}) {{block}}
|
{% if @def %}new_double{% else %}def_double{% end %}({{name}}, {{value_methods.double_splat}}) {{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}})
|
::Spectator::LazyDouble.new({{value_methods.double_splat}})
|
||||||
end
|
end
|
||||||
|
|
||||||
# Defines a new mock type.
|
# Defines a new mock type.
|
||||||
|
@ -218,24 +218,29 @@ 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)
|
||||||
{% # Construct a unique type name for the mock by using the number of defined types.
|
{% resolved = type.resolve
|
||||||
index = ::Spectator::DSL::Mocks::TYPES.size
|
# Construct a unique type name for the mock by using the number of defined types.
|
||||||
mock_type_name = "Mock#{index}".id
|
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
|
||||||
|
|
||||||
# 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, mock_type_name.symbolize}
|
::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}
|
||||||
|
|
||||||
resolved = type.resolve
|
base = if resolved.class?
|
||||||
base = if resolved.class?
|
:class
|
||||||
:class
|
elsif resolved.struct?
|
||||||
elsif resolved.struct?
|
:struct
|
||||||
:struct
|
else
|
||||||
else
|
:module
|
||||||
:module
|
end %}
|
||||||
end %}
|
|
||||||
|
|
||||||
::Spectator::Mock.define_subtype({{base}}, {{type.id}}, {{mock_type_name}}, {{name}}, {{**value_methods}}) {{block}}
|
{% begin %}
|
||||||
|
{{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.
|
||||||
|
@ -316,7 +321,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}}) {{block}}
|
{% if @def %}new_mock{% else %}def_mock{% end %}({{type}}, {{value_methods.double_splat}}) {{block}}
|
||||||
{% end %}
|
{% end %}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -426,7 +431,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}}) {{block}}
|
::Spectator::Mock.inject({{base}}, {{resolved.name}}, {{value_methods.double_splat}}) {{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.
|
||||||
|
|
|
@ -11,7 +11,7 @@ module Spectator
|
||||||
end
|
end
|
||||||
|
|
||||||
# Calls the `error` method on *visitor*.
|
# Calls the `error` method on *visitor*.
|
||||||
def accept(visitor)
|
def accept(visitor, &)
|
||||||
visitor.error(yield self)
|
visitor.error(yield self)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -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 = Metadata.new)
|
@group : ExampleGroup? = nil, metadata = nil)
|
||||||
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 = Metadata.new)
|
@group : ExampleGroup? = nil, metadata = nil)
|
||||||
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 = Metadata.new, &block : self ->)
|
@group : ExampleGroup? = nil, metadata = nil, &block : self ->)
|
||||||
super(name, location, metadata)
|
super(name, location, metadata)
|
||||||
|
|
||||||
@context = NullContext.new
|
@context = NullContext.new
|
||||||
|
@ -93,9 +93,10 @@ 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 = Metadata.new, reason = nil)
|
group : ExampleGroup? = nil, metadata = nil, reason = nil)
|
||||||
# Add pending tag and reason if they don't exist.
|
# Add pending tag and reason if they don't exist.
|
||||||
metadata = metadata.merge({:pending => nil, :reason => reason}) { |_, v, _| v }
|
tags = {:pending => nil, :reason => reason}
|
||||||
|
metadata = metadata ? metadata.merge(tags) { |_, v, _| v } : tags
|
||||||
new(name, location, group, metadata) { nil }
|
new(name, location, group, metadata) { nil }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -117,7 +118,7 @@ module Spectator
|
||||||
|
|
||||||
begin
|
begin
|
||||||
@result = Harness.run do
|
@result = Harness.run do
|
||||||
if proc = @name_proc.as?(Proc(Example, String))
|
if proc = @name_proc
|
||||||
self.name = proc.call(self)
|
self.name = proc.call(self)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -163,7 +164,7 @@ module Spectator
|
||||||
# The context casted to an instance of *klass* is provided as a block argument.
|
# The context casted to an instance of *klass* is provided as a block argument.
|
||||||
#
|
#
|
||||||
# TODO: Benchmark compiler performance using this method versus client-side casting in a proc.
|
# TODO: Benchmark compiler performance using this method versus client-side casting in a proc.
|
||||||
protected def with_context(klass)
|
protected def with_context(klass, &)
|
||||||
context = klass.cast(@context)
|
context = klass.cast(@context)
|
||||||
with context yield
|
with context yield
|
||||||
end
|
end
|
||||||
|
@ -183,7 +184,7 @@ module Spectator
|
||||||
end
|
end
|
||||||
|
|
||||||
# Yields this example and all parent groups.
|
# Yields this example and all parent groups.
|
||||||
def ascend
|
def ascend(&)
|
||||||
node = self
|
node = self
|
||||||
while node
|
while node
|
||||||
yield node
|
yield node
|
||||||
|
@ -278,7 +279,7 @@ module Spectator
|
||||||
# The block given to this method will be executed within the test context.
|
# The block given to this method will be executed within the test context.
|
||||||
#
|
#
|
||||||
# TODO: Benchmark compiler performance using this method versus client-side casting in a proc.
|
# TODO: Benchmark compiler performance using this method versus client-side casting in a proc.
|
||||||
protected def with_context(klass)
|
protected def with_context(klass, &)
|
||||||
context = @example.cast_context(klass)
|
context = @example.cast_context(klass)
|
||||||
with context yield
|
with context yield
|
||||||
end
|
end
|
||||||
|
|
|
@ -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 = Metadata.new)
|
@name : String? = nil, @location : Location? = nil, @metadata : Metadata? = nil)
|
||||||
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 = Metadata.new)
|
@name : Example -> String, @location : Location? = nil, @metadata : Metadata? = nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Constructs an example with previously defined attributes and context.
|
# Constructs an example with previously defined attributes and context.
|
||||||
|
|
|
@ -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 = Metadata.new)
|
@group : ExampleGroup? = nil, @metadata : Metadata? = nil)
|
||||||
# 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
|
||||||
|
|
|
@ -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 = Metadata.new)
|
def initialize(@name : Label = nil, @location : Location? = nil, @metadata : Metadata? = nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Constructs an example group with previously defined attributes, children, and hooks.
|
# Constructs an example group with previously defined attributes, children, and hooks.
|
||||||
|
|
|
@ -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 = Metadata.new)
|
group : ExampleGroup? = nil, metadata : Metadata? = nil)
|
||||||
super(name, location, group, metadata)
|
super(name, location, group, metadata)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -114,6 +114,21 @@ 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
|
||||||
|
@ -136,6 +151,36 @@ 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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 = Metadata.new)
|
location : Location? = nil, metadata : Metadata? = nil)
|
||||||
super(name, location, metadata)
|
super(name, location, metadata)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
require "../value"
|
||||||
require "./match_data"
|
require "./match_data"
|
||||||
|
|
||||||
module Spectator::Matchers
|
module Spectator::Matchers
|
||||||
|
@ -22,6 +23,19 @@ 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
|
||||||
|
|
|
@ -29,7 +29,26 @@ 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
|
||||||
expected.value.includes?(actual.value)
|
actual_value = 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.
|
||||||
|
|
|
@ -1,13 +1,61 @@
|
||||||
module Spectator
|
module Spectator
|
||||||
# Untyped arguments to a method call (message).
|
# Untyped arguments to a method call (message).
|
||||||
abstract class AbstractArguments
|
abstract class AbstractArguments
|
||||||
# Utility method for comparing two named tuples ignoring order.
|
# Use the string representation to avoid over complicating debug output.
|
||||||
private def compare_named_tuples(a : NamedTuple, b : NamedTuple)
|
def inspect(io : IO) : Nil
|
||||||
a.each do |k, v1|
|
to_s(io)
|
||||||
v2 = b.fetch(k) { return false }
|
end
|
||||||
return false unless v1 === v2
|
|
||||||
|
# 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
|
end
|
||||||
true
|
true
|
||||||
end
|
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
|
||||||
|
|
|
@ -81,7 +81,7 @@ module Spectator
|
||||||
|
|
||||||
# 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)
|
||||||
positional === other.positional && compare_named_tuples(kwargs, other.kwargs)
|
compare_tuples(positional, other.positional) && compare_named_tuples(kwargs, other.kwargs)
|
||||||
end
|
end
|
||||||
|
|
||||||
# :ditto:
|
# :ditto:
|
||||||
|
@ -90,17 +90,18 @@ module Spectator
|
||||||
|
|
||||||
i = 0
|
i = 0
|
||||||
other.args.each do |k, v2|
|
other.args.each do |k, v2|
|
||||||
|
break if i >= positional.size
|
||||||
next if kwargs.has_key?(k) # Covered by named arguments.
|
next if kwargs.has_key?(k) # Covered by named arguments.
|
||||||
|
|
||||||
v1 = positional.fetch(i) { return false }
|
v1 = positional[i]
|
||||||
i += 1
|
i += 1
|
||||||
return false unless v1 === v2
|
return false unless compare_values(v1, v2)
|
||||||
end
|
end
|
||||||
|
|
||||||
other.splat.try &.each do |v2|
|
other.splat.try &.each do |v2|
|
||||||
v1 = positional.fetch(i) { return false }
|
v1 = positional.fetch(i) { return false }
|
||||||
i += 1
|
i += 1
|
||||||
return false unless v1 === v2
|
return false unless compare_values(v1, v2)
|
||||||
end
|
end
|
||||||
|
|
||||||
i == positional.size
|
i == positional.size
|
||||||
|
|
|
@ -98,24 +98,35 @@ module Spectator
|
||||||
# Simplified string representation of a double.
|
# Simplified string representation of a double.
|
||||||
# Avoids displaying nested content and bloating method instantiation.
|
# Avoids displaying nested content and bloating method instantiation.
|
||||||
def to_s(io : IO) : Nil
|
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 << _spectator_stubbed_name
|
||||||
|
|
||||||
|
io << ":0x"
|
||||||
|
object_id.to_s(io, 16)
|
||||||
|
io << '>'
|
||||||
end
|
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 #{_spectator_stubbed_name} #{stub}" }
|
Log.debug { "Defined stub for #{inspect} #{stub}" }
|
||||||
@stubs.unshift(stub)
|
@stubs.unshift(stub)
|
||||||
end
|
end
|
||||||
|
|
||||||
protected def _spectator_remove_stub(stub : Stub) : Nil
|
protected def _spectator_remove_stub(stub : Stub) : Nil
|
||||||
Log.debug { "Removing stub #{stub} from #{_spectator_stubbed_name}" }
|
Log.debug { "Removing stub #{stub} from #{inspect}" }
|
||||||
@stubs.delete(stub)
|
@stubs.delete(stub)
|
||||||
end
|
end
|
||||||
|
|
||||||
protected def _spectator_clear_stubs : Nil
|
protected def _spectator_clear_stubs : Nil
|
||||||
Log.debug { "Clearing stubs for #{_spectator_stubbed_name}" }
|
Log.debug { "Clearing stubs for #{inspect}" }
|
||||||
@stubs.clear
|
@stubs.clear
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -145,17 +156,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) %}
|
||||||
"#<Double " + {{(anno[0] || :Anonymous.id).stringify}} + ">"
|
{{(anno[0] || :Anonymous.id).stringify}}
|
||||||
{% else %}
|
{% else %}
|
||||||
"#<Double Anonymous>"
|
"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) %}
|
||||||
"#<Class Double " + {{(anno[0] || :Anonymous.id).stringify}} + ">"
|
{{(anno[0] || :Anonymous.id).stringify}}
|
||||||
{% else %}
|
{% else %}
|
||||||
"#<Class Double Anonymous>"
|
"Anonymous"
|
||||||
{% end %}
|
{% end %}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -175,7 +186,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("#{_spectator_stubbed_name} received unexpected message #{call}")
|
raise UnexpectedMessage.new("#{inspect} received unexpected message #{call}")
|
||||||
end
|
end
|
||||||
|
|
||||||
private def _spectator_abstract_stub_fallback(call : MethodCall, type)
|
private def _spectator_abstract_stub_fallback(call : MethodCall, type)
|
||||||
|
@ -194,9 +205,9 @@ module Spectator
|
||||||
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 { "#{_spectator_stubbed_name} got undefined method `#{call}{% if call.block %} { ... }{% end %}`" }
|
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
|
||||||
|
|
|
@ -122,12 +122,12 @@ module Spectator
|
||||||
|
|
||||||
# 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)
|
||||||
positional === other.positional && compare_named_tuples(kwargs, other.kwargs)
|
compare_tuples(positional, other.positional) && compare_named_tuples(kwargs, other.kwargs)
|
||||||
end
|
end
|
||||||
|
|
||||||
# :ditto:
|
# :ditto:
|
||||||
def ===(other : FormalArguments)
|
def ===(other : FormalArguments)
|
||||||
compare_named_tuples(args, other.args) && splat === other.splat && compare_named_tuples(kwargs, other.kwargs)
|
compare_named_tuples(args, other.args) && compare_tuples(splat, other.splat) && compare_named_tuples(kwargs, other.kwargs)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -37,13 +37,13 @@ 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
|
||||||
"#<LazyDouble #{@name || "Anonymous"}>"
|
@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("#{_spectator_stubbed_name} received unexpected message #{call}")
|
raise UnexpectedMessage.new("#{inspect} received unexpected message #{call}")
|
||||||
else
|
else
|
||||||
Log.trace { "Fallback for #{call} - call original" }
|
Log.trace { "Fallback for #{call} - call original" }
|
||||||
yield
|
yield
|
||||||
|
@ -57,7 +57,7 @@ module Spectator
|
||||||
%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 { "#{_spectator_stubbed_name} got undefined method `#{%call}{% if call.block %} { ... }{% end %}`" }
|
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)
|
||||||
|
|
|
@ -30,7 +30,13 @@ module Spectator
|
||||||
|
|
||||||
# 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 << arguments
|
io << '#' << method
|
||||||
|
arguments.inspect(io)
|
||||||
|
end
|
||||||
|
|
||||||
|
# :ditto:
|
||||||
|
def inspect(io : IO) : Nil
|
||||||
|
to_s(io)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
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"
|
||||||
|
@ -36,7 +37,35 @@ 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 %}
|
||||||
{{base.id}} {{type_name.id}} < {{mocked_type.id}}
|
{% 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}}
|
||||||
|
{% end %}
|
||||||
include ::Spectator::Mocked
|
include ::Spectator::Mocked
|
||||||
extend ::Spectator::StubbedType
|
extend ::Spectator::StubbedType
|
||||||
|
|
||||||
|
@ -50,22 +79,22 @@ module Spectator
|
||||||
end
|
end
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
def _spectator_remove_stub(stub : ::Spectator::Stub) : Nil
|
def _spectator_remove_stub(stub : ::Spectator::Stub) : ::Nil
|
||||||
@_spectator_stubs.try &.delete(stub)
|
@_spectator_stubs.try &.delete(stub)
|
||||||
end
|
end
|
||||||
|
|
||||||
def _spectator_clear_stubs : Nil
|
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 %}
|
||||||
|
@ -73,7 +102,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 %}
|
||||||
|
@ -120,7 +149,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}} ::{{type_name.id}}
|
{{base.id}} {{"::".id unless type_name.id.starts_with?("::")}}{{type_name.id}}
|
||||||
include ::Spectator::Mocked
|
include ::Spectator::Mocked
|
||||||
extend ::Spectator::StubbedType
|
extend ::Spectator::StubbedType
|
||||||
|
|
||||||
|
@ -129,12 +158,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 %}
|
||||||
{% raise "Unsupported base type #{base} for injecting mock" %}
|
@@_spectator_mock_registry = ::Spectator::MockRegistry.new
|
||||||
{% 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
|
||||||
|
@ -143,11 +172,11 @@ module Spectator
|
||||||
entry.stubs
|
entry.stubs
|
||||||
end
|
end
|
||||||
|
|
||||||
def _spectator_remove_stub(stub : ::Spectator::Stub) : Nil
|
def _spectator_remove_stub(stub : ::Spectator::Stub) : ::Nil
|
||||||
@@_spectator_mock_registry[self]?.try &.stubs.delete(stub)
|
@@_spectator_mock_registry[self]?.try &.stubs.delete(stub)
|
||||||
end
|
end
|
||||||
|
|
||||||
def _spectator_clear_stubs : Nil
|
def _spectator_clear_stubs : ::Nil
|
||||||
@@_spectator_mock_registry.delete(self)
|
@@_spectator_mock_registry.delete(self)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -169,7 +198,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 %}
|
||||||
|
@ -178,7 +207,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 %}
|
||||||
|
|
43
src/spectator/mocks/mock_registry.cr
Normal file
43
src/spectator/mocks/mock_registry.cr
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
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
|
|
@ -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("#{_spectator_stubbed_name} received unexpected message #{call}")
|
raise UnexpectedMessage.new("#{inspect} received unexpected message #{call}")
|
||||||
else
|
else
|
||||||
Log.trace { "Fallback for #{call} - return self" }
|
Log.trace { "Fallback for #{call} - return self" }
|
||||||
self
|
self
|
||||||
|
@ -42,9 +42,9 @@ 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("#{_spectator_stubbed_name} received unexpected message #{call}")
|
raise UnexpectedMessage.new("#{inspect} received unexpected message #{call}")
|
||||||
else
|
else
|
||||||
raise TypeCastError.new("#{_spectator_stubbed_name} received message #{call} and is attempting to return `self`, but returned type must be `#{type}`.")
|
raise TypeCastError.new("#{inspect} received message #{call} and is attempting to return `self`, but returned type must be `#{type}`.")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ module Spectator
|
||||||
%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 { "#{_spectator_stubbed_name} got undefined method `#{%call}{% if call.block %} { ... }{% end %}`" }
|
Log.trace { "#{inspect} got undefined method `#{%call}{% if call.block %} { ... }{% end %}`" }
|
||||||
|
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
|
@ -126,7 +126,41 @@ module Spectator
|
||||||
{{method.body}}
|
{{method.body}}
|
||||||
end
|
end
|
||||||
|
|
||||||
{% original = "previous_def#{" { |*_spectator_yargs| yield *_spectator_yargs }".id if method.accepts_block?}".id %}
|
{% original = "previous_def"
|
||||||
|
# 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).
|
||||||
|
@ -145,7 +179,7 @@ module Spectator
|
||||||
::NamedTuple.new(
|
::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 %}
|
{% for arg, i in method.args %}{% if !method.splat_index || i < method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %}
|
||||||
),
|
),
|
||||||
{% if method.splat_index && (splat = method.args[method.splat_index].internal_name) %}{{splat.symbolize}}, {{splat}},{% end %}
|
{% if method.splat_index && !(splat = method.args[method.splat_index].internal_name).empty? %}{{splat.symbolize}}, {{splat}},{% end %}
|
||||||
::NamedTuple.new(
|
::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 %}
|
{% 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}})
|
).merge({{method.double_splat}})
|
||||||
|
@ -158,10 +192,24 @@ 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 method.return_type && method.return_type.resolve == NoReturn
|
{{ if rt = method.return_type
|
||||||
:no_return
|
if rt.is_a?(Path) && (resolved = rt.resolve?).is_a?(TypeNode) && resolved <= NoReturn
|
||||||
elsif method.return_type && method.return_type.resolve <= Nil || method.return_type.is_a?(Union) && method.return_type.types.map(&.resolve).includes?(Nil)
|
:no_return
|
||||||
:nil
|
else
|
||||||
|
# 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
|
||||||
|
else
|
||||||
|
:raise
|
||||||
|
end
|
||||||
|
end
|
||||||
else
|
else
|
||||||
:raise
|
:raise
|
||||||
end }})
|
end }})
|
||||||
|
@ -227,7 +275,42 @@ module Spectator
|
||||||
{{method.body}}
|
{{method.body}}
|
||||||
end
|
end
|
||||||
|
|
||||||
{% original = "previous_def#{" { |*_spectator_yargs| yield *_spectator_yargs }".id if method.accepts_block?}".id %}
|
{% original = "previous_def"
|
||||||
|
# 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.
|
||||||
|
@ -247,7 +330,7 @@ module Spectator
|
||||||
::NamedTuple.new(
|
::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 %}
|
{% for arg, i in method.args %}{% if !method.splat_index || i < method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %}
|
||||||
),
|
),
|
||||||
{% if method.splat_index && (splat = method.args[method.splat_index].internal_name) %}{{splat.symbolize}}, {{splat}},{% end %}
|
{% if method.splat_index && !(splat = method.args[method.splat_index].internal_name).empty? %}{{splat.symbolize}}, {{splat}},{% end %}
|
||||||
::NamedTuple.new(
|
::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 %}
|
{% 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}})
|
).merge({{method.double_splat}})
|
||||||
|
@ -259,15 +342,25 @@ 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 method.return_type %}
|
{% if rt = 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 method.return_type.resolve == NoReturn
|
{{ if rt.is_a?(Path) && (resolved = rt.resolve?).is_a?(TypeNode) && resolved <= NoReturn
|
||||||
:no_return
|
:no_return
|
||||||
elsif method.return_type.resolve <= Nil || method.return_type.is_a?(Union) && method.return_type.types.map(&.resolve).includes?(Nil)
|
|
||||||
:nil
|
|
||||||
else
|
else
|
||||||
:raise
|
# 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
|
||||||
|
else
|
||||||
|
: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.
|
||||||
|
@ -338,64 +431,93 @@ 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
|
||||||
# Reverse order of ancestors (there's currently no reverse method for ArrayLiteral).
|
definitions = [] of Nil
|
||||||
count = type.ancestors.size
|
scope = if type == @type
|
||||||
ancestors = type.ancestors.map_with_index { |_, i| type.ancestors[count - i - 1] } %}
|
:previous_def
|
||||||
{% for ancestor in ancestors %}
|
elsif type.module?
|
||||||
{% for method in ancestor.methods.reject do |meth|
|
type.name
|
||||||
meth.name.starts_with?("_spectator") ||
|
else
|
||||||
::Spectator::DSL::RESERVED_KEYWORDS.includes?(meth.name.symbolize)
|
:super
|
||||||
end %}
|
end.id
|
||||||
{{(method.abstract? ? :abstract_stub : :default_stub).id}} {{method.visibility.id if method.visibility != :public}} def {{"#{method.receiver}.".id if method.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 %}
|
|
||||||
|
|
||||||
{% for method in ancestor.class.methods.reject do |meth|
|
# Add entries for methods in the target type and its class type.
|
||||||
meth.name.starts_with?("_spectator") ||
|
[[:self.id, type.class], [nil, type]].each do |(receiver, t)|
|
||||||
::Spectator::DSL::RESERVED_KEYWORDS.includes?(meth.name.symbolize)
|
t.methods.each do |method|
|
||||||
end %}
|
definitions << {
|
||||||
default_stub {{method.visibility.id if method.visibility != :public}} def self.{{method.name}}(
|
type: t,
|
||||||
{% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %}
|
method: method,
|
||||||
{% if method.double_splat %}**{{method.double_splat}}, {% end %}
|
scope: scope,
|
||||||
{% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %}
|
receiver: receiver,
|
||||||
){% 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
|
||||||
{% end %}
|
|
||||||
{% end %}
|
|
||||||
|
|
||||||
{% for method in type.methods.reject do |meth|
|
# Iterate through all ancestors and add their methods.
|
||||||
meth.name.starts_with?("_spectator") ||
|
type.ancestors.each do |ancestor|
|
||||||
::Spectator::DSL::RESERVED_KEYWORDS.includes?(meth.name.symbolize)
|
[[:self.id, ancestor.class], [nil, ancestor]].each do |(receiver, t)|
|
||||||
end %}
|
t.methods.each do |method|
|
||||||
{{(method.abstract? ? :"abstract_stub abstract" : :default_stub).id}} {{method.visibility.id if method.visibility != :public}} def {{"#{method.receiver}.".id if method.receiver}}{{method.name}}(
|
# 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 %}
|
||||||
|
|
||||||
|
{% for definition in definitions %}
|
||||||
|
{% original_type = definition[:type]
|
||||||
|
method = definition[:method]
|
||||||
|
scope = definition[:scope]
|
||||||
|
receiver = definition[:receiver]
|
||||||
|
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 %}
|
||||||
|
# Redefinition of {{original_type}}{{"#".id}}{{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 %}
|
{% 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? %}
|
||||||
{% if type == @type %}previous_def{% else %}super{% end %}{% if method.accepts_block? %} { |*%yargs| yield *%yargs }{% end %}
|
{{scope}}{% if rewrite_args %}({% for arg, i in method.args %}
|
||||||
|
{% 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 its return value) to the correct type.
|
||||||
|
@ -415,29 +537,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}})
|
%cast = %value.as?({{type}})
|
||||||
if %cast.is_a?({{type}})
|
|
||||||
|
{% if fail_cast == :nil %}
|
||||||
%cast
|
%cast
|
||||||
else
|
{% elsif fail_cast == :raise %}
|
||||||
{% if fail_cast == :nil %}
|
# Check if nil was returned by the stub and if its okay to return it.
|
||||||
nil
|
if %value.nil? && %type.nilable?
|
||||||
{% elsif fail_cast == :raise %}
|
# 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.
|
# 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.
|
raise TypeCastError.new("#{_spectator_stubbed_name} received message #{ {{call}} } and is attempting to return a `#{%value.class}`, but returned type must be `#{%type}`.")
|
||||||
%type = begin
|
else
|
||||||
%value.class.to_s
|
# Types match and value can be returned as cast type.
|
||||||
rescue
|
%cast
|
||||||
"<Unknown>"
|
end
|
||||||
end
|
{% else %}
|
||||||
raise TypeCastError.new("#{_spectator_stubbed_name} received message #{ {{call}} } and is attempting to return a `#{%type}`, but returned type must be `#{ {{type}} }`.")
|
{% raise "fail_cast must be :nil, :raise, or :no_return, but got: #{fail_cast}" %}
|
||||||
{% else %}
|
{% end %}
|
||||||
{% raise "fail_cast must be :nil, :raise, or :no_return, but got: #{fail_cast}" %}
|
|
||||||
{% end %}
|
|
||||||
end
|
|
||||||
{% end %}
|
{% end %}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -30,14 +30,16 @@ module Spectator
|
||||||
end
|
end
|
||||||
|
|
||||||
# User-defined tags and values used for filtering and behavior modification.
|
# User-defined tags and values used for filtering and behavior modification.
|
||||||
getter metadata : Metadata
|
def metadata : Metadata
|
||||||
|
@metadata ||= Metadata.new
|
||||||
|
end
|
||||||
|
|
||||||
# Creates the node.
|
# Creates the node.
|
||||||
# The *name* describes the purpose of the node.
|
# The *name* describes the purpose of the node.
|
||||||
# It can be a `Symbol` to describe a type.
|
# It can be a `Symbol` to describe a type.
|
||||||
# The *location* tracks where the node exists in source code.
|
# The *location* tracks where the node exists in source code.
|
||||||
# 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, @metadata : Metadata = Metadata.new)
|
def initialize(@name : Label = nil, @location : Location? = nil, @metadata : Metadata? = nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Indicates whether the node has completed.
|
# Indicates whether the node has completed.
|
||||||
|
@ -46,17 +48,25 @@ module Spectator
|
||||||
# Checks if the node has been marked as pending.
|
# Checks if the node has been marked as pending.
|
||||||
# Pending items should be skipped during execution.
|
# Pending items should be skipped during execution.
|
||||||
def pending?
|
def pending?
|
||||||
metadata.has_key?(:pending) || metadata.has_key?(:skip)
|
return false unless md = @metadata
|
||||||
|
|
||||||
|
md.has_key?(:pending) || md.has_key?(:skip)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Gets the reason the node has been marked as pending.
|
# Gets the reason the node has been marked as pending.
|
||||||
def pending_reason
|
def pending_reason
|
||||||
metadata[:pending]? || metadata[:skip]? || metadata[:reason]? || DEFAULT_PENDING_REASON
|
return DEFAULT_PENDING_REASON unless md = @metadata
|
||||||
|
|
||||||
|
md[:pending]? || md[:skip]? || md[:reason]? || DEFAULT_PENDING_REASON
|
||||||
end
|
end
|
||||||
|
|
||||||
# Retrieves just the tag names applied to the node.
|
# Retrieves just the tag names applied to the node.
|
||||||
def tags
|
def tags
|
||||||
Tags.new(metadata.keys)
|
if md = @metadata
|
||||||
|
Tags.new(md.keys)
|
||||||
|
else
|
||||||
|
Tags.new
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Non-nil name used to show the node name.
|
# Non-nil name used to show the node name.
|
||||||
|
|
|
@ -9,7 +9,7 @@ module Spectator
|
||||||
end
|
end
|
||||||
|
|
||||||
# Calls the `pass` method on *visitor*.
|
# Calls the `pass` method on *visitor*.
|
||||||
def accept(visitor)
|
def accept(visitor, &)
|
||||||
visitor.pass(yield self)
|
visitor.pass(yield self)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ module Spectator
|
||||||
# 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`.
|
||||||
# A default *reason* can be given in case the user didn't provide one.
|
# A default *reason* can be given in case the user didn't provide one.
|
||||||
def initialize(@name : String? = nil, @location : Location? = nil,
|
def initialize(@name : String? = nil, @location : Location? = nil,
|
||||||
@metadata : Metadata = Metadata.new, @reason : String? = nil)
|
@metadata : Metadata? = nil, @reason : String? = nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Constructs an example with previously defined attributes.
|
# Constructs an example with previously defined attributes.
|
||||||
|
|
|
@ -28,7 +28,7 @@ module Spectator
|
||||||
end
|
end
|
||||||
|
|
||||||
# Calls the `pending` method on the *visitor*.
|
# Calls the `pending` method on the *visitor*.
|
||||||
def accept(visitor)
|
def accept(visitor, &)
|
||||||
visitor.pending(yield self)
|
visitor.pending(yield self)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -22,51 +22,106 @@ class Object
|
||||||
# ```
|
# ```
|
||||||
# require "spectator/should"
|
# require "spectator/should"
|
||||||
# ```
|
# ```
|
||||||
def should(matcher, message = nil)
|
def should(matcher, message = nil, *, _file = __FILE__, _line = __LINE__)
|
||||||
actual = ::Spectator::Value.new(self)
|
actual = ::Spectator::Value.new(self)
|
||||||
|
location = ::Spectator::Location.new(_file, _line)
|
||||||
match_data = matcher.match(actual)
|
match_data = matcher.match(actual)
|
||||||
expectation = ::Spectator::Expectation.new(match_data, message: message)
|
expectation = ::Spectator::Expectation.new(match_data, location, message)
|
||||||
::Spectator::Harness.current.report(expectation)
|
::Spectator::Harness.current.report(expectation)
|
||||||
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 should(matcher : ::Spectator::Matchers::TypeMatcher(U), message = nil, *, _file = __FILE__, _line = __LINE__) forall U
|
||||||
|
actual = ::Spectator::Value.new(self)
|
||||||
|
location = ::Spectator::Location.new(_file, _line)
|
||||||
|
match_data = matcher.match(actual)
|
||||||
|
expectation = ::Spectator::Expectation.new(match_data, location, message)
|
||||||
|
if ::Spectator::Harness.current.report(expectation)
|
||||||
|
return self if self.is_a?(U)
|
||||||
|
|
||||||
|
raise "Spectator bug: expected value should have cast to #{U}"
|
||||||
|
else
|
||||||
|
raise TypeCastError.new("Expected value should be a #{U}, but was actually #{self.class}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Works the same as `#should` except the condition is inverted.
|
# Works the same as `#should` except the condition is inverted.
|
||||||
# When `#should` succeeds, this method will fail, and vice-versa.
|
# When `#should` succeeds, this method will fail, and vice-versa.
|
||||||
def should_not(matcher, message = nil)
|
def should_not(matcher, message = nil, *, _file = __FILE__, _line = __LINE__)
|
||||||
actual = ::Spectator::Value.new(self)
|
actual = ::Spectator::Value.new(self)
|
||||||
|
location = ::Spectator::Location.new(_file, _line)
|
||||||
match_data = matcher.negated_match(actual)
|
match_data = matcher.negated_match(actual)
|
||||||
expectation = ::Spectator::Expectation.new(match_data, message: message)
|
expectation = ::Spectator::Expectation.new(match_data, location, message)
|
||||||
::Spectator::Harness.current.report(expectation)
|
::Spectator::Harness.current.report(expectation)
|
||||||
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 should_not(matcher : ::Spectator::Matchers::TypeMatcher(U), message = nil, *, _file = __FILE__, _line = __LINE__) forall U
|
||||||
|
actual = ::Spectator::Value.new(self)
|
||||||
|
location = ::Spectator::Location.new(_file, _line)
|
||||||
|
match_data = matcher.negated_match(actual)
|
||||||
|
expectation = ::Spectator::Expectation.new(match_data, location, message)
|
||||||
|
if ::Spectator::Harness.current.report(expectation)
|
||||||
|
return self unless self.is_a?(U)
|
||||||
|
|
||||||
|
raise "Spectator bug: expected value should not be #{U}"
|
||||||
|
else
|
||||||
|
raise TypeCastError.new("Expected value is not expected to be a #{U}, but was actually #{self.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 should_not(matcher : ::Spectator::Matchers::NilMatcher, message = nil, *, _file = __FILE__, _line = __LINE__)
|
||||||
|
actual = ::Spectator::Value.new(self)
|
||||||
|
location = ::Spectator::Location.new(_file, _line)
|
||||||
|
match_data = matcher.negated_match(actual)
|
||||||
|
expectation = ::Spectator::Expectation.new(match_data, location, message)
|
||||||
|
if ::Spectator::Harness.current.report(expectation)
|
||||||
|
return self unless self.nil?
|
||||||
|
|
||||||
|
raise "Spectator bug: expected value should not be nil"
|
||||||
|
else
|
||||||
|
raise NilAssertionError.new("Expected value should not be nil.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Works the same as `#should` except that the condition check is postponed.
|
# Works the same as `#should` except that the condition check is postponed.
|
||||||
# The expectation is checked after the example finishes and all hooks have run.
|
# The expectation is checked after the example finishes and all hooks have run.
|
||||||
def should_eventually(matcher, message = nil)
|
def should_eventually(matcher, message = nil, *, _file = __FILE__, _line = __LINE__)
|
||||||
::Spectator::Harness.current.defer { should(matcher, message) }
|
::Spectator::Harness.current.defer { should(matcher, message, _file: _file, _line: _line) }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Works the same as `#should_not` except that the condition check is postponed.
|
# Works the same as `#should_not` except that the condition check is postponed.
|
||||||
# The expectation is checked after the example finishes and all hooks have run.
|
# The expectation is checked after the example finishes and all hooks have run.
|
||||||
def should_never(matcher, message = nil)
|
def should_never(matcher, message = nil, *, _file = __FILE__, _line = __LINE__)
|
||||||
::Spectator::Harness.current.defer { should_not(matcher, message) }
|
::Spectator::Harness.current.defer { should_not(matcher, message, _file: _file, _line: _line) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
struct Proc(*T, R)
|
struct Proc(*T, R)
|
||||||
# Extension method to create an expectation for a block of code (proc).
|
# Extension method to create an expectation for a block of code (proc).
|
||||||
# Depending on the matcher, the proc may be executed multiple times.
|
# Depending on the matcher, the proc may be executed multiple times.
|
||||||
def should(matcher, message = nil)
|
def should(matcher, message = nil, *, _file = __FILE__, _line = __LINE__)
|
||||||
actual = ::Spectator::Block.new(self)
|
actual = ::Spectator::Block.new(self)
|
||||||
|
location = ::Spectator::Location.new(_file, _line)
|
||||||
match_data = matcher.match(actual)
|
match_data = matcher.match(actual)
|
||||||
expectation = ::Spectator::Expectation.new(match_data, message: message)
|
expectation = ::Spectator::Expectation.new(match_data, location, message)
|
||||||
::Spectator::Harness.current.report(expectation)
|
::Spectator::Harness.current.report(expectation)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Works the same as `#should` except the condition is inverted.
|
# Works the same as `#should` except the condition is inverted.
|
||||||
# When `#should` succeeds, this method will fail, and vice-versa.
|
# When `#should` succeeds, this method will fail, and vice-versa.
|
||||||
def should_not(matcher, message = nil)
|
def should_not(matcher, message = nil, *, _file = __FILE__, _line = __LINE__)
|
||||||
actual = ::Spectator::Block.new(self)
|
actual = ::Spectator::Block.new(self)
|
||||||
|
location = ::Spectator::Location.new(_file, _line)
|
||||||
match_data = matcher.negated_match(actual)
|
match_data = matcher.negated_match(actual)
|
||||||
expectation = ::Spectator::Expectation.new(match_data, message: message)
|
expectation = ::Spectator::Expectation.new(match_data, location, message)
|
||||||
::Spectator::Harness.current.report(expectation)
|
::Spectator::Harness.current.report(expectation)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -60,7 +60,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.
|
||||||
# For instance, adding a "pending" tag will mark tests as pending and skip execution.
|
# For instance, adding a "pending" tag will mark tests as pending and skip execution.
|
||||||
def start_group(name, location = nil, metadata = Metadata.new) : Nil
|
def start_group(name, location = nil, metadata = nil) : Nil
|
||||||
Log.trace { "Start group: #{name.inspect} @ #{location}; metadata: #{metadata}" }
|
Log.trace { "Start group: #{name.inspect} @ #{location}; metadata: #{metadata}" }
|
||||||
builder = ExampleGroupBuilder.new(name, location, metadata)
|
builder = ExampleGroupBuilder.new(name, location, metadata)
|
||||||
|
|
||||||
|
@ -86,7 +86,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.
|
||||||
# For instance, adding a "pending" tag will mark tests as pending and skip execution.
|
# For instance, adding a "pending" tag will mark tests as pending and skip execution.
|
||||||
def start_iterative_group(collection, name, iterator = nil, location = nil, metadata = Metadata.new) : Nil
|
def start_iterative_group(collection, name, iterator = nil, location = nil, metadata = nil) : Nil
|
||||||
Log.trace { "Start iterative group: #{name} (#{typeof(collection)}) @ #{location}; metadata: #{metadata}" }
|
Log.trace { "Start iterative group: #{name} (#{typeof(collection)}) @ #{location}; metadata: #{metadata}" }
|
||||||
builder = IterativeExampleGroupBuilder.new(collection, name, iterator, location, metadata)
|
builder = IterativeExampleGroupBuilder.new(collection, name, iterator, location, metadata)
|
||||||
|
|
||||||
|
@ -127,7 +127,7 @@ module Spectator
|
||||||
# It will be yielded two arguments - the example created by this method, and the *context* argument.
|
# It will be yielded two arguments - the example created by this method, and the *context* argument.
|
||||||
# The return value of the block is ignored.
|
# The return value of the block is ignored.
|
||||||
# It is expected that the test code runs when the block is called.
|
# It is expected that the test code runs when the block is called.
|
||||||
def add_example(name, location, context_builder, metadata = Metadata.new, &block : Example -> _) : Nil
|
def add_example(name, location, context_builder, metadata = nil, &block : Example -> _) : Nil
|
||||||
Log.trace { "Add example: #{name} @ #{location}; metadata: #{metadata}" }
|
Log.trace { "Add example: #{name} @ #{location}; metadata: #{metadata}" }
|
||||||
current << ExampleBuilder.new(context_builder, block, name, location, metadata)
|
current << ExampleBuilder.new(context_builder, block, name, location, metadata)
|
||||||
end
|
end
|
||||||
|
@ -144,7 +144,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.
|
||||||
# For instance, adding a "pending" tag will mark the test as pending and skip execution.
|
# For instance, adding a "pending" tag will mark the test as pending and skip execution.
|
||||||
# A default *reason* can be given in case the user didn't provide one.
|
# A default *reason* can be given in case the user didn't provide one.
|
||||||
def add_pending_example(name, location, metadata = Metadata.new, reason = nil) : Nil
|
def add_pending_example(name, location, metadata = nil, reason = nil) : Nil
|
||||||
Log.trace { "Add pending example: #{name} @ #{location}; metadata: #{metadata}" }
|
Log.trace { "Add pending example: #{name} @ #{location}; metadata: #{metadata}" }
|
||||||
current << PendingExampleBuilder.new(name, location, metadata, reason)
|
current << PendingExampleBuilder.new(name, location, metadata, reason)
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,7 +10,9 @@ module Spectator
|
||||||
|
|
||||||
# Checks whether the node satisfies the filter.
|
# Checks whether the node satisfies the filter.
|
||||||
def includes?(node) : Bool
|
def includes?(node) : Bool
|
||||||
node.metadata.any? { |key, value| key.to_s == @tag && (!@value || value == @value) }
|
return false unless metadata = node.metadata
|
||||||
|
|
||||||
|
metadata.any? { |key, value| key.to_s == @tag && (!@value || value == @value) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -34,7 +34,7 @@ class SpectatorTestContext < SpectatorContext
|
||||||
|
|
||||||
# Initial metadata for tests.
|
# Initial metadata for tests.
|
||||||
# This method should be overridden by example groups and examples.
|
# This method should be overridden by example groups and examples.
|
||||||
private def self.metadata
|
private def self.metadata : ::Spectator::Metadata?
|
||||||
::Spectator::Metadata.new
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,18 +13,13 @@ module Spectator
|
||||||
|
|
||||||
# Creates a wrapper for the specified value.
|
# Creates a wrapper for the specified value.
|
||||||
def initialize(value)
|
def initialize(value)
|
||||||
@pointer = Box.box(value)
|
@pointer = Value.new(value).as(Void*)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Retrieves the previously wrapped value.
|
# Retrieves the previously wrapped value.
|
||||||
# The *type* of the wrapped value must match otherwise an error will be raised.
|
# The *type* of the wrapped value must match otherwise an error will be raised.
|
||||||
def get(type : T.class) : T forall T
|
def get(type : T.class) : T forall T
|
||||||
{% begin %}
|
@pointer.unsafe_as(Value(T)).get
|
||||||
{% if T.nilable? %}
|
|
||||||
@pointer.null? ? nil :
|
|
||||||
{% end %}
|
|
||||||
Box(T).unbox(@pointer)
|
|
||||||
{% end %}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Retrieves the previously wrapped value.
|
# Retrieves the previously wrapped value.
|
||||||
|
@ -39,12 +34,20 @@ module Spectator
|
||||||
# type = wrapper.get { Int32 } # Returns Int32
|
# type = wrapper.get { Int32 } # Returns Int32
|
||||||
# ```
|
# ```
|
||||||
def get(& : -> T) : T forall T
|
def get(& : -> T) : T forall T
|
||||||
{% begin %}
|
@pointer.unsafe_as(Value(T)).get
|
||||||
{% if T.nilable? %}
|
end
|
||||||
@pointer.null? ? nil :
|
|
||||||
{% end %}
|
# Wrapper for a value.
|
||||||
Box(T).unbox(@pointer)
|
# Similar to `Box`, but doesn't segfault on nil and unions.
|
||||||
{% end %}
|
private class Value(T)
|
||||||
|
# Creates the wrapper.
|
||||||
|
def initialize(@value : T)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Retrieves the value.
|
||||||
|
def get : T
|
||||||
|
@value
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
7
util/nightly.sh
Executable file
7
util/nightly.sh
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
readonly image=crystallang/crystal:nightly
|
||||||
|
readonly code=/project
|
||||||
|
|
||||||
|
docker run -it -v "$PWD:${code}" -w "${code}" "${image}" crystal spec "$@"
|
5
util/test-all-individually.sh
Executable file
5
util/test-all-individually.sh
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
find spec/ -type f -name \*_spec.cr -print0 | \
|
||||||
|
xargs -0 -n1 crystal spec --error-on-warnings -v
|
Loading…
Add table
Add a link
Reference in a new issue