diff --git a/.gitignore b/.gitignore index f76b510..c4166ba 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,3 @@ # Ignore JUnit output output.xml - -/test.cr diff --git a/CHANGELOG.md b/CHANGELOG.md index f2d8d66..88b37de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,54 +4,7 @@ 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/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.12.1] - 2024-08-13 -### Fixed -- Fixed some global namespace issues with Crystal 1.13. [#57](https://github.com/icy-arctic-fox/spectator/pull/57) Thanks @GrantBirki ! -- Remove usage of deprecated double splat in macros. - -## [0.12.0] - 2024-02-03 -### Added -- Added ability to use matchers for case equality. [#55](https://github.com/icy-arctic-fox/spectator/issues/55) -- Added support for nested case equality when checking arguments with Array, Tuple, Hash, and NamedTuple. - -### Fixed -- Fixed some issues with the `be_within` matcher when used with expected and union types. - -## [0.11.7] - 2023-10-16 -### Fixed -- Fix memoized value (`let`) with a union type causing segfault. [#81](https://gitlab.com/arctic-fox/spectator/-/issues/81) - -## [0.11.6] - 2023-01-26 -### Added -- Added ability to cast types using the return value from expect/should statements with a type matcher. -- Added support for string interpolation in context names/labels. - -### Fixed -- Fix invalid syntax (unterminated call) when recording calls to stubs with an un-named splat. [#51](https://github.com/icy-arctic-fox/spectator/issues/51) -- Fix malformed method signature when using named splat with keyword arguments in mocked type. [#49](https://github.com/icy-arctic-fox/spectator/issues/49) - -### Changed -- Expectations using 'should' syntax report file and line where the 'should' keyword is instead of the test start. -- Add non-captured block argument in preparation for Crystal 1.8.0. - -## [0.11.5] - 2022-12-18 -### Added -- Added support for mock modules and types that include mocked modules. - -### Fixed -- Fix macro logic to support free variables, 'self', and variants on stubbed methods. [#48](https://github.com/icy-arctic-fox/spectator/issues/48) -- Fix method stubs used on methods that capture blocks. -- Fix type name resolution for when using custom types in a mocked typed. -- Prevent comparing range arguments with non-compatible types in stubs. [#48](https://github.com/icy-arctic-fox/spectator/issues/48) - -### Changed -- Simplify string representation of mock-related types. -- Remove unnecessary redefinitions of methods when adding stub functionality to a type. -- Allow metadata to be stored as nil to reduce overhead when tracking nodes without tags. -- Use normal equality (==) instead of case-equality (===) with proc arguments in stubs. -- Change stub value cast logic to avoid compiler bug. [#80](https://gitlab.com/arctic-fox/spectator/-/issues/80) - -## [0.11.4] - 2022-11-27 +## [0.11.4] ### Added - Add support for using named (keyword) arguments in place of positional arguments in stubs. [#47](https://github.com/icy-arctic-fox/spectator/issues/47) - Add `before`, `after`, and `around` as aliases for `before_each`, `after_each`, and `around_each` respectively. @@ -463,13 +416,8 @@ This has been changed so that it compiles and raises an error at runtime with a First version ready for public use. -[Unreleased]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.12.1...master -[0.12.1]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.12.0...v0.12.1 -[0.12.0]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.7...v0.12.0 -[0.11.7]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.6...v0.11.7 -[0.11.6]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.5...v0.11.6 -[0.11.5]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.4...v0.11.5 -[0.11.4]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.3...v0.11.4 +[Unreleased]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.4...master +[0.11.3]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.3...v0.11.4 [0.11.3]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.2...v0.11.3 [0.11.2]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.1...v0.11.2 [0.11.1]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.0...v0.11.1 diff --git a/README.md b/README.md index 00b5eb9..973b9a9 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Add this to your application's `shard.yml`: development_dependencies: spectator: gitlab: arctic-fox/spectator - version: ~> 0.12.0 + version: ~> 0.11.0 ``` Usage @@ -287,7 +287,7 @@ Spectator.describe Driver do # Call the mock method. subject.do_something(interface, dbl) # Verify everything went okay. - expect(interface).to have_received(:invoke).with(dbl) + expect(interface).to have_received(:invoke).with(thing) end end ``` diff --git a/shard.yml b/shard.yml index ac8b54f..15c9a65 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: spectator -version: 0.12.1 +version: 0.11.4 description: | Feature-rich testing framework for Crystal inspired by RSpec. diff --git a/spec/docs/mocks_spec.cr b/spec/docs/mocks_spec.cr index 58c16f8..5908787 100644 --- a/spec/docs/mocks_spec.cr +++ b/spec/docs/mocks_spec.cr @@ -123,109 +123,6 @@ Spectator.describe "Mocks Docs" do 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 struct MyStruct def something diff --git a/spec/docs/readme_spec.cr b/spec/docs/readme_spec.cr index 2ed7fd5..6024906 100644 --- a/spec/docs/readme_spec.cr +++ b/spec/docs/readme_spec.cr @@ -1,28 +1,26 @@ require "../spec_helper" -module Readme - abstract class Interface - abstract def invoke(thing) : String - end +private abstract class Interface + abstract def invoke(thing) : String +end - # Type being tested. - class Driver - def do_something(interface : Interface, thing) - interface.invoke(thing) - end +# Type being tested. +private class Driver + def do_something(interface : Interface, thing) + interface.invoke(thing) end end -Spectator.describe Readme::Driver do +Spectator.describe Driver do # Define a mock for Interface. - mock Readme::Interface + mock Interface # Define a double that the interface will use. double(:my_double, foo: 42) it "does a thing" do # Create an instance of the mock interface. - interface = mock(Readme::Interface) + interface = mock(Interface) # Indicate that `#invoke` should return "test" when called. allow(interface).to receive(:invoke).and_return("test") diff --git a/spec/features/expect_type_spec.cr b/spec/features/expect_type_spec.cr deleted file mode 100644 index 6ed9949..0000000 --- a/spec/features/expect_type_spec.cr +++ /dev/null @@ -1,70 +0,0 @@ -require "../spec_helper" - -Spectator.describe "Expect Type", :smoke do - context "with expect syntax" do - it "ensures a type is cast" do - value = 42.as(String | Int32) - expect(value).to be_a(String | Int32) - expect(value).to compile_as(String | Int32) - value = expect(value).to be_a(Int32) - expect(value).to eq(42) - expect(value).to be_a(Int32) - expect(value).to compile_as(Int32) - expect(value).to_not respond_to(:downcase) - end - - it "ensures a type is not nil" do - value = 42.as(Int32?) - expect(value).to be_a(Int32?) - expect(value).to compile_as(Int32?) - value = expect(value).to_not be_nil - expect(value).to eq(42) - expect(value).to be_a(Int32) - expect(value).to compile_as(Int32) - expect { value.not_nil! }.to_not raise_error(NilAssertionError) - end - - it "removes types from a union" do - value = 42.as(String | Int32) - expect(value).to be_a(String | Int32) - expect(value).to compile_as(String | Int32) - value = expect(value).to_not be_a(String) - expect(value).to eq(42) - expect(value).to be_a(Int32) - expect(value).to compile_as(Int32) - expect(value).to_not respond_to(:downcase) - end - end - - context "with should syntax" do - it "ensures a type is cast" do - value = 42.as(String | Int32) - value.should be_a(String | Int32) - value = value.should be_a(Int32) - value.should eq(42) - value.should be_a(Int32) - value.should compile_as(Int32) - value.should_not respond_to(:downcase) - end - - it "ensures a type is not nil" do - value = 42.as(Int32?) - value.should be_a(Int32?) - value = value.should_not be_nil - value.should eq(42) - value.should be_a(Int32) - value.should compile_as(Int32) - expect { value.not_nil! }.to_not raise_error(NilAssertionError) - end - - it "removes types from a union" do - value = 42.as(String | Int32) - value.should be_a(String | Int32) - value = value.should_not be_a(String) - value.should eq(42) - value.should be_a(Int32) - value.should compile_as(Int32) - value.should_not respond_to(:downcase) - end - end -end diff --git a/spec/features/interpolated_label_spec.cr b/spec/features/interpolated_label_spec.cr deleted file mode 100644 index add8e93..0000000 --- a/spec/features/interpolated_label_spec.cr +++ /dev/null @@ -1,22 +0,0 @@ -require "../spec_helper" - -Spectator.describe "Interpolated Label", :smoke do - let(foo) { "example" } - let(bar) { "context" } - - it "interpolates #{foo} labels" do |example| - expect(example.name).to eq("interpolates example labels") - end - - context "within a #{bar}" do - let(foo) { "multiple" } - - it "interpolates context labels" do |example| - expect(example.group.name).to eq("within a context") - end - - it "interpolates #{foo} levels" do |example| - expect(example.name).to eq("interpolates multiple levels") - end - end -end diff --git a/spec/issues/github_issue_44_spec.cr b/spec/issues/github_issue_44_spec.cr index da6cbf3..d1b9716 100644 --- a/spec/issues/github_issue_44_spec.cr +++ b/spec/issues/github_issue_44_spec.cr @@ -26,15 +26,12 @@ Spectator.describe "GitHub Issue #44" do # Original issue uses keyword arguments in place of positional arguments. context "keyword arguments in place of positional arguments" do before_each do - pipe = Process::Redirect::Pipe - expect(Process).to receive(:run).with(command, shell: true, output: pipe).and_raise(exception) + expect(Process).to receive(:run).with(command, shell: true, output: :pipe).and_raise(exception) end - it "must stub Process.run" do - expect do - Process.run(command, shell: true, output: :pipe) do |_process| - end - end.to raise_error(File::NotFoundError, "File not found") + it "must stub Process.run", skip: "Keyword arguments in place of positional arguments not supported with expect-receive" do + Process.run(command, shell: true, output: :pipe) do |_process| + end end end end diff --git a/spec/issues/github_issue_48_spec.cr b/spec/issues/github_issue_48_spec.cr deleted file mode 100644 index b958c1b..0000000 --- a/spec/issues/github_issue_48_spec.cr +++ /dev/null @@ -1,135 +0,0 @@ -require "../spec_helper" - -Spectator.describe "GitHub Issue #48" do - class Test - def return_this(thing : T) : T forall T - thing - end - - def map(thing : T, & : T -> U) : U forall T, U - yield thing - end - - def make_nilable(thing : T) : T? forall T - thing.as(T?) - end - - def itself : self - self - end - - def itself? : self? - self.as(self?) - end - - def generic(thing : T) : Array(T) forall T - Array.new(100) { thing } - end - - def union : Int32 | String - 42.as(Int32 | String) - end - - def capture(&block : -> T) forall T - block - end - - def capture(thing : T, &block : T -> T) forall T - block.call(thing) - block - end - - def range(r : Range) - r - end - end - - mock Test, make_nilable: nil - - let(fake) { mock(Test) } - - it "handles free variables" do - allow(fake).to receive(:return_this).and_return("different") - expect(fake.return_this("test")).to eq("different") - end - - it "raises on type cast error with free variables" do - allow(fake).to receive(:return_this).and_return(42) - expect { fake.return_this("test") }.to raise_error(TypeCastError, /String/) - end - - it "handles free variables with a block" do - allow(fake).to receive(:map).and_return("stub") - expect(fake.map(:mapped, &.to_s)).to eq("stub") - end - - it "raises on type cast error with a block and free variables" do - allow(fake).to receive(:map).and_return(42) - expect { fake.map(:mapped, &.to_s) }.to raise_error(TypeCastError, /String/) - end - - it "handles nilable free variables" do - expect(fake.make_nilable("foo")).to be_nil - end - - it "handles 'self' return type" do - not_self = mock(Test) - allow(fake).to receive(:itself).and_return(not_self) - expect(fake.itself).to be(not_self) - end - - it "raises on type cast error with 'self' return type" do - allow(fake).to receive(:itself).and_return(42) - expect { fake.itself }.to raise_error(TypeCastError, /#{class_mock(Test)}/) - end - - it "handles nilable 'self' return type" do - not_self = mock(Test) - allow(fake).to receive(:itself?).and_return(not_self) - expect(fake.itself?).to be(not_self) - end - - it "handles generic return type" do - allow(fake).to receive(:generic).and_return([42]) - expect(fake.generic(42)).to eq([42]) - end - - it "raises on type cast error with generic return type" do - allow(fake).to receive(:generic).and_return("test") - expect { fake.generic(42) }.to raise_error(TypeCastError, /Array\(Int32\)/) - end - - it "handles union return types" do - allow(fake).to receive(:union).and_return("test") - expect(fake.union).to eq("test") - end - - it "raises on type cast error with union return type" do - allow(fake).to receive(:union).and_return(:test) - expect { fake.union }.to raise_error(TypeCastError, /Symbol/) - end - - it "handles captured blocks" do - proc = ->{} - allow(fake).to receive(:capture).and_return(proc) - expect(fake.capture { nil }).to be(proc) - end - - it "raises on type cast error with captured blocks" do - proc = ->{ 42 } - allow(fake).to receive(:capture).and_return(proc) - expect { fake.capture { "other" } }.to raise_error(TypeCastError, /Proc\(String\)/) - end - - it "handles captured blocks with arguments" do - proc = ->(x : Int32) { x * 2 } - allow(fake).to receive(:capture).and_return(proc) - expect(fake.capture(5) { 5 }).to be(proc) - end - - it "handles range comparisons against non-comparable types" do - range = 1..10 - allow(fake).to receive(:range).and_return(range) - expect(fake.range(1..3)).to eq(range) - end -end diff --git a/spec/issues/github_issue_49_spec.cr b/spec/issues/github_issue_49_spec.cr deleted file mode 100644 index 6161a57..0000000 --- a/spec/issues/github_issue_49_spec.cr +++ /dev/null @@ -1,6 +0,0 @@ -require "../spec_helper" - -# https://github.com/icy-arctic-fox/spectator/issues/49 -Spectator.describe "GitHub Issue #49" do - # mock File -end diff --git a/spec/issues/github_issue_55_spec.cr b/spec/issues/github_issue_55_spec.cr deleted file mode 100644 index 92c2b42..0000000 --- a/spec/issues/github_issue_55_spec.cr +++ /dev/null @@ -1,48 +0,0 @@ -require "../spec_helper" - -Spectator.describe "GitHub Issue #55" do - GROUP_NAME = "CallCenter" - - let(name) { "TimeTravel" } - let(source) { "my.time.travel.experiment" } - - class Analytics(T) - property start_time = Time.local - property end_time = Time.local - - def initialize(@brain_talker : T) - end - - def instrument(*, name, source, &) - @brain_talker.send(payload: { - :group => GROUP_NAME, - :name => name, - :source => source, - :start => start_time, - :end => end_time, - }, action: "analytics") - end - end - - double(:brain_talker, send: nil) - - let(brain_talker) { double(:brain_talker) } - let(analytics) { Analytics.new(brain_talker) } - - it "tracks the time it takes to run the block" do - analytics.start_time = expected_start_time = Time.local - expected_end_time = expected_start_time + 10.seconds - analytics.end_time = expected_end_time + 0.5.seconds # Offset to ensure non-exact match. - - analytics.instrument(name: name, source: source) do - end - - expect(brain_talker).to have_received(:send).with(payload: { - :group => GROUP_NAME, - :name => name, - :source => source, - :start => expected_start_time, - :end => be_within(1.second).of(expected_end_time), - }, action: "analytics") - end -end diff --git a/spec/issues/gitlab_issue_51_spec.cr b/spec/issues/gitlab_issue_51_spec.cr index 996af80..c809607 100644 --- a/spec/issues/gitlab_issue_51_spec.cr +++ b/spec/issues/gitlab_issue_51_spec.cr @@ -1,33 +1,31 @@ require "../spec_helper" -module GitLabIssue51 - class Foo - def call(str : String) : String? - "" - end - - def alt1_call(str : String) : String? - nil - end - - def alt2_call(str : String) : String? - [str, nil].sample - end +private class Foo + def call(str : String) : String? + "" end - class Bar - def call(a_foo) : Nil # Must add nil restriction here, otherwise a segfault occurs from returning the result of #alt2_call. - a_foo.call("") - a_foo.alt1_call("") - a_foo.alt2_call("") - end + def alt1_call(str : String) : String? + nil + end + + def alt2_call(str : String) : String? + [str, nil].sample end end -Spectator.describe GitLabIssue51::Bar do - mock GitLabIssue51::Foo, call: "", alt1_call: "", alt2_call: "" +private class Bar + def call(a_foo) : Nil # Must add nil restriction here, otherwise a segfault occurs from returning the result of #alt2_call. + a_foo.call("") + a_foo.alt1_call("") + a_foo.alt2_call("") + end +end - let(:foo) { mock(GitLabIssue51::Foo) } +Spectator.describe Bar do + mock Foo, call: "", alt1_call: "", alt2_call: "" + + let(:foo) { mock(Foo) } subject(:call) { described_class.new.call(foo) } describe "#call" do diff --git a/spec/issues/gitlab_issue_80_spec.cr b/spec/issues/gitlab_issue_80_spec.cr deleted file mode 100644 index 9090130..0000000 --- a/spec/issues/gitlab_issue_80_spec.cr +++ /dev/null @@ -1,30 +0,0 @@ -require "../spec_helper" - -# https://gitlab.com/arctic-fox/spectator/-/issues/80 - -class Item -end - -class ItemUser - @item = Item.new - - def item - @item - end -end - -Spectator.describe "test1" do - it "without mock" do - item_user = ItemUser.new - item = item_user.item - item == item - end -end - -Spectator.describe "test2" do - mock Item do - end - - it "without mock" do - end -end diff --git a/spec/spectator/dsl/mocks/double_spec.cr b/spec/spectator/dsl/mocks/double_spec.cr index 5547ae0..89f652c 100644 --- a/spec/spectator/dsl/mocks/double_spec.cr +++ b/spec/spectator/dsl/mocks/double_spec.cr @@ -168,7 +168,7 @@ Spectator.describe "Double DSL", :smoke do context "methods accepting blocks" do double(:test7) do - stub def foo(&) + stub def foo yield end diff --git a/spec/spectator/dsl/mocks/mock_spec.cr b/spec/spectator/dsl/mocks/mock_spec.cr index cd57cdc..db63dd9 100644 --- a/spec/spectator/dsl/mocks/mock_spec.cr +++ b/spec/spectator/dsl/mocks/mock_spec.cr @@ -40,17 +40,17 @@ Spectator.describe "Mock DSL", :smoke do arg end - def method4(&) : Symbol + def method4 : Symbol @_spectator_invocations << :method4 yield end - def method5(&) + def method5 @_spectator_invocations << :method5 yield.to_i end - def method6(&) + def method6 @_spectator_invocations << :method6 yield end @@ -60,7 +60,7 @@ Spectator.describe "Mock DSL", :smoke do {arg, args, kwarg, kwargs} end - def method8(arg, *args, kwarg, **kwargs, &) + def method8(arg, *args, kwarg, **kwargs) @_spectator_invocations << :method8 yield {arg, args, kwarg, kwargs} @@ -80,7 +80,7 @@ Spectator.describe "Mock DSL", :smoke do "stubbed" end - stub def method4(&) : Symbol + stub def method4 : Symbol yield :block end @@ -258,12 +258,12 @@ Spectator.describe "Mock DSL", :smoke do # NOTE: Abstract methods that yield must have yield functionality defined in the method. # 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. - stub def method5(&) + stub def method5 yield end # NOTE: Another quirk where a default implementation must be provided because `&` is dropped. - stub def method6(&) : Symbol + stub def method6 : Symbol yield end @@ -381,12 +381,12 @@ Spectator.describe "Mock DSL", :smoke do # NOTE: Abstract methods that yield must have yield functionality defined in the method. # 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. - stub def method5(&) + stub def method5 yield end # NOTE: Another quirk where a default implementation must be provided because `&` is dropped. - stub def method6(&) : Symbol + stub def method6 : Symbol yield 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. # 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. - stub def method5(&) + stub def method5 yield end # NOTE: Another quirk where a default implementation must be provided because `&` is dropped. - stub def method6(&) : Symbol + stub def method6 : Symbol yield end @@ -577,12 +577,12 @@ Spectator.describe "Mock DSL", :smoke do # NOTE: Abstract methods that yield must have yield functionality defined in the method. # 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. - stub def method5(&) + stub def method5 yield end # NOTE: Another quirk where a default implementation must be provided because `&` is dropped. - stub def method6(&) : Symbol + stub def method6 : Symbol yield end end @@ -620,11 +620,11 @@ Spectator.describe "Mock DSL", :smoke do :original end - def method3(&) + def method3 yield end - def method4(&) : Int32 + def method4 : Int32 yield.to_i end @@ -749,11 +749,11 @@ Spectator.describe "Mock DSL", :smoke do :original end - def method3(&) + def method3 yield end - def method4(&) : Int32 + def method4 : Int32 yield.to_i end @@ -1027,262 +1027,4 @@ Spectator.describe "Mock DSL", :smoke do expect(fake.reference).to eq("reference") 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 diff --git a/spec/spectator/dsl/mocks/null_double_spec.cr b/spec/spectator/dsl/mocks/null_double_spec.cr index 06d35ee..1219f50 100644 --- a/spec/spectator/dsl/mocks/null_double_spec.cr +++ b/spec/spectator/dsl/mocks/null_double_spec.cr @@ -156,7 +156,7 @@ Spectator.describe "Null double DSL" do context "methods accepting blocks" do double(:test7) do - stub def foo(&) + stub def foo yield end diff --git a/spec/spectator/mocks/double_spec.cr b/spec/spectator/mocks/double_spec.cr index e55c549..1b1abc9 100644 --- a/spec/spectator/mocks/double_spec.cr +++ b/spec/spectator/mocks/double_spec.cr @@ -212,10 +212,14 @@ Spectator.describe Spectator::Double do expect(dbl.hash).to be_a(UInt64) expect(dbl.in?([42])).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.not_nil!).to be(dbl) + expect(dbl.pretty_inspect).to contain("EmptyDouble") expect(dbl.pretty_print(pp)).to be_nil 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.object_id).to be_a(UInt64) expect(dbl.same?(dbl)).to be_true @@ -297,7 +301,7 @@ Spectator.describe Spectator::Double do arg end - stub def self.baz(arg, &) + stub def self.baz(arg) yield end end @@ -465,7 +469,7 @@ Spectator.describe Spectator::Double do it "stores calls to non-stubbed methods" do expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /baz/) - expect(called_method_names(dbl)).to contain(:baz) + expect(called_method_names(dbl)).to eq(%i[baz]) end it "stores arguments for a call" do @@ -475,68 +479,4 @@ Spectator.describe Spectator::Double do expect(call.arguments).to eq(args) 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 diff --git a/spec/spectator/mocks/lazy_double_spec.cr b/spec/spectator/mocks/lazy_double_spec.cr index 8ea5a5d..c3402b5 100644 --- a/spec/spectator/mocks/lazy_double_spec.cr +++ b/spec/spectator/mocks/lazy_double_spec.cr @@ -275,7 +275,7 @@ Spectator.describe Spectator::LazyDouble do it "stores calls to non-stubbed methods" do expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /baz/) - expect(called_method_names(dbl)).to contain(:baz) + expect(called_method_names(dbl)).to eq(%i[baz]) end it "stores arguments for a call" do @@ -285,68 +285,4 @@ Spectator.describe Spectator::LazyDouble do expect(call.arguments).to eq(args) 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 diff --git a/spec/spectator/mocks/mock_spec.cr b/spec/spectator/mocks/mock_spec.cr index 3ddd0fe..40b4bf0 100644 --- a/spec/spectator/mocks/mock_spec.cr +++ b/spec/spectator/mocks/mock_spec.cr @@ -29,18 +29,8 @@ Spectator.describe Spectator::Mock do @_spectator_invocations << :method3 "original" end - - def method4 : Thing - self - end - - def method5 : OtherThing - OtherThing.new - end end - class OtherThing; end - Spectator::Mock.define_subtype(:class, Thing, MockThing, :mock_name, method1: 123) do stub def method2 :stubbed @@ -114,20 +104,6 @@ Spectator.describe Spectator::Mock do mock.method3 expect(mock._spectator_invocations).to contain_exactly(:method3) 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 context "with an abstract class" do @@ -144,14 +120,8 @@ Spectator.describe Spectator::Mock do end abstract def method4 - - abstract def method4 : Thing - - abstract def method5 : OtherThing end - class OtherThing; end - 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. 123 @@ -229,20 +199,6 @@ Spectator.describe Spectator::Mock do mock.method3 expect(mock._spectator_invocations).to contain_exactly(:method3) 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 context "with an abstract struct" do @@ -259,14 +215,8 @@ Spectator.describe Spectator::Mock do end abstract def method4 - - abstract def method4 : Thing - - abstract def method5 : OtherThing end - class OtherThing; end - 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. 123 @@ -336,22 +286,6 @@ Spectator.describe Spectator::Mock do mock.method3 expect(mock._spectator_invocations).to contain_exactly(:method3) 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 context "class method stubs" do @@ -364,21 +298,11 @@ Spectator.describe Spectator::Mock do arg end - def self.baz(arg, &) + def self.baz(arg) yield end - - def self.thing : Thing - new - end - - def self.other : OtherThing - OtherThing.new - end end - class OtherThing; end - Spectator::Mock.define_subtype(:class, Thing, MockThing) do stub def self.foo :stub @@ -443,20 +367,6 @@ Spectator.describe Spectator::Mock do expect(restricted(mock)).to eq(:stub) 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 before { mock._spectator_define_stub(foo_stub) } @@ -491,203 +401,6 @@ Spectator.describe Spectator::Mock do 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 abstract class Thing abstract def oops : NoReturn @@ -929,7 +642,7 @@ Spectator.describe Spectator::Mock do arg end - def self.baz(arg, &) + def self.baz(arg) yield end end diff --git a/spec/spectator/mocks/null_double_spec.cr b/spec/spectator/mocks/null_double_spec.cr index a6fc7d2..4a8ef3a 100644 --- a/spec/spectator/mocks/null_double_spec.cr +++ b/spec/spectator/mocks/null_double_spec.cr @@ -186,9 +186,12 @@ Spectator.describe Spectator::NullDouble do expect(dbl.hash).to be_a(UInt64) expect(dbl.in?([42])).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.not_nil!).to be(dbl) + expect(dbl.pretty_inspect).to contain("EmptyDouble") expect(dbl.tap { nil }).to be(dbl) + expect(dbl.to_s).to contain("EmptyDouble") expect(dbl.try { nil }).to be_nil expect(dbl.object_id).to be_a(UInt64) expect(dbl.same?(dbl)).to be_true @@ -259,7 +262,7 @@ Spectator.describe Spectator::NullDouble do arg end - stub def self.baz(arg, &) + stub def self.baz(arg) yield end end @@ -436,68 +439,4 @@ Spectator.describe Spectator::NullDouble do expect(call.arguments).to eq(args) 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 diff --git a/src/spectator/context.cr b/src/spectator/context.cr index 15b9335..7fc126b 100644 --- a/src/spectator/context.cr +++ b/src/spectator/context.cr @@ -4,11 +4,6 @@ # 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. 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. # This prevents the default behavior, which normally stringifies instance variables. # Due to the sheer amount of types Spectator can create diff --git a/src/spectator/dsl/expectations.cr b/src/spectator/dsl/expectations.cr index dba2e9b..a35a15c 100644 --- a/src/spectator/dsl/expectations.cr +++ b/src/spectator/dsl/expectations.cr @@ -182,7 +182,7 @@ module Spectator::DSL # expect(false).to be_true # end # ``` - def aggregate_failures(label = nil, &) + def aggregate_failures(label = nil) ::Spectator::Harness.current.aggregate_failures(label) do yield end diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index da06906..0e7b47b 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -137,11 +137,7 @@ module Spectator::DSL what.is_a?(NilLiteral) %} {{what}} {% elsif what.is_a?(StringInterpolation) %} - {{@type.name}}.new.eval do - {{what}} - rescue e - "" - end + {% raise "String interpolation isn't supported for example group names" %} {% else %} {{what.stringify}} {% end %} diff --git a/src/spectator/dsl/metadata.cr b/src/spectator/dsl/metadata.cr index 04092b9..308bcbd 100644 --- a/src/spectator/dsl/metadata.cr +++ b/src/spectator/dsl/metadata.cr @@ -6,9 +6,6 @@ module Spectator::DSL private macro _spectator_metadata(name, source, *tags, **metadata) private def self.{{name.id}} %metadata = {{source.id}}.dup - {% unless tags.empty? && metadata.empty? %} - %metadata ||= ::Spectator::Metadata.new - {% end %} {% for k in tags %} %metadata[{{k.id.symbolize}}] = nil {% end %} diff --git a/src/spectator/dsl/mocks.cr b/src/spectator/dsl/mocks.cr index 5eff1a9..94a41f9 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -31,7 +31,7 @@ module Spectator::DSL ::Spectator::DSL::Mocks::TYPES << {name.id.symbolize, @type.name(generic_args: false).symbolize, double_type_name.symbolize} %} # Define the plain double type. - ::Spectator::Double.define({{double_type_name}}, {{name}}, {{value_methods.double_splat}}) do + ::Spectator::Double.define({{double_type_name}}, {{name}}, {{**value_methods}}) do # Returns a new double that responds to undefined methods with itself. # See: `NullDouble` def as_null_object @@ -43,7 +43,7 @@ module Spectator::DSL {% begin %} # Define a matching null double type. - ::Spectator::NullDouble.define({{null_double_type_name}}, {{name}}, {{value_methods.double_splat}}) {{block}} + ::Spectator::NullDouble.define({{null_double_type_name}}, {{name}}, {{**value_methods}}) {{block}} {% end %} end @@ -94,9 +94,9 @@ module Spectator::DSL begin %double = {% if found_tuple %} - {{found_tuple[2].id}}.new({{value_methods.double_splat}}) + {{found_tuple[2].id}}.new({{**value_methods}}) {% else %} - ::Spectator::LazyDouble.new({{name}}, {{value_methods.double_splat}}) + ::Spectator::LazyDouble.new({{name}}, {{**value_methods}}) {% end %} ::Spectator::Harness.current?.try(&.cleanup { %double._spectator_reset }) %double @@ -176,7 +176,7 @@ module Spectator::DSL # See `#def_double`. macro double(name, **value_methods, &block) {% begin %} - {% if @def %}new_double{% else %}def_double{% end %}({{name}}, {{value_methods.double_splat}}) {{block}} + {% if @def %}new_double{% else %}def_double{% end %}({{name}}, {{**value_methods}}) {{block}} {% end %} end @@ -189,7 +189,7 @@ module Spectator::DSL # expect(dbl.foo).to eq(42) # ``` macro double(**value_methods) - ::Spectator::LazyDouble.new({{value_methods.double_splat}}) + ::Spectator::LazyDouble.new({{**value_methods}}) end # Defines a new mock type. @@ -218,29 +218,24 @@ module Spectator::DSL # end # ``` private macro def_mock(type, name = nil, **value_methods, &block) - {% resolved = type.resolve - # Construct a unique type name for the mock by using the number of defined types. - 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 + {% # Construct a unique type name for the mock by using the number of defined types. + index = ::Spectator::DSL::Mocks::TYPES.size + mock_type_name = "Mock#{index}".id - # Store information about how the mock is defined and its context. - # This is important for constructing an instance of the mock later. - ::Spectator::DSL::Mocks::TYPES << {type.id.symbolize, @type.name(generic_args: false).symbolize, "#{"::".id unless resolved.name.starts_with?("::")}#{resolved.name}::#{mock_type_name}".id.symbolize} + # Store information about how the mock is defined and its context. + # 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} - base = if resolved.class? - :class - elsif resolved.struct? - :struct - else - :module - end %} + resolved = type.resolve + base = if resolved.class? + :class + elsif resolved.struct? + :struct + else + :module + end %} - {% 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 %} + ::Spectator::Mock.define_subtype({{base}}, {{type.id}}, {{mock_type_name}}, {{name}}, {{**value_methods}}) {{block}} end # Instantiates a mock. @@ -321,7 +316,7 @@ module Spectator::DSL 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) %} {% begin %} - {% if @def %}new_mock{% else %}def_mock{% end %}({{type}}, {{value_methods.double_splat}}) {{block}} + {% if @def %}new_mock{% else %}def_mock{% end %}({{type}}, {{**value_methods}}) {{block}} {% end %} end @@ -431,7 +426,7 @@ module Spectator::DSL # 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::Mock.inject({{base}}, {{resolved.name}}, {{value_methods.double_splat}}) {{block}} + ::Spectator::Mock.inject({{base}}, ::{{resolved.name}}, {{**value_methods}}) {{block}} end # Targets a stubbable object (such as a mock or double) for operations. diff --git a/src/spectator/error_result.cr b/src/spectator/error_result.cr index f58da20..a4531fb 100644 --- a/src/spectator/error_result.cr +++ b/src/spectator/error_result.cr @@ -11,7 +11,7 @@ module Spectator end # Calls the `error` method on *visitor*. - def accept(visitor, &) + def accept(visitor) visitor.error(yield self) end diff --git a/src/spectator/example.cr b/src/spectator/example.cr index e18676c..3625ded 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -40,7 +40,7 @@ module Spectator # Note: The metadata will not be merged with the parent metadata. def initialize(@context : Context, @entrypoint : self ->, name : String? = nil, location : Location? = nil, - @group : ExampleGroup? = nil, metadata = nil) + @group : ExampleGroup? = nil, metadata = Metadata.new) super(name, location, metadata) # Ensure group is linked. @@ -58,7 +58,7 @@ module Spectator # Note: The metadata will not be merged with the parent metadata. def initialize(@context : Context, @entrypoint : self ->, @name_proc : Example -> String, location : Location? = nil, - @group : ExampleGroup? = nil, metadata = nil) + @group : ExampleGroup? = nil, metadata = Metadata.new) super(nil, location, metadata) # Ensure group is linked. @@ -75,7 +75,7 @@ module Spectator # A set of *metadata* can be used for filtering and modifying example behavior. # Note: The metadata will not be merged with the parent metadata. def initialize(name : String? = nil, location : Location? = nil, - @group : ExampleGroup? = nil, metadata = nil, &block : self ->) + @group : ExampleGroup? = nil, metadata = Metadata.new, &block : self ->) super(name, location, metadata) @context = NullContext.new @@ -93,10 +93,9 @@ module Spectator # A set of *metadata* can be used for filtering and modifying example behavior. # Note: The metadata will not be merged with the parent metadata. def self.pending(name : String? = nil, location : Location? = nil, - group : ExampleGroup? = nil, metadata = nil, reason = nil) + group : ExampleGroup? = nil, metadata = Metadata.new, reason = nil) # Add pending tag and reason if they don't exist. - tags = {:pending => nil, :reason => reason} - metadata = metadata ? metadata.merge(tags) { |_, v, _| v } : tags + metadata = metadata.merge({:pending => nil, :reason => reason}) { |_, v, _| v } new(name, location, group, metadata) { nil } end @@ -118,7 +117,7 @@ module Spectator begin @result = Harness.run do - if proc = @name_proc + if proc = @name_proc.as?(Proc(Example, String)) self.name = proc.call(self) end @@ -164,7 +163,7 @@ module Spectator # 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. - protected def with_context(klass, &) + protected def with_context(klass) context = klass.cast(@context) with context yield end @@ -184,7 +183,7 @@ module Spectator end # Yields this example and all parent groups. - def ascend(&) + def ascend node = self while node yield node @@ -279,7 +278,7 @@ module Spectator # 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. - protected def with_context(klass, &) + protected def with_context(klass) context = @example.cast_context(klass) with context yield end diff --git a/src/spectator/example_builder.cr b/src/spectator/example_builder.cr index 23398d2..bb640df 100644 --- a/src/spectator/example_builder.cr +++ b/src/spectator/example_builder.cr @@ -15,7 +15,7 @@ module Spectator # 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`. def initialize(@context_builder : -> Context, @entrypoint : Example ->, - @name : String? = nil, @location : Location? = nil, @metadata : Metadata? = nil) + @name : String? = nil, @location : Location? = nil, @metadata : Metadata = Metadata.new) end # Creates the builder. @@ -24,7 +24,7 @@ module Spectator # 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`. def initialize(@context_builder : -> Context, @entrypoint : Example ->, - @name : Example -> String, @location : Location? = nil, @metadata : Metadata? = nil) + @name : Example -> String, @location : Location? = nil, @metadata : Metadata = Metadata.new) end # Constructs an example with previously defined attributes and context. diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index 55a3233..dc1fa57 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -79,7 +79,7 @@ module Spectator # 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. def initialize(@name : Label = nil, @location : Location? = nil, - @group : ExampleGroup? = nil, @metadata : Metadata? = nil) + @group : ExampleGroup? = nil, @metadata : Metadata = Metadata.new) # Ensure group is linked. group << self if group end @@ -87,7 +87,7 @@ module Spectator delegate size, unsafe_fetch, to: @nodes # Yields this group and all parent groups. - def ascend(&) + def ascend group = self while group yield group diff --git a/src/spectator/example_group_builder.cr b/src/spectator/example_group_builder.cr index 207cb6e..05c740f 100644 --- a/src/spectator/example_group_builder.cr +++ b/src/spectator/example_group_builder.cr @@ -28,7 +28,7 @@ module Spectator # Creates the builder. # Initially, the builder will have no children and no hooks. # The *name*, *location*, and *metadata* will be applied to the `ExampleGroup` produced by `#build`. - def initialize(@name : Label = nil, @location : Location? = nil, @metadata : Metadata? = nil) + def initialize(@name : Label = nil, @location : Location? = nil, @metadata : Metadata = Metadata.new) end # Constructs an example group with previously defined attributes, children, and hooks. diff --git a/src/spectator/example_group_iteration.cr b/src/spectator/example_group_iteration.cr index 0d20a29..d6576d2 100644 --- a/src/spectator/example_group_iteration.cr +++ b/src/spectator/example_group_iteration.cr @@ -18,7 +18,7 @@ module Spectator # 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. def initialize(@item : T, name : Label = nil, location : Location? = nil, - group : ExampleGroup? = nil, metadata : Metadata? = nil) + group : ExampleGroup? = nil, metadata : Metadata = Metadata.new) super(name, location, group, metadata) end end diff --git a/src/spectator/expectation.cr b/src/spectator/expectation.cr index 79d8473..780e299 100644 --- a/src/spectator/expectation.cr +++ b/src/spectator/expectation.cr @@ -114,21 +114,6 @@ module Spectator report(match_data, message) 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. @[AlwaysInline] def to_not(stub : Stub, message = nil) : Nil @@ -151,36 +136,6 @@ module Spectator report(match_data, message) 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: @[AlwaysInline] def not_to(matcher, message = nil) : Nil diff --git a/src/spectator/fail_result.cr b/src/spectator/fail_result.cr index 082a0d2..c283a80 100644 --- a/src/spectator/fail_result.cr +++ b/src/spectator/fail_result.cr @@ -24,7 +24,7 @@ module Spectator end # Calls the `failure` method on *visitor*. - def accept(visitor, &) + def accept(visitor) visitor.fail(yield self) end diff --git a/src/spectator/formatting/components/block.cr b/src/spectator/formatting/components/block.cr index 22411a7..40cd5a8 100644 --- a/src/spectator/formatting/components/block.cr +++ b/src/spectator/formatting/components/block.cr @@ -13,7 +13,7 @@ module Spectator::Formatting::Components end # 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 yield @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. # Ensure that _only_ one line is produced by the block, # otherwise the indent will be lost. - private def line(io, &) + private def line(io) @indent.times { io << ' ' } yield io.puts diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 1f9fa09..6be48b9 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -43,7 +43,7 @@ module Spectator # 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. # The result of running the test code will be returned. - def self.run(&) : Result + def self.run : Result with_harness do |harness| harness.run { yield } end @@ -53,7 +53,7 @@ module Spectator # 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. # The result of the block is returned. - private def self.with_harness(&) + private def self.with_harness previous = @@current begin @@current = harness = new @@ -70,7 +70,7 @@ module Spectator # 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. - def run(&) : Result + def run : Result elapsed, error = capture { yield } elapsed2, error2 = capture { run_deferred } run_cleanup @@ -106,7 +106,7 @@ module Spectator @cleanup << block end - def aggregate_failures(label = nil, &) + def aggregate_failures(label = nil) previous = @aggregate @aggregate = aggregate = [] of Expectation begin @@ -135,7 +135,7 @@ module Spectator # 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). - private def capture(&) : Tuple(Time::Span, Exception?) + private def capture : Tuple(Time::Span, Exception?) error = nil elapsed = Time.measure do error = catch { yield } @@ -146,7 +146,7 @@ module Spectator # 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 doesn't raise an error, then nil is returned. - private def catch(&) : Exception? + private def catch : Exception? yield rescue e e diff --git a/src/spectator/iterative_example_group_builder.cr b/src/spectator/iterative_example_group_builder.cr index fe67c7a..39ad549 100644 --- a/src/spectator/iterative_example_group_builder.cr +++ b/src/spectator/iterative_example_group_builder.cr @@ -15,7 +15,7 @@ module Spectator # 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. def initialize(@collection : Enumerable(T), name : String? = nil, @iterators : Array(String) = [] of String, - location : Location? = nil, metadata : Metadata? = nil) + location : Location? = nil, metadata : Metadata = Metadata.new) super(name, location, metadata) end diff --git a/src/spectator/matchers/exception_matcher.cr b/src/spectator/matchers/exception_matcher.cr index b26d390..adec663 100644 --- a/src/spectator/matchers/exception_matcher.cr +++ b/src/spectator/matchers/exception_matcher.cr @@ -97,7 +97,7 @@ module Spectator::Matchers # Runs a block of code and returns the exception it threw. # If no exception was thrown, *nil* is returned. - private def capture_exception(&) + private def capture_exception exception = nil begin yield diff --git a/src/spectator/matchers/matcher.cr b/src/spectator/matchers/matcher.cr index 05adb81..e54e55e 100644 --- a/src/spectator/matchers/matcher.cr +++ b/src/spectator/matchers/matcher.cr @@ -1,4 +1,3 @@ -require "../value" require "./match_data" module Spectator::Matchers @@ -23,19 +22,6 @@ module Spectator::Matchers # A successful match with `#match` should normally fail for this method, and vice-versa. 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 match_data_description(actual.label) end diff --git a/src/spectator/matchers/range_matcher.cr b/src/spectator/matchers/range_matcher.cr index 8a9a307..8c31810 100644 --- a/src/spectator/matchers/range_matcher.cr +++ b/src/spectator/matchers/range_matcher.cr @@ -29,26 +29,7 @@ module Spectator::Matchers # Checks whether the matcher is satisfied with the expression given to it. private def match?(actual : Expression(T)) : Bool forall T - 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) + expected.value.includes?(actual.value) end # Message displayed when the matcher isn't satisfied. diff --git a/src/spectator/mocks/abstract_arguments.cr b/src/spectator/mocks/abstract_arguments.cr index 4a6f75f..810d2fe 100644 --- a/src/spectator/mocks/abstract_arguments.cr +++ b/src/spectator/mocks/abstract_arguments.cr @@ -1,61 +1,13 @@ module Spectator # Untyped arguments to a method call (message). abstract class AbstractArguments - # Use the string representation to avoid over complicating debug output. - def inspect(io : IO) : Nil - to_s(io) - end - - # Utility method for comparing two tuples considering special types. - private def compare_tuples(a : Tuple | Array, b : Tuple | Array) - return false if a.size != b.size - - a.zip(b) do |a_value, b_value| - return false unless compare_values(a_value, b_value) - end - true - end - - # Utility method for comparing two tuples considering special types. - # Supports nilable tuples (ideal for splats). - private def compare_tuples(a : Tuple? | Array?, b : Tuple? | Array?) - return false if a.nil? ^ b.nil? - - compare_tuples(a.not_nil!, b.not_nil!) - end - # Utility method for comparing two named tuples ignoring order. - private def compare_named_tuples(a : NamedTuple | Hash, b : NamedTuple | Hash) + private def compare_named_tuples(a : NamedTuple, b : NamedTuple) a.each do |k, v1| v2 = b.fetch(k) { return false } - return false unless compare_values(v1, v2) + return false unless 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 diff --git a/src/spectator/mocks/arguments.cr b/src/spectator/mocks/arguments.cr index f15a0ba..f9c6e19 100644 --- a/src/spectator/mocks/arguments.cr +++ b/src/spectator/mocks/arguments.cr @@ -81,7 +81,7 @@ module Spectator # Checks if another set of arguments matches this set of arguments. def ===(other : Arguments) - compare_tuples(positional, other.positional) && compare_named_tuples(kwargs, other.kwargs) + positional === other.positional && compare_named_tuples(kwargs, other.kwargs) end # :ditto: @@ -90,18 +90,17 @@ module Spectator i = 0 other.args.each do |k, v2| - break if i >= positional.size next if kwargs.has_key?(k) # Covered by named arguments. - v1 = positional[i] + v1 = positional.fetch(i) { return false } i += 1 - return false unless compare_values(v1, v2) + return false unless v1 === v2 end other.splat.try &.each do |v2| v1 = positional.fetch(i) { return false } i += 1 - return false unless compare_values(v1, v2) + return false unless v1 === v2 end i == positional.size diff --git a/src/spectator/mocks/double.cr b/src/spectator/mocks/double.cr index e5e43f4..8143ba0 100644 --- a/src/spectator/mocks/double.cr +++ b/src/spectator/mocks/double.cr @@ -98,35 +98,24 @@ module Spectator # Simplified string representation of a double. # Avoids displaying nested content and bloating method instantiation. def to_s(io : IO) : Nil - io << "#<" + {{@type.name(generic_args: false).stringify}} + " " - io << _spectator_stubbed_name << '>' - end - - # :ditto: - def inspect(io : IO) : Nil - io << "#<" + {{@type.name(generic_args: false).stringify}} + " " io << _spectator_stubbed_name - - io << ":0x" - object_id.to_s(io, 16) - io << '>' end # Defines a stub to change the behavior of a method in this double. # # 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 - Log.debug { "Defined stub for #{inspect} #{stub}" } + Log.debug { "Defined stub for #{_spectator_stubbed_name} #{stub}" } @stubs.unshift(stub) end protected def _spectator_remove_stub(stub : Stub) : Nil - Log.debug { "Removing stub #{stub} from #{inspect}" } + Log.debug { "Removing stub #{stub} from #{_spectator_stubbed_name}" } @stubs.delete(stub) end protected def _spectator_clear_stubs : Nil - Log.debug { "Clearing stubs for #{inspect}" } + Log.debug { "Clearing stubs for #{_spectator_stubbed_name}" } @stubs.clear end @@ -156,17 +145,17 @@ module Spectator # Returns the double's name formatted for user output. private def _spectator_stubbed_name : String {% if anno = @type.annotation(StubbedName) %} - {{(anno[0] || :Anonymous.id).stringify}} + "#" {% else %} - "Anonymous" + "#" {% end %} end private def self._spectator_stubbed_name : String {% if anno = @type.annotation(StubbedName) %} - {{(anno[0] || :Anonymous.id).stringify}} + "#" {% else %} - "Anonymous" + "#" {% end %} end @@ -186,7 +175,7 @@ module Spectator "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." end - raise UnexpectedMessage.new("#{inspect} received unexpected message #{call}") + raise UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}") end private def _spectator_abstract_stub_fallback(call : MethodCall, type) @@ -205,9 +194,9 @@ module Spectator call = ::Spectator::MethodCall.new({{call.name.symbolize}}, args) _spectator_record_call(call) - Log.trace { "#{inspect} got undefined method `#{call}{% if call.block %} { ... }{% end %}`" } + Log.trace { "#{_spectator_stubbed_name} got undefined method `#{call}{% if call.block %} { ... }{% end %}`" } - raise ::Spectator::UnexpectedMessage.new("#{inspect} received unexpected message #{call}") + raise ::Spectator::UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}") nil # Necessary for compiler to infer return type as nil. Avoids runtime "can't execute ... `x` has no type errors". end end diff --git a/src/spectator/mocks/formal_arguments.cr b/src/spectator/mocks/formal_arguments.cr index 1c0ca69..e440214 100644 --- a/src/spectator/mocks/formal_arguments.cr +++ b/src/spectator/mocks/formal_arguments.cr @@ -122,12 +122,12 @@ module Spectator # Checks if another set of arguments matches this set of arguments. def ===(other : Arguments) - compare_tuples(positional, other.positional) && compare_named_tuples(kwargs, other.kwargs) + positional === other.positional && compare_named_tuples(kwargs, other.kwargs) end # :ditto: def ===(other : FormalArguments) - compare_named_tuples(args, other.args) && compare_tuples(splat, other.splat) && compare_named_tuples(kwargs, other.kwargs) + compare_named_tuples(args, other.args) && splat === other.splat && compare_named_tuples(kwargs, other.kwargs) end end end diff --git a/src/spectator/mocks/lazy_double.cr b/src/spectator/mocks/lazy_double.cr index 75fe30c..9a9257d 100644 --- a/src/spectator/mocks/lazy_double.cr +++ b/src/spectator/mocks/lazy_double.cr @@ -37,13 +37,13 @@ module Spectator # Returns the double's name formatted for user output. private def _spectator_stubbed_name : String - @name || "Anonymous" + "#" end private def _spectator_stub_fallback(call : MethodCall, &) if _spectator_stub_for_method?(call.method) Log.info { "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." } - raise UnexpectedMessage.new("#{inspect} received unexpected message #{call}") + raise UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}") else Log.trace { "Fallback for #{call} - call original" } yield @@ -57,7 +57,7 @@ module Spectator %call = ::Spectator::MethodCall.new({{call.name.symbolize}}, %args) _spectator_record_call(%call) - Log.trace { "#{inspect} got undefined method `#{%call}{% if call.block %} { ... }{% end %}`" } + Log.trace { "#{_spectator_stubbed_name} got undefined method `#{%call}{% if call.block %} { ... }{% end %}`" } # Attempt to find a stub that satisfies the method call and arguments. if %stub = _spectator_find_stub(%call) diff --git a/src/spectator/mocks/method_call.cr b/src/spectator/mocks/method_call.cr index 9c5fd01..b8e973e 100644 --- a/src/spectator/mocks/method_call.cr +++ b/src/spectator/mocks/method_call.cr @@ -30,13 +30,7 @@ module Spectator # Constructs a string containing the method name and arguments. def to_s(io : IO) : Nil - io << '#' << method - arguments.inspect(io) - end - - # :ditto: - def inspect(io : IO) : Nil - to_s(io) + io << '#' << method << arguments end end end diff --git a/src/spectator/mocks/mock.cr b/src/spectator/mocks/mock.cr index d2a1fde..ba72b37 100644 --- a/src/spectator/mocks/mock.cr +++ b/src/spectator/mocks/mock.cr @@ -1,6 +1,5 @@ require "./method_call" require "./mocked" -require "./mock_registry" require "./reference_mock_registry" require "./stub" require "./stubbed_name" @@ -37,35 +36,7 @@ module Spectator macro define_subtype(base, mocked_type, type_name, name = nil, **value_methods, &block) {% begin %} {% if name %}@[::Spectator::StubbedName({{name}})]{% end %} - {% if base.id == :module.id %} - {{base.id}} {{type_name.id}} - include {{mocked_type.id}} - - # Mock class that includes the mocked module {{mocked_type.id}} - {% if name %}@[::Spectator::StubbedName({{name}})]{% end %} - private class ClassIncludingMock{{type_name.id}} - include {{type_name.id}} - end - - # Returns a mock class that includes the mocked module {{mocked_type.id}}. - def self.new(*args, **kwargs) : ClassIncludingMock{{type_name.id}} - # FIXME: Creating the instance normally with `.new` causing infinite recursion. - inst = ClassIncludingMock{{type_name.id}}.allocate - inst.initialize(*args, **kwargs) - inst - end - - # Returns a mock class that includes the mocked module {{mocked_type.id}}. - def self.new(*args, **kwargs) : ClassIncludingMock{{type_name.id}} - # FIXME: Creating the instance normally with `.new` causing infinite recursion. - inst = ClassIncludingMock{{type_name.id}}.allocate - inst.initialize(*args, **kwargs) { |*yargs| yield *yargs } - inst - end - - {% else %} - {{base.id}} {{type_name.id}} < {{mocked_type.id}} - {% end %} + {{base.id}} {{type_name.id}} < {{mocked_type.id}} include ::Spectator::Mocked extend ::Spectator::StubbedType @@ -79,22 +50,22 @@ module Spectator end {% end %} - def _spectator_remove_stub(stub : ::Spectator::Stub) : ::Nil + def _spectator_remove_stub(stub : ::Spectator::Stub) : Nil @_spectator_stubs.try &.delete(stub) end - def _spectator_clear_stubs : ::Nil + def _spectator_clear_stubs : Nil @_spectator_stubs = nil 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 # 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) %} "#" \{% else %} @@ -102,7 +73,7 @@ module Spectator \{% end %} end - private def self._spectator_stubbed_name : ::String + private def self._spectator_stubbed_name : String \{% if anno = @type.annotation(::Spectator::StubbedName) %} "#" \{% else %} @@ -149,7 +120,7 @@ module Spectator macro inject(base, type_name, name = nil, **value_methods, &block) {% begin %} {% if name %}@[::Spectator::StubbedName({{name}})]{% end %} - {{base.id}} {{"::".id unless type_name.id.starts_with?("::")}}{{type_name.id}} + {{base.id}} ::{{type_name.id}} include ::Spectator::Mocked extend ::Spectator::StubbedType @@ -158,12 +129,12 @@ module Spectator {% elsif base == :struct %} @@_spectator_mock_registry = ::Spectator::ValueMockRegistry(self).new {% else %} - @@_spectator_mock_registry = ::Spectator::MockRegistry.new + {% raise "Unsupported base type #{base} for injecting mock" %} {% 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 entry = @@_spectator_mock_registry.fetch(self) do @@ -172,11 +143,11 @@ module Spectator entry.stubs 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) end - def _spectator_clear_stubs : ::Nil + def _spectator_clear_stubs : Nil @@_spectator_mock_registry.delete(self) end @@ -198,7 +169,7 @@ module Spectator end # 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) %} "#" \{% else %} @@ -207,7 +178,7 @@ module Spectator end # 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) %} "#" \{% else %} diff --git a/src/spectator/mocks/mock_registry.cr b/src/spectator/mocks/mock_registry.cr deleted file mode 100644 index 29390c6..0000000 --- a/src/spectator/mocks/mock_registry.cr +++ /dev/null @@ -1,43 +0,0 @@ -require "./mock_registry_entry" -require "./stub" - -module Spectator - # Stores collections of stubs for mocked types. - # - # This type is intended for all mocked modules that have functionality "injected." - # That is, the type itself has mock functionality bolted on. - # Adding instance members should be avoided, for instance, it could mess up serialization. - class MockRegistry - @entry : MockRegistryEntry? - - # Retrieves all stubs. - def [](_object = nil) - @entry.not_nil! - end - - # Retrieves all stubs. - def []?(_object = nil) - @entry - end - - # Retrieves all stubs. - # - # Yields to the block on the first retrieval. - # This allows a mock to populate the registry with initial stubs. - def fetch(object : Reference, & : -> Array(Stub)) - entry = @entry - if entry.nil? - entry = MockRegistryEntry.new - entry.stubs = yield - @entry = entry - else - entry - end - end - - # Clears all stubs defined for a mocked object. - def delete(object : Reference) : Nil - @entry = nil - end - end -end diff --git a/src/spectator/mocks/null_double.cr b/src/spectator/mocks/null_double.cr index 587f4ab..0ddd03d 100644 --- a/src/spectator/mocks/null_double.cr +++ b/src/spectator/mocks/null_double.cr @@ -26,7 +26,7 @@ module Spectator private def _spectator_abstract_stub_fallback(call : MethodCall) if _spectator_stub_for_method?(call.method) Log.info { "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." } - raise UnexpectedMessage.new("#{inspect} received unexpected message #{call}") + raise UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}") else Log.trace { "Fallback for #{call} - return self" } self @@ -42,9 +42,9 @@ module Spectator private def _spectator_abstract_stub_fallback(call : MethodCall, type) if _spectator_stub_for_method?(call.method) Log.info { "Stubs are defined for #{call.method.inspect}, but none matched (no argument constraints met)." } - raise UnexpectedMessage.new("#{inspect} received unexpected message #{call}") + raise UnexpectedMessage.new("#{_spectator_stubbed_name} received unexpected message #{call}") else - raise TypeCastError.new("#{inspect} received message #{call} and is attempting to return `self`, but returned type must be `#{type}`.") + raise TypeCastError.new("#{_spectator_stubbed_name} received message #{call} and is attempting to return `self`, but returned type must be `#{type}`.") end end @@ -56,7 +56,7 @@ module Spectator %call = ::Spectator::MethodCall.new({{call.name.symbolize}}, %args) _spectator_record_call(%call) - Log.trace { "#{inspect} got undefined method `#{%call}{% if call.block %} { ... }{% end %}`" } + Log.trace { "#{_spectator_stubbed_name} got undefined method `#{%call}{% if call.block %} { ... }{% end %}`" } self end diff --git a/src/spectator/mocks/stubbable.cr b/src/spectator/mocks/stubbable.cr index 3385ad4..cd7c407 100644 --- a/src/spectator/mocks/stubbable.cr +++ b/src/spectator/mocks/stubbable.cr @@ -126,41 +126,7 @@ module Spectator {{method.body}} end - {% 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 %} + {% original = "previous_def#{" { |*_spectator_yargs| yield *_spectator_yargs }".id if method.accepts_block?}".id %} {% # 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). @@ -179,7 +145,7 @@ module Spectator ::NamedTuple.new( {% for arg, i in method.args %}{% if !method.splat_index || i < method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %} ), - {% if method.splat_index && !(splat = method.args[method.splat_index].internal_name).empty? %}{{splat.symbolize}}, {{splat}},{% end %} + {% if method.splat_index && (splat = method.args[method.splat_index].internal_name) %}{{splat.symbolize}}, {{splat}},{% end %} ::NamedTuple.new( {% for arg, i in method.args %}{% if method.splat_index && i > method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %} ).merge({{method.double_splat}}) @@ -192,24 +158,10 @@ module Spectator # Cast the stub or return value to the expected type. # This is necessary to match the expected return type of the original method. _spectator_cast_stub_value(%stub, %call, typeof({{original}}), - {{ if rt = method.return_type - if rt.is_a?(Path) && (resolved = rt.resolve?).is_a?(TypeNode) && resolved <= NoReturn - :no_return - 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 + {{ if method.return_type && method.return_type.resolve == NoReturn + :no_return + elsif method.return_type && method.return_type.resolve <= Nil || method.return_type.is_a?(Union) && method.return_type.types.map(&.resolve).includes?(Nil) + :nil else :raise end }}) @@ -275,42 +227,7 @@ module Spectator {{method.body}} end - {% 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 %} - + {% original = "previous_def#{" { |*_spectator_yargs| yield *_spectator_yargs }".id if method.accepts_block?}".id %} {% end %} {% # Reconstruct the method signature. @@ -330,7 +247,7 @@ module Spectator ::NamedTuple.new( {% for arg, i in method.args %}{% if !method.splat_index || i < method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %} ), - {% if method.splat_index && !(splat = method.args[method.splat_index].internal_name).empty? %}{{splat.symbolize}}, {{splat}},{% end %} + {% if method.splat_index && (splat = method.args[method.splat_index].internal_name) %}{{splat.symbolize}}, {{splat}},{% end %} ::NamedTuple.new( {% for arg, i in method.args %}{% if method.splat_index && i > method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %} ).merge({{method.double_splat}}) @@ -342,25 +259,15 @@ module Spectator if %stub = _spectator_find_stub(%call) # Cast the stub or return value to the expected type. # This is necessary to match the expected return type of the original method. - {% if rt = method.return_type %} + {% if method.return_type %} # Return type restriction takes priority since it can be a superset of the original implementation. _spectator_cast_stub_value(%stub, %call, {{method.return_type}}, - {{ if rt.is_a?(Path) && (resolved = rt.resolve?).is_a?(TypeNode) && resolved <= NoReturn + {{ if method.return_type.resolve == NoReturn :no_return + elsif method.return_type.resolve <= Nil || method.return_type.is_a?(Union) && method.return_type.types.map(&.resolve).includes?(Nil) + :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 + :raise end }}) {% elsif !method.abstract? %} # The method isn't abstract, infer the type it returns without calling it. @@ -431,93 +338,64 @@ module Spectator # Redefines all methods and ones inherited from its parents and mixins to support stubs. private macro stub_type(type_name = @type) {% type = type_name.resolve - definitions = [] of Nil - scope = if type == @type - :previous_def - elsif type.module? - type.name - else - :super - end.id + # Reverse order of ancestors (there's currently no reverse method for ArrayLiteral). + count = type.ancestors.size + ancestors = type.ancestors.map_with_index { |_, i| type.ancestors[count - i - 1] } %} + {% for ancestor in ancestors %} + {% for method in ancestor.methods.reject do |meth| + meth.name.starts_with?("_spectator") || + ::Spectator::DSL::RESERVED_KEYWORDS.includes?(meth.name.symbolize) + end %} + {{(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 %} - # Add entries for methods in the target type and its class type. - [[:self.id, type.class], [nil, type]].each do |(receiver, t)| - t.methods.each do |method| - definitions << { - type: t, - method: method, - scope: scope, - receiver: receiver, - } - end - end + {% for method in ancestor.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 %} + super{% if method.accepts_block? %} { |*%yargs| yield *%yargs }{% end %} + end + {% end %} + {% end %} - # Iterate through all ancestors and add their methods. - type.ancestors.each do |ancestor| - [[:self.id, ancestor.class], [nil, ancestor]].each do |(receiver, t)| - t.methods.each do |method| - # Skip methods already found to prevent redefining them multiple times. - unless definitions.any? do |d| - m = d[:method] - m.name == method.name && - m.args == method.args && - m.splat_index == method.splat_index && - m.double_splat == method.double_splat && - m.block_arg == method.block_arg - end - definitions << { - type: t, - method: method, - scope: :super.id, - receiver: receiver, - } - end - end - end - end - - definitions = definitions.reject do |definition| - name = definition[:method].name - name.starts_with?("_spectator") || ::Spectator::DSL::RESERVED_KEYWORDS.includes?(name.symbolize) - end %} - - {% 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 method in type.methods.reject do |meth| + meth.name.starts_with?("_spectator") || + ::Spectator::DSL::RESERVED_KEYWORDS.includes?(meth.name.symbolize) + end %} + {{(method.abstract? ? :"abstract_stub abstract" : :default_stub).id}} {{method.visibility.id if method.visibility != :public}} def {{"#{method.receiver}.".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 %} - {% unless method.abstract? %} - {{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 %} + {% unless method.abstract? %} + {% if type == @type %}previous_def{% else %}super{% end %}{% if method.accepts_block? %} { |*%yargs| yield *%yargs }{% 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 # Utility macro for casting a stub (and its return value) to the correct type. @@ -537,30 +415,29 @@ module Spectator # Get the value as-is from the stub. # This will be compiled as a union of all known stubbed value types. %value = {{stub}}.call({{call}}) - %type = {{type}} # Attempt to cast the value to the method's return type. # 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. %cast = %value.as?({{type}}) - - {% if fail_cast == :nil %} + if %cast.is_a?({{type}}) %cast - {% elsif fail_cast == :raise %} - # Check if nil was returned by the stub and if its okay to return it. - if %value.nil? && %type.nilable? - # Value was nil and nil is allowed to be returned. - %type.cast(%cast) - elsif %cast.nil? + else + {% if fail_cast == :nil %} + nil + {% elsif fail_cast == :raise %} # The stubbed value was something else entirely and cannot be cast to the return type. - raise TypeCastError.new("#{_spectator_stubbed_name} received message #{ {{call}} } and is attempting to return a `#{%value.class}`, but returned type must be `#{%type}`.") - else - # Types match and value can be returned as cast type. - %cast - end - {% else %} - {% raise "fail_cast must be :nil, :raise, or :no_return, but got: #{fail_cast}" %} - {% end %} + # There's something weird going on (compiler bug?) that sometimes causes this class lookup to fail. + %type = begin + %value.class.to_s + rescue + "" + end + raise TypeCastError.new("#{_spectator_stubbed_name} received message #{ {{call}} } and is attempting to return a `#{%type}`, but returned type must be `#{ {{type}} }`.") + {% else %} + {% raise "fail_cast must be :nil, :raise, or :no_return, but got: #{fail_cast}" %} + {% end %} + end {% end %} end end diff --git a/src/spectator/node.cr b/src/spectator/node.cr index 6a5d068..807c8df 100644 --- a/src/spectator/node.cr +++ b/src/spectator/node.cr @@ -30,16 +30,14 @@ module Spectator end # User-defined tags and values used for filtering and behavior modification. - def metadata : Metadata - @metadata ||= Metadata.new - end + getter metadata : Metadata # Creates the node. # The *name* describes the purpose of the node. # It can be a `Symbol` to describe a type. # The *location* tracks where the node exists in source code. # A set of *metadata* can be used for filtering and modifying example behavior. - def initialize(@name : Label = nil, @location : Location? = nil, @metadata : Metadata? = nil) + def initialize(@name : Label = nil, @location : Location? = nil, @metadata : Metadata = Metadata.new) end # Indicates whether the node has completed. @@ -48,25 +46,17 @@ module Spectator # Checks if the node has been marked as pending. # Pending items should be skipped during execution. def pending? - return false unless md = @metadata - - md.has_key?(:pending) || md.has_key?(:skip) + metadata.has_key?(:pending) || metadata.has_key?(:skip) end # Gets the reason the node has been marked as pending. def pending_reason - return DEFAULT_PENDING_REASON unless md = @metadata - - md[:pending]? || md[:skip]? || md[:reason]? || DEFAULT_PENDING_REASON + metadata[:pending]? || metadata[:skip]? || metadata[:reason]? || DEFAULT_PENDING_REASON end # Retrieves just the tag names applied to the node. def tags - if md = @metadata - Tags.new(md.keys) - else - Tags.new - end + Tags.new(metadata.keys) end # Non-nil name used to show the node name. diff --git a/src/spectator/pass_result.cr b/src/spectator/pass_result.cr index 21ed6c5..20e3b04 100644 --- a/src/spectator/pass_result.cr +++ b/src/spectator/pass_result.cr @@ -9,7 +9,7 @@ module Spectator end # Calls the `pass` method on *visitor*. - def accept(visitor, &) + def accept(visitor) visitor.pass(yield self) end diff --git a/src/spectator/pending_example_builder.cr b/src/spectator/pending_example_builder.cr index 434efe5..a1f0292 100644 --- a/src/spectator/pending_example_builder.cr +++ b/src/spectator/pending_example_builder.cr @@ -11,7 +11,7 @@ module Spectator # 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. def initialize(@name : String? = nil, @location : Location? = nil, - @metadata : Metadata? = nil, @reason : String? = nil) + @metadata : Metadata = Metadata.new, @reason : String? = nil) end # Constructs an example with previously defined attributes. diff --git a/src/spectator/pending_result.cr b/src/spectator/pending_result.cr index 57f7fd7..cff38c5 100644 --- a/src/spectator/pending_result.cr +++ b/src/spectator/pending_result.cr @@ -28,7 +28,7 @@ module Spectator end # Calls the `pending` method on the *visitor*. - def accept(visitor, &) + def accept(visitor) visitor.pending(yield self) end diff --git a/src/spectator/should.cr b/src/spectator/should.cr index f0fe075..eb0733f 100644 --- a/src/spectator/should.cr +++ b/src/spectator/should.cr @@ -22,106 +22,51 @@ class Object # ``` # require "spectator/should" # ``` - def should(matcher, message = nil, *, _file = __FILE__, _line = __LINE__) + def should(matcher, message = nil) actual = ::Spectator::Value.new(self) - location = ::Spectator::Location.new(_file, _line) match_data = matcher.match(actual) - expectation = ::Spectator::Expectation.new(match_data, location, message) + expectation = ::Spectator::Expectation.new(match_data, message: message) ::Spectator::Harness.current.report(expectation) 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. # When `#should` succeeds, this method will fail, and vice-versa. - def should_not(matcher, message = nil, *, _file = __FILE__, _line = __LINE__) + def should_not(matcher, message = nil) 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) + expectation = ::Spectator::Expectation.new(match_data, message: message) ::Spectator::Harness.current.report(expectation) 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. # The expectation is checked after the example finishes and all hooks have run. - def should_eventually(matcher, message = nil, *, _file = __FILE__, _line = __LINE__) - ::Spectator::Harness.current.defer { should(matcher, message, _file: _file, _line: _line) } + def should_eventually(matcher, message = nil) + ::Spectator::Harness.current.defer { should(matcher, message) } end # 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. - def should_never(matcher, message = nil, *, _file = __FILE__, _line = __LINE__) - ::Spectator::Harness.current.defer { should_not(matcher, message, _file: _file, _line: _line) } + def should_never(matcher, message = nil) + ::Spectator::Harness.current.defer { should_not(matcher, message) } end end struct Proc(*T, R) # Extension method to create an expectation for a block of code (proc). # Depending on the matcher, the proc may be executed multiple times. - def should(matcher, message = nil, *, _file = __FILE__, _line = __LINE__) + def should(matcher, message = nil) actual = ::Spectator::Block.new(self) - location = ::Spectator::Location.new(_file, _line) match_data = matcher.match(actual) - expectation = ::Spectator::Expectation.new(match_data, location, message) + expectation = ::Spectator::Expectation.new(match_data, message: message) ::Spectator::Harness.current.report(expectation) end # Works the same as `#should` except the condition is inverted. # When `#should` succeeds, this method will fail, and vice-versa. - def should_not(matcher, message = nil, *, _file = __FILE__, _line = __LINE__) + def should_not(matcher, message = nil) actual = ::Spectator::Block.new(self) - location = ::Spectator::Location.new(_file, _line) match_data = matcher.negated_match(actual) - expectation = ::Spectator::Expectation.new(match_data, location, message) + expectation = ::Spectator::Expectation.new(match_data, message: message) ::Spectator::Harness.current.report(expectation) end end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 17b0284..265b41d 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -60,7 +60,7 @@ module Spectator # # 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. - def start_group(name, location = nil, metadata = nil) : Nil + def start_group(name, location = nil, metadata = Metadata.new) : Nil Log.trace { "Start group: #{name.inspect} @ #{location}; metadata: #{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. # 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 = nil) : Nil + def start_iterative_group(collection, name, iterator = nil, location = nil, metadata = Metadata.new) : Nil Log.trace { "Start iterative group: #{name} (#{typeof(collection)}) @ #{location}; metadata: #{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. # The return value of the block is ignored. # It is expected that the test code runs when the block is called. - def add_example(name, location, context_builder, metadata = nil, &block : Example -> _) : Nil + def add_example(name, location, context_builder, metadata = Metadata.new, &block : Example -> _) : Nil Log.trace { "Add example: #{name} @ #{location}; metadata: #{metadata}" } current << ExampleBuilder.new(context_builder, block, name, location, metadata) end @@ -144,7 +144,7 @@ module Spectator # 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. # A default *reason* can be given in case the user didn't provide one. - def add_pending_example(name, location, metadata = nil, reason = nil) : Nil + def add_pending_example(name, location, metadata = Metadata.new, reason = nil) : Nil Log.trace { "Add pending example: #{name} @ #{location}; metadata: #{metadata}" } current << PendingExampleBuilder.new(name, location, metadata, reason) end diff --git a/src/spectator/tag_node_filter.cr b/src/spectator/tag_node_filter.cr index 0dedd59..d360712 100644 --- a/src/spectator/tag_node_filter.cr +++ b/src/spectator/tag_node_filter.cr @@ -10,9 +10,7 @@ module Spectator # Checks whether the node satisfies the filter. def includes?(node) : Bool - return false unless metadata = node.metadata - - metadata.any? { |key, value| key.to_s == @tag && (!@value || value == @value) } + node.metadata.any? { |key, value| key.to_s == @tag && (!@value || value == @value) } end end end diff --git a/src/spectator/test_context.cr b/src/spectator/test_context.cr index e04fe56..a68c5b9 100644 --- a/src/spectator/test_context.cr +++ b/src/spectator/test_context.cr @@ -34,7 +34,7 @@ class SpectatorTestContext < SpectatorContext # Initial metadata for tests. # This method should be overridden by example groups and examples. - private def self.metadata : ::Spectator::Metadata? - nil + private def self.metadata + ::Spectator::Metadata.new end end diff --git a/src/spectator/wrapper.cr b/src/spectator/wrapper.cr index 76f6f44..9874dec 100644 --- a/src/spectator/wrapper.cr +++ b/src/spectator/wrapper.cr @@ -13,13 +13,18 @@ module Spectator # Creates a wrapper for the specified value. def initialize(value) - @pointer = Value.new(value).as(Void*) + @pointer = Box.box(value) end # Retrieves the previously wrapped value. # The *type* of the wrapped value must match otherwise an error will be raised. def get(type : T.class) : T forall T - @pointer.unsafe_as(Value(T)).get + {% begin %} + {% if T.nilable? %} + @pointer.null? ? nil : + {% end %} + Box(T).unbox(@pointer) + {% end %} end # Retrieves the previously wrapped value. @@ -34,20 +39,12 @@ module Spectator # type = wrapper.get { Int32 } # Returns Int32 # ``` def get(& : -> T) : T forall T - @pointer.unsafe_as(Value(T)).get - end - - # Wrapper for a value. - # Similar to `Box`, but doesn't segfault on nil and unions. - private class Value(T) - # Creates the wrapper. - def initialize(@value : T) - end - - # Retrieves the value. - def get : T - @value - end + {% begin %} + {% if T.nilable? %} + @pointer.null? ? nil : + {% end %} + Box(T).unbox(@pointer) + {% end %} end end end diff --git a/util/nightly.sh b/util/nightly.sh deleted file mode 100755 index 460a839..0000000 --- a/util/nightly.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/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 "$@" diff --git a/util/test-all-individually.sh b/util/test-all-individually.sh deleted file mode 100755 index 97bdd36..0000000 --- a/util/test-all-individually.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -e - -find spec/ -type f -name \*_spec.cr -print0 | \ - xargs -0 -n1 crystal spec --error-on-warnings -v