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/.gitlab-ci.yml b/.gitlab-ci.yml index d627d27..b3adb42 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,7 +13,7 @@ before_script: spec: script: - - crystal spec --error-on-warnings --junit_output=. spec/matchers/ spec/spectator/*.cr + - crystal spec --error-on-warnings --junit_output=. spec/runtime_example_spec.cr spec/matchers/ spec/spectator/*.cr artifacts: when: always paths: diff --git a/CHANGELOG.md b/CHANGELOG.md index f2d8d66..cc33e24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,82 +4,6 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.12.1] - 2024-08-13 -### Fixed -- Fixed some global namespace issues with Crystal 1.13. [#57](https://github.com/icy-arctic-fox/spectator/pull/57) Thanks @GrantBirki ! -- Remove usage of deprecated double splat in macros. - -## [0.12.0] - 2024-02-03 -### Added -- Added ability to use matchers for case equality. [#55](https://github.com/icy-arctic-fox/spectator/issues/55) -- Added support for nested case equality when checking arguments with Array, Tuple, Hash, and NamedTuple. - -### Fixed -- Fixed some issues with the `be_within` matcher when used with expected and union types. - -## [0.11.7] - 2023-10-16 -### Fixed -- Fix memoized value (`let`) with a union type causing segfault. [#81](https://gitlab.com/arctic-fox/spectator/-/issues/81) - -## [0.11.6] - 2023-01-26 -### Added -- Added ability to cast types using the return value from expect/should statements with a type matcher. -- Added support for string interpolation in context names/labels. - -### Fixed -- Fix invalid syntax (unterminated call) when recording calls to stubs with an un-named splat. [#51](https://github.com/icy-arctic-fox/spectator/issues/51) -- Fix malformed method signature when using named splat with keyword arguments in mocked type. [#49](https://github.com/icy-arctic-fox/spectator/issues/49) - -### Changed -- Expectations using 'should' syntax report file and line where the 'should' keyword is instead of the test start. -- Add non-captured block argument in preparation for Crystal 1.8.0. - -## [0.11.5] - 2022-12-18 -### Added -- Added support for mock modules and types that include mocked modules. - -### Fixed -- Fix macro logic to support free variables, 'self', and variants on stubbed methods. [#48](https://github.com/icy-arctic-fox/spectator/issues/48) -- Fix method stubs used on methods that capture blocks. -- Fix type name resolution for when using custom types in a mocked typed. -- Prevent comparing range arguments with non-compatible types in stubs. [#48](https://github.com/icy-arctic-fox/spectator/issues/48) - -### Changed -- Simplify string representation of mock-related types. -- Remove unnecessary redefinitions of methods when adding stub functionality to a type. -- Allow metadata to be stored as nil to reduce overhead when tracking nodes without tags. -- Use normal equality (==) instead of case-equality (===) with proc arguments in stubs. -- Change stub value cast logic to avoid compiler bug. [#80](https://gitlab.com/arctic-fox/spectator/-/issues/80) - -## [0.11.4] - 2022-11-27 -### Added -- Add support for using named (keyword) arguments in place of positional arguments in stubs. [#47](https://github.com/icy-arctic-fox/spectator/issues/47) -- Add `before`, `after`, and `around` as aliases for `before_each`, `after_each`, and `around_each` respectively. - -### Fixed -- Clear stubs defined with `expect().to receive()` syntax after test finishes to prevent leakage between tests. -- Ensure stubs defined with `allow().to receive()` syntax are cleared after test finishes when used inside a test (another leakage). -- Fix crash caused when logging is enabled after running an example that attempts to exit. - -### Removed -- Removed support for stubbing undefined (untyped) methods in lazy doubles. Avoids possible segfault. - -## [0.11.3] - 2022-09-03 -### Fixed -- Display error block (failure message and stack trace) when using `fail`. [#78](https://gitlab.com/arctic-fox/spectator/-/issues/78) -- Defining a custom matcher outside of the `Spectator` namespace no longer produces a compilation error. [#46](https://github.com/icy-arctic-fox/spectator/issues/46) - -## [0.11.2] - 2022-08-07 -### Fixed -- `expect_raises` with block and no arguments produces compilation error. [#77](https://gitlab.com/arctic-fox/spectator/-/issues/77) - -### Changed -- `-e` (`--example`) CLI option performs a partial match instead of exact match. [#71](https://gitlab.com/arctic-fox/spectator/-/issues/71) [#45](https://github.com/icy-arctic-fox/spectator/issues/45) - -## [0.11.1] - 2022-07-18 -### Fixed -- Workaround nilable type issue with memoized value. [#76](https://gitlab.com/arctic-fox/spectator/-/issues/76) - ## [0.11.0] - 2022-07-14 ### Changed - Overhauled mock and double system. [#63](https://gitlab.com/arctic-fox/spectator/-/issues/63) @@ -463,16 +387,7 @@ This has been changed so that it compiles and raises an error at runtime with a First version ready for public use. -[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 -[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 +[Unreleased]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.11.0...master [0.11.0]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.10.6...v0.11.0 [0.10.6]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.10.5...v0.10.6 [0.10.5]: https://gitlab.com/arctic-fox/spectator/-/compare/v0.10.4...v0.10.5 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..dc55c9b 100644 --- a/shard.yml +++ b/shard.yml @@ -1,16 +1,16 @@ name: spectator -version: 0.12.1 +version: 0.11.0 description: | - Feature-rich testing framework for Crystal inspired by RSpec. + A feature-rich spec testing framework for Crystal with similarities to RSpec. authors: - Michael Miller -crystal: 1.6.0 +crystal: 1.5.0 license: MIT development_dependencies: ameba: github: crystal-ameba/ameba - version: ~> 1.2.0 + version: ~> 1.0.0 diff --git a/spec/docs/custom_matchers_spec.cr b/spec/docs/custom_matchers_spec.cr deleted file mode 100644 index d3ee565..0000000 --- a/spec/docs/custom_matchers_spec.cr +++ /dev/null @@ -1,91 +0,0 @@ -require "../spec_helper" - -# https://gitlab.com/arctic-fox/spectator/-/wikis/Custom-Matchers -Spectator.describe "Custom Matchers Docs" do - context "value matcher" do - # Sub-type of Matcher to suit our needs. - # Notice this is a struct. - struct MultipleOfMatcher(ExpectedType) < Spectator::Matchers::ValueMatcher(ExpectedType) - # Short text about the matcher's purpose. - # This explains what condition satisfies the matcher. - # The description is used when the one-liner syntax is used. - def description : String - "is a multiple of #{expected.label}" - end - - # Checks whether the matcher is satisfied with the expression given to it. - private def match?(actual : Spectator::Expression(T)) : Bool forall T - actual.value % expected.value == 0 - end - - # Message displayed when the matcher isn't satisfied. - # The message should typically only contain the test expression labels. - private def failure_message(actual : Spectator::Expression(T)) : String forall T - "#{actual.label} is not a multiple of #{expected.label}" - end - - # Message displayed when the matcher isn't satisfied and is negated. - # This is essentially what would satisfy the matcher if it wasn't negated. - # The message should typically only contain the test expression labels. - private def failure_message_when_negated(actual : Spectator::Expression(T)) : String forall T - "#{actual.label} is a multiple of #{expected.label}" - end - end - - # The DSL portion of the matcher. - # This captures the test expression and creates an instance of the matcher. - macro be_a_multiple_of(expected) - %value = ::Spectator::Value.new({{expected}}, {{expected.stringify}}) - MultipleOfMatcher.new(%value) - end - - specify do - expect(9).to be_a_multiple_of(3) - # or negated: - expect(5).to_not be_a_multiple_of(2) - end - - specify "failure messages" do - expect { expect(9).to be_a_multiple_of(5) }.to raise_error(Spectator::ExpectationFailed, "9 is not a multiple of 5") - expect { expect(6).to_not be_a_multiple_of(3) }.to raise_error(Spectator::ExpectationFailed, "6 is a multiple of 3") - end - end - - context "standard matcher" do - struct OddMatcher < Spectator::Matchers::StandardMatcher - def description : String - "is odd" - end - - private def match?(actual : Spectator::Expression(T)) : Bool forall T - actual.value % 2 == 1 - end - - private def failure_message(actual : Spectator::Expression(T)) : String forall T - "#{actual.label} is not odd" - end - - private def failure_message_when_negated(actual : Spectator::Expression(T)) : String forall T - "#{actual.label} is odd" - end - - private def does_not_match?(actual : Spectator::Expression(T)) : Bool forall T - actual.value % 2 == 0 - end - end - - macro be_odd - OddMatcher.new - end - - specify do - expect(9).to be_odd - expect(2).to_not be_odd - end - - specify "failure messages" do - expect { expect(2).to be_odd }.to raise_error(Spectator::ExpectationFailed, "2 is not odd") - expect { expect(3).to_not be_odd }.to raise_error(Spectator::ExpectationFailed, "3 is odd") - end - end -end diff --git a/spec/docs/mocks_spec.cr b/spec/docs/mocks_spec.cr index 58c16f8..6e164ae 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 @@ -249,9 +146,9 @@ Spectator.describe "Mocks Docs" do inst.something end - it "reverts to default stub for other examples" do + it "leaks stubs to other examples" do inst = mock(MyStruct) - expect(inst.something).to eq(5) # Default stub used instead of original behavior. + expect(inst.something).to eq(7) # Previous stub was leaked. end end end 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/helpers/.gitkeep b/spec/helpers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/spec/helpers/example.cr b/spec/helpers/example.cr new file mode 100644 index 0000000..34034ec --- /dev/null +++ b/spec/helpers/example.cr @@ -0,0 +1,71 @@ +require "ecr" +require "json" +require "./result" + +module Spectator::SpecHelpers + # Wrapper for compiling and running an example at runtime and getting a result. + class Example + # Creates the example. + # The *spec_helper_path* is the path to spec_helper.cr file. + # The name or ID of the example is given by *example_id*. + # Lastly, the source code for the example is given by *example_code*. + def initialize(@spec_helper_path : String, @example_id : String, @example_code : String) + end + + # Instructs the Crystal compiler to compile the test. + # Returns an instance of `JSON::Any`. + # This will be the outcome and information about the test. + # Output will be suppressed for the test. + # If an error occurs while attempting to compile and run the test, an error will be raised. + def compile + # Create a temporary file containing the test. + with_tempfile do |source_file| + args = ["run", "--no-color", source_file, "--", "--json"] + Process.run(crystal_executable, args) do |process| + JSON.parse(process.output) + rescue JSON::ParseException + raise "Compilation of example #{@example_id} failed\n\n#{process.error.gets_to_end}" + end + end + end + + # Same as `#compile`, but returns the result of the first example in the test. + # Returns a `SpectatorHelpers::Result` instance. + def result + output = compile + example = output["examples"][0] + Result.from_json_any(example) + end + + # Constructs the string representation of the example. + # This produces the Crystal source code. + # *io* is the file handle to write to. + # The *dir* is the directory of the file being written to. + # This is needed to resolve the relative path to the spec_helper.cr file. + private def write(io, dir) + spec_helper_path = Path[@spec_helper_path].relative_to(dir) # ameba:disable Lint/UselessAssign + ECR.embed(__DIR__ + "/example.ecr", io) + end + + # Creates a temporary file containing the compilable example code. + # Yields the path of the temporary file. + # Ensures the file is deleted after it is done being used. + private def with_tempfile + tempfile = File.tempfile("_#{@example_id}_spec.cr") do |file| + dir = File.dirname(file.path) + write(file, dir) + end + + begin + yield tempfile.path + ensure + tempfile.delete + end + end + + # Attempts to find the Crystal compiler on the system or raises an error. + private def crystal_executable + Process.find_executable("crystal") || raise("Could not find Crystal compiler") + end + end +end diff --git a/spec/helpers/example.ecr b/spec/helpers/example.ecr new file mode 100644 index 0000000..53355bf --- /dev/null +++ b/spec/helpers/example.ecr @@ -0,0 +1,5 @@ +require "<%= spec_helper_path %>" + +Spectator.describe "<%= @example_id %>" do + <%= @example_code %> +end diff --git a/spec/helpers/expectation.cr b/spec/helpers/expectation.cr new file mode 100644 index 0000000..fd4d84d --- /dev/null +++ b/spec/helpers/expectation.cr @@ -0,0 +1,28 @@ +module Spectator::SpecHelpers + # Information about an `expect` call in an example. + struct Expectation + # Indicates whether the expectation passed or failed. + getter? satisfied : Bool + + # Message when the expectation failed. + # Only available when `#satisfied?` is false. + getter! message : String + + # Additional information about the expectation. + # Only available when `#satisfied?` is false. + getter! values : Hash(String, String) + + # Creates the expectation outcome. + def initialize(@satisfied, @message, @values) + end + + # Extracts the expectation information from a `JSON::Any` object. + def self.from_json_any(object : JSON::Any) + satisfied = object["satisfied"].as_bool + message = object["failure"]?.try(&.as_s?) + values = object["values"]?.try(&.as_h?) + values = values.transform_values(&.as_s) if values + new(satisfied, message, values) + end + end +end diff --git a/spec/helpers/result.cr b/spec/helpers/result.cr new file mode 100644 index 0000000..edafd75 --- /dev/null +++ b/spec/helpers/result.cr @@ -0,0 +1,67 @@ +module Spectator::SpecHelpers + # Information about an example compiled and run at runtime. + struct Result + # Status of the example after running. + enum Outcome + Success + Failure + Error + Unknown + end + + # Full name and description of the example. + getter name : String + + # Status of the example after running. + getter outcome : Outcome + + # List of expectations ran in the example. + getter expectations : Array(Expectation) + + # Creates the result. + def initialize(@name, @outcome, @expectations) + end + + # Checks if the example was successful. + def success? + outcome.success? + end + + # :ditto: + def successful? + outcome.success? + end + + # Checks if the example failed, but did not error. + def failure? + outcome.failure? + end + + # Checks if the example encountered an error. + def error? + outcome.error? + end + + # Extracts the result information from a `JSON::Any` object. + def self.from_json_any(object : JSON::Any) + name = object["description"].as_s + outcome = parse_outcome_string(object["status"].as_s) + expectations = if (list = object["expectations"].as_a?) + list.map { |e| Expectation.from_json_any(e) } + else + [] of Expectation + end + new(name, outcome, expectations) + end + + # Converts a result string, such as "fail" to an enum value. + private def self.parse_outcome_string(string) + case string + when /pass/i then Outcome::Success + when /fail/i then Outcome::Failure + when /error/i then Outcome::Error + else Outcome::Unknown + end + end + end +end diff --git a/spec/issues/github_issue_44_spec.cr b/spec/issues/github_issue_44_spec.cr index da6cbf3..0ab646d 100644 --- a/spec/issues/github_issue_44_spec.cr +++ b/spec/issues/github_issue_44_spec.cr @@ -9,32 +9,12 @@ Spectator.describe "GitHub Issue #44" do let(command) { "ls -l" } let(exception) { File::NotFoundError.new("File not found", file: "test.file") } - context "with positional arguments" do - before_each do - pipe = Process::Redirect::Pipe - expect(Process).to receive(:run).with(command, nil, nil, false, true, pipe, pipe, pipe, nil).and_raise(exception) - end - - it "must stub Process.run" do - expect do - Process.run(command, shell: true, output: :pipe) do |_process| - end - end.to raise_error(File::NotFoundError, "File not found") - end + before_each do + expect(Process).to receive(:run).with(command, shell: true, output: :pipe).and_raise(exception) end - # Original issue uses keyword arguments in place of positional arguments. - context "keyword arguments in place of positional arguments" do - before_each do - pipe = Process::Redirect::Pipe - expect(Process).to receive(:run).with(command, shell: true, output: pipe).and_raise(exception) - end - - it "must stub Process.run" do - expect do - Process.run(command, shell: true, output: :pipe) do |_process| - end - end.to raise_error(File::NotFoundError, "File not found") + skip "must stub Process.run", skip: "Method mock not applied" do + Process.run(command, shell: true, output: :pipe) do |_process| end end end diff --git a/spec/issues/github_issue_47_spec.cr b/spec/issues/github_issue_47_spec.cr deleted file mode 100644 index 3576a2d..0000000 --- a/spec/issues/github_issue_47_spec.cr +++ /dev/null @@ -1,18 +0,0 @@ -require "../spec_helper" - -Spectator.describe "GitHub Issue #47" do - class Original - def foo(arg1, arg2) - # ... - end - end - - mock Original - - let(fake) { mock(Original) } - - specify do - expect(fake).to receive(:foo).with("arg1", arg2: "arg2") - fake.foo("arg1", "arg2") - end -end 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 deleted file mode 100644 index 996af80..0000000 --- a/spec/issues/gitlab_issue_51_spec.cr +++ /dev/null @@ -1,109 +0,0 @@ -require "../spec_helper" - -module GitLabIssue51 - class Foo - def call(str : String) : String? - "" - end - - def alt1_call(str : String) : String? - nil - end - - def alt2_call(str : String) : String? - [str, nil].sample - end - end - - class Bar - def call(a_foo) : Nil # Must add nil restriction here, otherwise a segfault occurs from returning the result of #alt2_call. - a_foo.call("") - a_foo.alt1_call("") - a_foo.alt2_call("") - end - end -end - -Spectator.describe GitLabIssue51::Bar do - mock GitLabIssue51::Foo, call: "", alt1_call: "", alt2_call: "" - - let(:foo) { mock(GitLabIssue51::Foo) } - subject(:call) { described_class.new.call(foo) } - - describe "#call" do - it "invokes Foo#call" do - call - expect(foo).to have_received(:call) - end - - it "invokes Foo#alt1_call" do - call - expect(foo).to have_received(:alt1_call) - end - - it "invokes Foo#alt2_call" do - call - expect(foo).to have_received(:alt2_call) - end - - describe "with an explicit return of nil" do - it "should invoke Foo#call?" do - allow(foo).to receive(:call).and_return(nil) - call - expect(foo).to have_received(:call) - end - - it "invokes Foo#alt1_call" do - allow(foo).to receive(:alt1_call).and_return(nil) - call - expect(foo).to have_received(:alt1_call) - end - - it "invokes Foo#alt2_call" do - allow(foo).to receive(:alt2_call).and_return(nil) - call - expect(foo).to have_received(:alt2_call) - end - end - - describe "with returns set in before_each for all calls" do - before_each do - allow(foo).to receive(:call).and_return(nil) - allow(foo).to receive(:alt1_call).and_return(nil) - allow(foo).to receive(:alt2_call).and_return(nil) - end - - it "should invoke Foo#call?" do - call - expect(foo).to have_received(:call) - end - - it "should invoke Foo#alt1_call?" do - call - expect(foo).to have_received(:alt1_call) - end - - it "should invoke Foo#alt2_call?" do - call - expect(foo).to have_received(:alt2_call) - end - end - - describe "with returns set in before_each for alt calls only" do - before_each do - allow(foo).to receive(:alt1_call).and_return(nil) - allow(foo).to receive(:alt2_call).and_return(nil) - end - - it "invokes Foo#alt1_call" do - call - expect(foo).to have_received(:alt1_call) - end - - it "invokes Foo#alt2_call" do - call - expect(foo).to have_received(:alt2_call) - end - end - end -end diff --git a/spec/issues/gitlab_issue_76_spec.cr b/spec/issues/gitlab_issue_76_spec.cr deleted file mode 100644 index 3427af8..0000000 --- a/spec/issues/gitlab_issue_76_spec.cr +++ /dev/null @@ -1,6 +0,0 @@ -require "../spec_helper" - -Spectator.describe "GitLab Issue #76" do - let(:value) { nil.as(Int32?) } - specify { expect(value).to be_nil } -end diff --git a/spec/issues/gitlab_issue_77_spec.cr b/spec/issues/gitlab_issue_77_spec.cr deleted file mode 100644 index f13c1b7..0000000 --- a/spec/issues/gitlab_issue_77_spec.cr +++ /dev/null @@ -1,10 +0,0 @@ -require "../spec_helper" - -# https://gitlab.com/arctic-fox/spectator/-/issues/77 -Spectator.describe "GitLab Issue #77" do - it "fails" do - expect_raises do - raise "Error!" - end - end -end 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/matchers/receive_matcher_spec.cr b/spec/matchers/receive_matcher_spec.cr index b5addc5..6dfdd28 100644 --- a/spec/matchers/receive_matcher_spec.cr +++ b/spec/matchers/receive_matcher_spec.cr @@ -169,7 +169,7 @@ Spectator.describe Spectator::Matchers::ReceiveMatcher do end context "with method calls" do - before do + before_each do dbl.test_method dbl.test_method(1, "wrong", :xyz, foo: "foobarbaz") dbl.irrelevant("foo") @@ -289,14 +289,14 @@ Spectator.describe Spectator::Matchers::ReceiveMatcher do pre_condition { expect(match_data).to be_a(failed_match) } - before do + before_each do dbl.test_method dbl.test_method(1, "test", :xyz, foo: "foobarbaz") dbl.irrelevant("foo") end it "has the expected call listed" do - is_expected.to contain({:expected, "Not #{stub.message}"}) + is_expected.to contain({:expected, "Not #{stub}"}) end it "has the list of called methods" do diff --git a/spec/rspec/core/explicit_subject_spec.cr b/spec/rspec/core/explicit_subject_spec.cr index 075bad7..3775b8a 100644 --- a/spec/rspec/core/explicit_subject_spec.cr +++ b/spec/rspec/core/explicit_subject_spec.cr @@ -52,7 +52,7 @@ Spectator.describe "Explicit Subject" do describe Array(Int32) do # TODO: Multiple arguments to describe/context. subject { [] of Int32 } - before { subject.push(1, 2, 3) } + before_each { subject.push(1, 2, 3) } it "has the prescribed elements" do expect(subject).to eq([1, 2, 3]) diff --git a/spec/runtime_example_spec.cr b/spec/runtime_example_spec.cr new file mode 100644 index 0000000..01ae9a3 --- /dev/null +++ b/spec/runtime_example_spec.cr @@ -0,0 +1,58 @@ +require "./spec_helper" + +# This is a meta test that ensures specs can be compiled and run at runtime. +# The purpose of this is to report an error if this process fails. +# Other tests will fail, but display a different name/description of the test. +# This clearly indicates that runtime testing failed. +# +# Runtime compilation is used to get output of tests as well as check syntax. +# Some specs are too complex to be ran normally. +# Additionally, this allows examples to easily check specific failure cases. +# Plus, it makes testing user-reported issues easy. +Spectator.describe "Runtime compilation", :slow, :compile do + given_example passing_example do + it "does something" do + expect(true).to be_true + end + end + + it "can compile and retrieve the result of an example" do + expect(passing_example).to be_successful + end + + it "can retrieve expectations" do + expect(passing_example.expectations).to_not be_empty + end + + given_example failing_example do + it "does something" do + expect(true).to be_false + end + + it "doesn't run" do + expect(true).to be_false + end + end + + it "detects failed examples" do + expect(failing_example).to be_failure + end + + given_example malformed_example do + it "does something" do + asdf + end + end + + it "raises on compilation errors" do + expect { malformed_example }.to raise_error(/compilation/i) + end + + given_expectation satisfied_expectation do + expect(true).to be_true + end + + it "can compile and retrieve expectations" do + expect(satisfied_expectation).to be_satisfied + end +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index e2f9578..4c16fd3 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -15,3 +15,35 @@ end macro specify_fails(description = nil, &block) it_fails {{description}} {{block}} end + +# Defines an example ("it" block) that is lazily compiled. +# When the example is referenced with *id*, it will be compiled and the results retrieved. +# The value returned by *id* will be a `Spectator::SpecHelpers::Result`. +# This allows the test result to be inspected. +macro given_example(id, &block) + let({{id}}) do + ::Spectator::SpecHelpers::Example.new( + {{__FILE__}}, + {{id.id.stringify}}, + {{block.body.stringify}} + ).result + end +end + +# Defines an example ("it" block) that is lazily compiled. +# The "it" block must be omitted, as the block provided to this macro will be wrapped in one. +# When the expectation is referenced with *id*, it will be compiled and the result retrieved. +# The value returned by *id* will be a `Spectator::SpecHelpers::Expectation`. +# This allows an expectation to be inspected. +# Only the last expectation performed will be returned. +# An error is raised if no expectations ran. +macro given_expectation(id, &block) + let({{id}}) do + result = ::Spectator::SpecHelpers::Example.new( + {{__FILE__}}, + {{id.id.stringify}}, + {{"it do\n" + block.body.stringify + "\nend"}} + ).result + result.expectations.last || raise("No expectations found from {{id.id}}") + end +end diff --git a/spec/spectator/dsl/mocks/allow_receive_spec.cr b/spec/spectator/dsl/mocks/allow_receive_spec.cr deleted file mode 100644 index 473c74b..0000000 --- a/spec/spectator/dsl/mocks/allow_receive_spec.cr +++ /dev/null @@ -1,188 +0,0 @@ -require "../../../spec_helper" - -Spectator.describe "Allow stub DSL" do - context "with a double" do - double(:dbl) do - # Ensure the original is never called. - stub abstract def foo : Nil - stub abstract def foo(arg) : Nil - stub abstract def value : Int32 - end - - let(dbl) { double(:dbl) } - - # Ensure invocations don't leak between examples. - pre_condition { expect(dbl).to_not have_received(:foo), "Leaked method calls from previous examples" } - - # Ensure stubs don't leak between examples. - pre_condition do - expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage) - end - - it "matches when a message is received" do - allow(dbl).to receive(:foo) - expect { dbl.foo }.to_not raise_error - end - - it "returns the correct value" do - allow(dbl).to receive(:value).and_return(42) - expect(dbl.value).to eq(42) - end - - it "matches when a message is received with matching arguments" do - allow(dbl).to receive(:foo).with(:bar) - expect { dbl.foo(:bar) }.to_not raise_error - end - - it "raises when a message without arguments is received" do - allow(dbl).to receive(:foo).with(:bar) - expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage, /foo/) - end - - it "raises when a message with different arguments is received" do - allow(dbl).to receive(:foo).with(:baz) - expect { dbl.foo(:bar) }.to raise_error(Spectator::UnexpectedMessage, /foo/) - end - end - - context "with a class double" do - double(:dbl) do - # Ensure the original is never called. - abstract_stub def self.foo : Nil - end - - abstract_stub def self.foo(arg) : Nil - end - - abstract_stub def self.value : Int32 - 42 - end - end - - let(dbl) { class_double(:dbl) } - - # Ensure invocations don't leak between examples. - pre_condition { expect(dbl).to_not have_received(:foo), "Leaked method calls from previous examples" } - - # Ensure stubs don't leak between examples. - pre_condition do - expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage) - end - - it "matches when a message is received" do - allow(dbl).to receive(:foo) - expect { dbl.foo }.to_not raise_error - end - - it "returns the correct value" do - allow(dbl).to receive(:value).and_return(42) - expect(dbl.value).to eq(42) - end - - it "matches when a message is received with matching arguments" do - allow(dbl).to receive(:foo).with(:bar) - expect { dbl.foo(:bar) }.to_not raise_error - end - - it "raises when a message without arguments is received" do - allow(dbl).to receive(:foo).with(:bar) - expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage, /foo/) - end - - it "raises when a message with different arguments is received" do - allow(dbl).to receive(:foo).with(:baz) - expect { dbl.foo(:bar) }.to raise_error(Spectator::UnexpectedMessage, /foo/) - end - end - - context "with a mock" do - abstract class MyClass - abstract def foo : Int32 - abstract def foo(arg) : Int32 - end - - mock(MyClass) - - let(fake) { mock(MyClass) } - - # Ensure invocations don't leak between examples. - pre_condition { expect(fake).to_not have_received(:foo), "Leaked method calls from previous examples" } - - # Ensure stubs don't leak between examples. - pre_condition do - expect { fake.foo }.to raise_error(Spectator::UnexpectedMessage) - end - - it "matches when a message is received" do - allow(fake).to receive(:foo).and_return(42) - expect(fake.foo).to eq(42) - end - - it "returns the correct value" do - allow(fake).to receive(:foo).and_return(42) - expect(fake.foo).to eq(42) - end - - it "matches when a message is received with matching arguments" do - allow(fake).to receive(:foo).with(:bar).and_return(42) - expect(fake.foo(:bar)).to eq(42) - end - - it "raises when a message without arguments is received" do - allow(fake).to receive(:foo).with(:bar) - expect { fake.foo }.to raise_error(Spectator::UnexpectedMessage, /foo/) - end - - it "raises when a message with different arguments is received" do - allow(fake).to receive(:foo).with(:baz) - expect { fake.foo(:bar) }.to raise_error(Spectator::UnexpectedMessage, /foo/) - end - end - - context "with a class mock" do - class MyClass - def self.foo : Int32 - 42 - end - - def self.foo(arg) : Int32 - 42 - end - end - - mock(MyClass) - - let(fake) { class_mock(MyClass) } - - # Ensure invocations don't leak between examples. - pre_condition { expect(fake).to_not have_received(:foo), "Leaked method calls from previous examples" } - - # Ensure stubs don't leak between examples. - pre_condition { expect(fake.foo).to eq(42) } - - it "matches when a message is received" do - allow(fake).to receive(:foo).and_return(0) - expect(fake.foo).to eq(0) - end - - it "returns the correct value" do - allow(fake).to receive(:foo).and_return(0) - expect(fake.foo).to eq(0) - end - - it "matches when a message is received with matching arguments" do - allow(fake).to receive(:foo).with(:bar).and_return(0) - expect(fake.foo(:bar)).to eq(0) - end - - it "calls the original when a message without arguments is received" do - allow(fake).to receive(:foo).with(:bar) - expect(fake.foo).to eq(42) - end - - it "calls the original when a message with different arguments is received" do - allow(fake).to receive(:foo).with(:baz) - expect(fake.foo(:bar)).to eq(42) - end - end -end diff --git a/spec/spectator/dsl/mocks/double_spec.cr b/spec/spectator/dsl/mocks/double_spec.cr index 5547ae0..d3eda6d 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 @@ -312,7 +312,7 @@ Spectator.describe "Double DSL", :smoke do let(override) { :override } let(dbl) { double(:context_double, override: override) } - before { allow(dbl).to receive(:memoize).and_return(memoize) } + before_each { allow(dbl).to receive(:memoize).and_return(memoize) } it "doesn't change predefined values" do expect(dbl.predefined).to eq(:predefined) diff --git a/spec/spectator/dsl/mocks/expect_receive_spec.cr b/spec/spectator/dsl/mocks/expect_receive_spec.cr index a249ad3..0a0242b 100644 --- a/spec/spectator/dsl/mocks/expect_receive_spec.cr +++ b/spec/spectator/dsl/mocks/expect_receive_spec.cr @@ -14,12 +14,6 @@ Spectator.describe "Deferred stub expectation DSL" do # Ensure invocations don't leak between examples. pre_condition { expect(dbl).to_not have_received(:foo), "Leaked method calls from previous examples" } - # Ensure stubs don't leak between examples. - pre_condition do - expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage) - dbl._spectator_clear_calls # Don't include previous call in results. - end - it "matches when a message is received" do expect(dbl).to receive(:foo) dbl.foo @@ -73,12 +67,6 @@ Spectator.describe "Deferred stub expectation DSL" do # Ensure invocations don't leak between examples. pre_condition { expect(dbl).to_not have_received(:foo), "Leaked method calls from previous examples" } - # Ensure stubs don't leak between examples. - pre_condition do - expect { dbl.foo }.to raise_error(Spectator::UnexpectedMessage) - dbl._spectator_clear_calls # Don't include previous call in results. - end - it "matches when a message is received" do expect(dbl).to receive(:foo) dbl.foo @@ -126,12 +114,6 @@ Spectator.describe "Deferred stub expectation DSL" do # Ensure invocations don't leak between examples. pre_condition { expect(fake).to_not have_received(:foo), "Leaked method calls from previous examples" } - # Ensure stubs don't leak between examples. - pre_condition do - expect { fake.foo }.to raise_error(Spectator::UnexpectedMessage) - fake._spectator_clear_calls # Don't include previous call in results. - end - it "matches when a message is received" do expect(fake).to receive(:foo).and_return(42) fake.foo(:bar) @@ -184,20 +166,14 @@ Spectator.describe "Deferred stub expectation DSL" do # Ensure invocations don't leak between examples. pre_condition { expect(fake).to_not have_received(:foo), "Leaked method calls from previous examples" } - # Ensure stubs don't leak between examples. - pre_condition do - expect(fake.foo).to eq(42) - fake._spectator_clear_calls # Don't include previous call in results. - end - it "matches when a message is received" do - expect(fake).to receive(:foo).and_return(0) + expect(fake).to receive(:foo).and_return(42) fake.foo(:bar) end it "returns the correct value" do - expect(fake).to receive(:foo).and_return(0) - expect(fake.foo).to eq(0) + expect(fake).to receive(:foo).and_return(42) + expect(fake.foo).to eq(42) end it "matches when a message isn't received" do @@ -205,12 +181,12 @@ Spectator.describe "Deferred stub expectation DSL" do end it "matches when a message is received with matching arguments" do - expect(fake).to receive(:foo).with(:bar).and_return(0) + expect(fake).to receive(:foo).with(:bar).and_return(42) fake.foo(:bar) end it "matches when a message without arguments is received" do - expect(fake).to_not receive(:foo).with(:bar).and_return(0) + expect(fake).to_not receive(:foo).with(:bar).and_return(42) fake.foo end @@ -219,7 +195,7 @@ Spectator.describe "Deferred stub expectation DSL" do end it "matches when a message with arguments isn't received" do - expect(fake).to_not receive(:foo).with(:baz).and_return(0) + expect(fake).to_not receive(:foo).with(:baz).and_return(42) fake.foo(:bar) end end diff --git a/spec/spectator/dsl/mocks/mock_spec.cr b/spec/spectator/dsl/mocks/mock_spec.cr index cd57cdc..d636826 100644 --- a/spec/spectator/dsl/mocks/mock_spec.cr +++ b/spec/spectator/dsl/mocks/mock_spec.cr @@ -11,7 +11,7 @@ Spectator.describe "Mock DSL", :smoke do args[1].as(Int32), args[2].as(Int32), }, - args[:kwarg].as(Int32), + args[3].as(Int32), { x: args[:x].as(Int32), y: args[:y].as(Int32), @@ -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 @@ -947,7 +947,7 @@ Spectator.describe "Mock DSL", :smoke do let(override) { :override } let(fake) { mock(Dummy, override: override) } - before { allow(fake).to receive(:memoize).and_return(memoize) } + before_each { allow(fake).to receive(:memoize).and_return(memoize) } it "doesn't change predefined values" do expect(fake.predefined).to eq(:predefined) @@ -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/allow_spec.cr b/spec/spectator/mocks/allow_spec.cr index 090014e..5bc16a8 100644 --- a/spec/spectator/mocks/allow_spec.cr +++ b/spec/spectator/mocks/allow_spec.cr @@ -9,31 +9,5 @@ Spectator.describe Spectator::Allow do it "applies a stub" do expect { alw.to(stub) }.to change { dbl.foo }.from(42).to(123) end - - context "leak" do - class Thing - def foo - 42 - end - end - - mock Thing - - getter(thing : Thing) { mock(Thing) } - - # Workaround type restrictions requiring a constant. - def fake - class_mock(Thing).cast(thing) - end - - specify do - expect { allow(fake).to(stub) }.to change { fake.foo }.from(42).to(123) - end - - # This example must be run after the previous (random order may break this). - it "clears the stub after the example completes" do - expect { fake.foo }.to eq(42) - end - end end end diff --git a/spec/spectator/mocks/arguments_spec.cr b/spec/spectator/mocks/arguments_spec.cr index f6a09b7..422b6c6 100644 --- a/spec/spectator/mocks/arguments_spec.cr +++ b/spec/spectator/mocks/arguments_spec.cr @@ -1,15 +1,21 @@ require "../../spec_helper" Spectator.describe Spectator::Arguments do - subject(arguments) { Spectator::Arguments.new({42, "foo"}, {bar: "baz", qux: 123}) } - - it "stores the arguments" do - expect(arguments).to have_attributes( + subject(arguments) do + Spectator::Arguments.new( args: {42, "foo"}, kwargs: {bar: "baz", qux: 123} ) end + it "stores the arguments" do + expect(arguments.args).to eq({42, "foo"}) + end + + it "stores the keyword arguments" do + expect(arguments.kwargs).to eq({bar: "baz", qux: 123}) + end + describe ".capture" do subject { Spectator::Arguments.capture(42, "foo", bar: "baz", qux: 123) } @@ -18,20 +24,22 @@ Spectator.describe Spectator::Arguments do end end - describe "#[](index)" do - it "returns a positional argument" do - aggregate_failures do - expect(arguments[0]).to eq(42) - expect(arguments[1]).to eq("foo") + describe "#[]" do + context "with an index" do + it "returns a positional argument" do + aggregate_failures do + expect(arguments[0]).to eq(42) + expect(arguments[1]).to eq("foo") + end end end - end - describe "#[](symbol)" do - it "returns a keyword argument" do - aggregate_failures do - expect(arguments[:bar]).to eq("baz") - expect(arguments[:qux]).to eq(123) + context "with a symbol" do + it "returns a named argument" do + aggregate_failures do + expect(arguments[:bar]).to eq("baz") + expect(arguments[:qux]).to eq(123) + end end end end @@ -55,79 +63,50 @@ Spectator.describe Spectator::Arguments do describe "#==" do subject { arguments == other } - context "with Arguments" do - context "with equal arguments" do - let(other) { arguments } + context "with equal arguments" do + let(other) { arguments } - it { is_expected.to be_true } - end - - context "with different arguments" do - let(other) { Spectator::Arguments.new({123, :foo, "bar"}, {opt: "foobar"}) } - - it { is_expected.to be_false } - end - - context "with the same kwargs in a different order" do - let(other) { Spectator::Arguments.new(arguments.args, {qux: 123, bar: "baz"}) } - - it { is_expected.to be_true } - end - - context "with a missing kwarg" do - let(other) { Spectator::Arguments.new(arguments.args, {bar: "baz"}) } - - it { is_expected.to be_false } - end - - context "with an extra kwarg" do - let(other) { Spectator::Arguments.new(arguments.args, {bar: "baz", qux: 123, extra: 0}) } - - it { is_expected.to be_false } + it "returns true" do + is_expected.to be_true end end - context "with FormalArguments" do - context "with equal arguments" do - let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123}) } - - it { is_expected.to be_true } + context "with different arguments" do + let(other) do + Spectator::Arguments.new( + args: {123, :foo, "bar"}, + kwargs: {opt: "foobar"} + ) end - context "with different arguments" do - let(other) { Spectator::FormalArguments.new({arg1: 123, arg2: :foo, arg3: "bar"}, {opt: "foobar"}) } + it "returns false" do + is_expected.to be_false + end + end - it { is_expected.to be_false } + context "with the same kwargs in a different order" do + let(other) do + Spectator::Arguments.new( + args: arguments.args, + kwargs: {qux: 123, bar: "baz"} + ) end - context "with the same kwargs in a different order" do - let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {qux: 123, bar: "baz"}) } + it "returns true" do + is_expected.to be_true + end + end - it { is_expected.to be_true } + context "with a missing kwarg" do + let(other) do + Spectator::Arguments.new( + args: arguments.args, + kwargs: {bar: "baz"} + ) end - context "with a missing kwarg" do - let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz"}) } - - it { is_expected.to be_false } - end - - context "with an extra kwarg" do - let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123, extra: 0}) } - - it { is_expected.to be_false } - end - - context "with different splat arguments" do - let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, {bar: "baz", qux: 123}) } - - it { is_expected.to be_false } - end - - context "with mixed positional tuple types" do - let(other) { Spectator::FormalArguments.new({arg1: 42}, :splat, {"foo"}, {bar: "baz", qux: 123}) } - - it { is_expected.to be_true } + it "returns false" do + is_expected.to be_false end end end @@ -135,149 +114,76 @@ Spectator.describe Spectator::Arguments do describe "#===" do subject { pattern === arguments } - context "with Arguments" do - context "with equal arguments" do - let(pattern) { arguments } + context "with equal arguments" do + let(pattern) { arguments } - it { is_expected.to be_true } - end - - context "with matching arguments" do - let(pattern) { Spectator::Arguments.new({Int32, /foo/}, {bar: /baz/, qux: Int32}) } - - it { is_expected.to be_true } - end - - context "with non-matching arguments" do - let(pattern) { Spectator::Arguments.new({Float64, /bar/}, {bar: /foo/, qux: "123"}) } - - it { is_expected.to be_false } - end - - context "with different arguments" do - let(pattern) { Spectator::Arguments.new({123, :foo, "bar"}, {opt: "foobar"}) } - - it { is_expected.to be_false } - end - - context "with the same kwargs in a different order" do - let(pattern) { Spectator::Arguments.new(arguments.args, {qux: Int32, bar: /baz/}) } - - it { is_expected.to be_true } - end - - context "with an additional kwarg" do - let(pattern) { Spectator::Arguments.new(arguments.args, {bar: /baz/}) } - - it { is_expected.to be_true } - end - - context "with a missing kwarg" do - let(pattern) { Spectator::Arguments.new(arguments.args, {bar: /baz/, qux: Int32, extra: 0}) } - - it { is_expected.to be_false } + it "returns true" do + is_expected.to be_true end end - context "with FormalArguments" do - let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123}) } - - context "with equal arguments" do - let(pattern) { Spectator::Arguments.new({42, "foo"}, {bar: "baz", qux: 123}) } - - it { is_expected.to be_true } + context "with different arguments" do + let(pattern) do + Spectator::Arguments.new( + args: {123, :foo, "bar"}, + kwargs: {opt: "foobar"} + ) end - context "with matching arguments" do - let(pattern) { Spectator::Arguments.new({Int32, /foo/}, {bar: /baz/, qux: Int32}) } + it "returns false" do + is_expected.to be_false + end + end - it { is_expected.to be_true } + context "with the same kwargs in a different order" do + let(pattern) do + Spectator::Arguments.new( + args: arguments.args, + kwargs: {qux: 123, bar: "baz"} + ) end - context "with non-matching arguments" do - let(pattern) { Spectator::Arguments.new({Float64, /bar/}, {bar: /foo/, qux: "123"}) } + it "returns true" do + is_expected.to be_true + end + end - it { is_expected.to be_false } + context "with a missing kwarg" do + let(pattern) do + Spectator::Arguments.new( + args: arguments.args, + kwargs: {bar: "baz"} + ) end - context "with different arguments" do - let(pattern) { Spectator::Arguments.new({123, :foo, "bar"}, {opt: "foobar"}) } + it "returns false" do + is_expected.to be_false + end + end - it { is_expected.to be_false } + context "with matching types and regex" do + let(pattern) do + Spectator::Arguments.new( + args: {Int32, /foo/}, + kwargs: {bar: String, qux: 123} + ) end - context "with the same kwargs in a different order" do - let(pattern) { Spectator::Arguments.new(arguments.positional, {qux: Int32, bar: /baz/}) } + it "returns true" do + is_expected.to be_true + end + end - it { is_expected.to be_true } + context "with different types and regex" do + let(pattern) do + Spectator::Arguments.new( + args: {Symbol, /bar/}, + kwargs: {bar: String, qux: 42} + ) end - context "with an additional kwarg" do - let(pattern) { Spectator::Arguments.new(arguments.positional, {bar: /baz/}) } - - it { is_expected.to be_true } - end - - context "with a missing kwarg" do - let(pattern) { Spectator::Arguments.new(arguments.positional, {bar: /baz/, qux: Int32, extra: 0}) } - - it { is_expected.to be_false } - end - - context "with different splat arguments" do - let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, super.kwargs) } - let(pattern) { Spectator::Arguments.new({Int32, /foo/, 5}, arguments.kwargs) } - - it { is_expected.to be_false } - end - - context "with matching mixed positional tuple types" do - let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, super.kwargs) } - let(pattern) { Spectator::Arguments.new({Int32, /foo/, 1, 2, 3}, arguments.kwargs) } - - it { is_expected.to be_true } - end - - context "with non-matching mixed positional tuple types" do - let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, super.kwargs) } - let(pattern) { Spectator::Arguments.new({Float64, /bar/, 3, 2, Symbol}, arguments.kwargs) } - - it { is_expected.to be_false } - end - - context "with matching args spilling over into splat and mixed positional tuple types" do - let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } - let(pattern) { Spectator::Arguments.capture(Int32, /foo/, Symbol, Symbol, :z, bar: /baz/, qux: Int32) } - - it { is_expected.to be_true } - end - - context "with non-matching args spilling over into splat and mixed positional tuple types" do - let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } - let(pattern) { Spectator::Arguments.capture(Float64, /bar/, Symbol, String, :z, bar: /foo/, qux: Int32) } - - it { is_expected.to be_false } - end - - context "with matching mixed named positional and keyword arguments" do - let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } - let(pattern) { Spectator::Arguments.capture(/foo/, Symbol, :y, Symbol, arg1: Int32, bar: /baz/, qux: 123) } - - it { is_expected.to be_true } - end - - context "with non-matching mixed named positional and keyword arguments" do - let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } - let(pattern) { Spectator::Arguments.capture(5, Symbol, :z, Symbol, arg2: /foo/, bar: /baz/, qux: Int32) } - - it { is_expected.to be_false } - end - - context "with non-matching mixed named positional and keyword arguments" do - let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } - let(pattern) { Spectator::Arguments.capture(/bar/, String, :y, Symbol, arg1: 0, bar: /foo/, qux: Float64) } - - it { is_expected.to be_false } + it "returns false" do + is_expected.to be_false end end end diff --git a/spec/spectator/mocks/double_spec.cr b/spec/spectator/mocks/double_spec.cr index e55c549..c93a723 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 @@ -305,7 +309,7 @@ Spectator.describe Spectator::Double do subject(dbl) { ClassDouble } let(foo_stub) { Spectator::ValueStub.new(:foo, :override) } - after { dbl._spectator_clear_stubs } + after_each { dbl._spectator_clear_stubs } it "overrides an existing method" do expect { dbl._spectator_define_stub(foo_stub) }.to change { dbl.foo }.from(:stub).to(:override) @@ -353,7 +357,7 @@ Spectator.describe Spectator::Double do end describe "._spectator_clear_stubs" do - before { dbl._spectator_define_stub(foo_stub) } + before_each { dbl._spectator_define_stub(foo_stub) } it "removes previously defined stubs" do expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(:override).to(:stub) @@ -361,7 +365,7 @@ Spectator.describe Spectator::Double do end describe "._spectator_calls" do - before { dbl._spectator_clear_calls } + before_each { dbl._spectator_clear_calls } # Retrieves symbolic names of methods called on a double. def called_method_names(dbl) @@ -436,7 +440,7 @@ Spectator.describe Spectator::Double do subject(dbl) { FooBarDouble.new } let(stub) { Spectator::ValueStub.new(:foo, 5) } - before { dbl._spectator_define_stub(stub) } + before_each { dbl._spectator_define_stub(stub) } it "removes previously defined stubs" do expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(5).to(42) @@ -447,7 +451,7 @@ Spectator.describe Spectator::Double do subject(dbl) { FooBarDouble.new } let(stub) { Spectator::ValueStub.new(:foo, 5) } - before { dbl._spectator_define_stub(stub) } + before_each { dbl._spectator_define_stub(stub) } # Retrieves symbolic names of methods called on a double. def called_method_names(dbl) @@ -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/formal_arguments_spec.cr b/spec/spectator/mocks/formal_arguments_spec.cr deleted file mode 100644 index 963b6eb..0000000 --- a/spec/spectator/mocks/formal_arguments_spec.cr +++ /dev/null @@ -1,325 +0,0 @@ -require "../../spec_helper" - -Spectator.describe Spectator::FormalArguments do - subject(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } - - it "stores the arguments" do - expect(arguments).to have_attributes( - args: {arg1: 42, arg2: "foo"}, - splat_name: :splat, - splat: {:x, :y, :z}, - kwargs: {bar: "baz", qux: 123} - ) - end - - describe ".build" do - subject { Spectator::FormalArguments.build({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, {bar: "baz", qux: 123}) } - - it "stores the arguments and keyword arguments" do - is_expected.to have_attributes( - args: {arg1: 42, arg2: "foo"}, - splat_name: :splat, - splat: {1, 2, 3}, - kwargs: {bar: "baz", qux: 123} - ) - end - - context "without a splat" do - subject { Spectator::FormalArguments.build({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123}) } - - it "stores the arguments and keyword arguments" do - is_expected.to have_attributes( - args: {arg1: 42, arg2: "foo"}, - splat: nil, - kwargs: {bar: "baz", qux: 123} - ) - end - end - end - - describe "#[](index)" do - it "returns a positional argument" do - aggregate_failures do - expect(arguments[0]).to eq(42) - expect(arguments[1]).to eq("foo") - end - end - - it "returns splat arguments" do - aggregate_failures do - expect(arguments[2]).to eq(:x) - expect(arguments[3]).to eq(:y) - expect(arguments[4]).to eq(:z) - end - end - - context "with named positional arguments" do - subject(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } - - it "returns a positional argument" do - aggregate_failures do - expect(arguments[0]).to eq(42) - expect(arguments[1]).to eq("foo") - end - end - - it "returns splat arguments" do - aggregate_failures do - expect(arguments[2]).to eq(:x) - expect(arguments[3]).to eq(:y) - expect(arguments[4]).to eq(:z) - end - end - end - end - - describe "#[](symbol)" do - it "returns a keyword argument" do - aggregate_failures do - expect(arguments[:bar]).to eq("baz") - expect(arguments[:qux]).to eq(123) - end - end - - context "with named positional arguments" do - subject(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } - - it "returns a positional argument" do - aggregate_failures do - expect(arguments[:arg1]).to eq(42) - expect(arguments[:arg2]).to eq("foo") - end - end - - it "returns a keyword argument" do - aggregate_failures do - expect(arguments[:bar]).to eq("baz") - expect(arguments[:qux]).to eq(123) - end - end - end - end - - describe "#to_s" do - subject { arguments.to_s } - - it "formats the arguments" do - is_expected.to eq("(arg1: 42, arg2: \"foo\", *splat: {:x, :y, :z}, bar: \"baz\", qux: 123)") - end - - context "when empty" do - let(arguments) { Spectator::FormalArguments.none } - - it "returns (no args)" do - is_expected.to eq("(no args)") - end - end - - context "with a splat and no arguments" do - let(arguments) { Spectator::FormalArguments.build(NamedTuple.new, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } - - it "omits the splat name" do - is_expected.to eq("(:x, :y, :z, bar: \"baz\", qux: 123)") - end - end - end - - describe "#==" do - subject { arguments == other } - - context "with Arguments" do - context "with equal arguments" do - let(other) { Spectator::Arguments.new(arguments.positional, arguments.kwargs) } - - it { is_expected.to be_true } - end - - context "with different arguments" do - let(other) { Spectator::Arguments.new({123, :foo, "bar"}, {opt: "foobar"}) } - - it { is_expected.to be_false } - end - - context "with the same kwargs in a different order" do - let(other) { Spectator::Arguments.new(arguments.positional, {qux: 123, bar: "baz"}) } - - it { is_expected.to be_true } - end - - context "with a missing kwarg" do - let(other) { Spectator::Arguments.new(arguments.positional, {bar: "baz"}) } - - it { is_expected.to be_false } - end - - context "with an extra kwarg" do - let(other) { Spectator::Arguments.new(arguments.positional, {bar: "baz", qux: 123, extra: 0}) } - - it { is_expected.to be_false } - end - end - - context "with FormalArguments" do - context "with equal arguments" do - let(other) { arguments } - - it { is_expected.to be_true } - end - - context "with different arguments" do - let(other) { Spectator::FormalArguments.new({arg1: 123, arg2: :foo, arg3: "bar"}, :splat, {1, 2, 3}, {opt: "foobar"}) } - - it { is_expected.to be_false } - end - - context "with the same kwargs in a different order" do - let(other) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {qux: 123, bar: "baz"}) } - - it { is_expected.to be_true } - end - - context "with a missing kwarg" do - let(other) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: "baz"}) } - - it { is_expected.to be_false } - end - - context "with an extra kwarg" do - let(other) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: "baz", qux: 123, extra: 0}) } - - it { is_expected.to be_false } - end - - context "with different splat arguments" do - let(other) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, {1, 2, 3}, arguments.kwargs) } - - it { is_expected.to be_false } - end - - context "with mixed positional tuple types" do - let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, arguments.splat_name, arguments.splat, arguments.kwargs) } - - it { is_expected.to be_true } - end - - context "with mixed positional tuple types (flipped)" do - let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } - let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } - - it { is_expected.to be_true } - end - end - end - - describe "#===" do - subject { pattern === arguments } - - context "with Arguments" do - let(arguments) { Spectator::Arguments.new({42, "foo"}, {bar: "baz", qux: 123}) } - - context "with equal arguments" do - let(pattern) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123}) } - - it { is_expected.to be_true } - end - - context "with matching arguments" do - let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, {bar: /baz/, qux: Int32}) } - - it { is_expected.to be_true } - end - - context "with non-matching arguments" do - let(pattern) { Spectator::FormalArguments.new({arg1: Float64, arg2: /bar/}, {bar: /foo/, qux: "123"}) } - - it { is_expected.to be_false } - end - - context "with different arguments" do - let(pattern) { Spectator::FormalArguments.new({arg1: 123, arg2: :foo, arg3: "bar"}, {opt: "foobar"}) } - - it { is_expected.to be_false } - end - - context "with the same kwargs in a different order" do - let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, {qux: Int32, bar: /baz/}) } - - it { is_expected.to be_true } - end - - context "with an additional kwarg" do - let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, {bar: /baz/}) } - - it { is_expected.to be_true } - end - - context "with a missing kwarg" do - let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, {bar: /baz/, qux: Int32, extra: 0}) } - - it { is_expected.to be_false } - end - end - - context "with FormalArguments" do - context "with equal arguments" do - let(pattern) { arguments } - - it { is_expected.to be_true } - end - - context "with matching arguments" do - let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, :splat, {Symbol, Symbol, :z}, {bar: /baz/, qux: Int32}) } - - it { is_expected.to be_true } - end - - context "with non-matching arguments" do - let(pattern) { Spectator::FormalArguments.new({arg1: Float64, arg2: /bar/}, :splat, {String, Int32, :x}, {bar: /foo/, qux: "123"}) } - - it { is_expected.to be_false } - end - - context "with different arguments" do - let(pattern) { Spectator::FormalArguments.new({arg1: 123, arg2: :foo, arg3: "bar"}, :splat, {1, 2, 3}, {opt: "foobar"}) } - - it { is_expected.to be_false } - end - - context "with the same kwargs in a different order" do - let(pattern) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {qux: Int32, bar: /baz/}) } - - it { is_expected.to be_true } - end - - context "with an additional kwarg" do - let(pattern) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: /baz/}) } - - it { is_expected.to be_true } - end - - context "with a missing kwarg" do - let(pattern) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: /baz/, qux: Int32, extra: 0}) } - - it { is_expected.to be_false } - end - - context "with different splat arguments" do - let(pattern) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, {1, 2, 3}, arguments.kwargs) } - - it { is_expected.to be_false } - end - - context "with matching mixed positional tuple types" do - let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, arguments.splat_name, arguments.splat, arguments.kwargs) } - - it { is_expected.to be_true } - end - - context "with non-matching mixed positional tuple types" do - let(pattern) { Spectator::FormalArguments.new({arg1: Float64, arg2: /bar/}, arguments.splat_name, arguments.splat, arguments.kwargs) } - - it { is_expected.to be_false } - end - end - end -end diff --git a/spec/spectator/mocks/lazy_double_spec.cr b/spec/spectator/mocks/lazy_double_spec.cr index 8ea5a5d..34883dc 100644 --- a/spec/spectator/mocks/lazy_double_spec.cr +++ b/spec/spectator/mocks/lazy_double_spec.cr @@ -235,9 +235,16 @@ Spectator.describe Spectator::LazyDouble do end context "with previously undefined methods" do - it "raises an error" do + it "can stub methods" do stub = Spectator::ValueStub.new(:baz, :xyz) - expect { dbl._spectator_define_stub(stub) }.to raise_error(/stub/) + dbl._spectator_define_stub(stub) + expect(dbl.baz).to eq(:xyz) + end + + it "uses a stub only if an argument constraint is met" do + stub = Spectator::ValueStub.new(:baz, :xyz, Spectator::Arguments.capture(:right)) + dbl._spectator_define_stub(stub) + expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /baz/) end end end @@ -246,18 +253,27 @@ Spectator.describe Spectator::LazyDouble do subject(dbl) { Spectator::LazyDouble.new(foo: 42, bar: "baz") } let(stub) { Spectator::ValueStub.new(:foo, 5) } - before { dbl._spectator_define_stub(stub) } + before_each { dbl._spectator_define_stub(stub) } it "removes previously defined stubs" do expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(5).to(42) end + + it "raises on methods without an implementation" do + stub = Spectator::ValueStub.new(:baz, :xyz) + dbl._spectator_define_stub(stub) + expect(dbl.baz).to eq(:xyz) + + dbl._spectator_clear_stubs + expect { dbl.baz }.to raise_error(Spectator::UnexpectedMessage, /baz/) + end end describe "#_spectator_calls" do subject(dbl) { Spectator::LazyDouble.new(foo: 42, bar: "baz") } let(stub) { Spectator::ValueStub.new(:foo, 5) } - before { dbl._spectator_define_stub(stub) } + before_each { dbl._spectator_define_stub(stub) } # Retrieves symbolic names of methods called on a double. def called_method_names(dbl) @@ -275,7 +291,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 +301,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..0c19759 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 @@ -388,7 +312,7 @@ Spectator.describe Spectator::Mock do let(mock) { MockThing } let(foo_stub) { Spectator::ValueStub.new(:foo, :override) } - after { mock._spectator_clear_stubs } + after_each { mock._spectator_clear_stubs } it "overrides an existing method" do expect { mock._spectator_define_stub(foo_stub) }.to change { mock.foo }.from(:stub).to(:override) @@ -443,22 +367,8 @@ Spectator.describe Spectator::Mock do expect(restricted(mock)).to eq(:stub) 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) } + before_each { mock._spectator_define_stub(foo_stub) } it "removes previously defined stubs" do expect { mock._spectator_clear_stubs }.to change { mock.foo }.from(:override).to(:stub) @@ -466,7 +376,7 @@ Spectator.describe Spectator::Mock do end describe "._spectator_calls" do - before { mock._spectator_clear_calls } + before_each { mock._spectator_clear_calls } # Retrieves symbolic names of methods called on a mock. def called_method_names(mock) @@ -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 @@ -697,7 +410,7 @@ Spectator.describe Spectator::Mock do let(mock) { MockThing.new } - after { mock._spectator_clear_stubs } + after_each { mock._spectator_clear_stubs } it "raises a TypeCastError when using a value-based stub" do stub = Spectator::ValueStub.new(:oops, nil).as(Spectator::Stub) @@ -748,7 +461,7 @@ Spectator.describe Spectator::Mock do let(mock) { MockedClass.new } # Necessary to clear stubs to prevent leakages between tests. - after { mock._spectator_clear_stubs } + after_each { mock._spectator_clear_stubs } it "overrides responses from methods with keyword arguments" do expect(mock.method1).to eq(123) @@ -858,8 +571,8 @@ Spectator.describe Spectator::Mock do let(mock) { MockedStruct.new } # Necessary to clear stubs to prevent leakages between tests. - after { mock._spectator_clear_stubs } - after { MockedStruct._spectator_invocations.clear } + after_each { mock._spectator_clear_stubs } + after_each { MockedStruct._spectator_invocations.clear } it "overrides responses from methods with keyword arguments" do expect(mock.method1).to eq(123) @@ -929,7 +642,7 @@ Spectator.describe Spectator::Mock do arg end - def self.baz(arg, &) + def self.baz(arg) yield end end @@ -943,7 +656,7 @@ Spectator.describe Spectator::Mock do let(mock) { Thing } let(foo_stub) { Spectator::ValueStub.new(:foo, :override) } - after { mock._spectator_clear_stubs } + after_each { mock._spectator_clear_stubs } it "overrides an existing method" do expect { mock._spectator_define_stub(foo_stub) }.to change { mock.foo }.from(:stub).to(:override) @@ -999,7 +712,7 @@ Spectator.describe Spectator::Mock do end describe "._spectator_clear_stubs" do - before { mock._spectator_define_stub(foo_stub) } + before_each { mock._spectator_define_stub(foo_stub) } it "removes previously defined stubs" do expect { mock._spectator_clear_stubs }.to change { mock.foo }.from(:override).to(:stub) @@ -1007,7 +720,7 @@ Spectator.describe Spectator::Mock do end describe "._spectator_calls" do - before { mock._spectator_clear_calls } + before_each { mock._spectator_clear_calls } # Retrieves symbolic names of methods called on a mock. def called_method_names(mock) @@ -1043,7 +756,7 @@ Spectator.describe Spectator::Mock do let(mock) { NoReturnThing.new } - after { mock._spectator_clear_stubs } + after_each { mock._spectator_clear_stubs } it "raises a TypeCastError when using a value-based stub" do stub = Spectator::ValueStub.new(:oops, nil).as(Spectator::Stub) diff --git a/spec/spectator/mocks/null_double_spec.cr b/spec/spectator/mocks/null_double_spec.cr index a6fc7d2..1aa86ca 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 @@ -267,7 +270,7 @@ Spectator.describe Spectator::NullDouble do subject(dbl) { ClassDouble } let(foo_stub) { Spectator::ValueStub.new(:foo, :override) } - after { dbl._spectator_clear_stubs } + after_each { dbl._spectator_clear_stubs } it "overrides an existing method" do expect { dbl._spectator_define_stub(foo_stub) }.to change { dbl.foo }.from(:stub).to(:override) @@ -315,7 +318,7 @@ Spectator.describe Spectator::NullDouble do end describe "._spectator_clear_stubs" do - before { dbl._spectator_define_stub(foo_stub) } + before_each { dbl._spectator_define_stub(foo_stub) } it "removes previously defined stubs" do expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(:override).to(:stub) @@ -323,7 +326,7 @@ Spectator.describe Spectator::NullDouble do end describe "._spectator_calls" do - before { dbl._spectator_clear_calls } + before_each { dbl._spectator_clear_calls } # Retrieves symbolic names of methods called on a double. def called_method_names(dbl) @@ -398,7 +401,7 @@ Spectator.describe Spectator::NullDouble do subject(dbl) { FooBarDouble.new } let(stub) { Spectator::ValueStub.new(:foo, 5) } - before { dbl._spectator_define_stub(stub) } + before_each { dbl._spectator_define_stub(stub) } it "removes previously defined stubs" do expect { dbl._spectator_clear_stubs }.to change { dbl.foo }.from(5).to(42) @@ -409,7 +412,7 @@ Spectator.describe Spectator::NullDouble do subject(dbl) { FooBarDouble.new } let(stub) { Spectator::ValueStub.new(:foo, 5) } - before { dbl._spectator_define_stub(stub) } + before_each { dbl._spectator_define_stub(stub) } # Retrieves symbolic names of methods called on a double. def called_method_names(dbl) @@ -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/abstract_expression.cr b/src/spectator/abstract_expression.cr index 6ab4cbf..b58c1cc 100644 --- a/src/spectator/abstract_expression.cr +++ b/src/spectator/abstract_expression.cr @@ -34,7 +34,7 @@ module Spectator # Produces a string representation of the expression. # This consists of the label (if one is available) and the value. - def to_s(io : IO) : Nil + def to_s(io) if (label = @label) io << label << ": " end @@ -43,7 +43,7 @@ module Spectator # Produces a detailed string representation of the expression. # This consists of the label (if one is available) and the value. - def inspect(io : IO) : Nil + def inspect(io) if (label = @label) io << label << ": " end diff --git a/src/spectator/anything.cr b/src/spectator/anything.cr index aa25e3c..e4d7b34 100644 --- a/src/spectator/anything.cr +++ b/src/spectator/anything.cr @@ -13,12 +13,12 @@ module Spectator end # Displays "anything". - def to_s(io : IO) : Nil + def to_s(io) io << "anything" end # Displays "". - def inspect(io : IO) : Nil + def inspect(io) io << "" end end diff --git a/src/spectator/config/cli_arguments_applicator.cr b/src/spectator/config/cli_arguments_applicator.cr index 15c9f94..496fc1a 100644 --- a/src/spectator/config/cli_arguments_applicator.cr +++ b/src/spectator/config/cli_arguments_applicator.cr @@ -112,7 +112,7 @@ module Spectator # Adds the example filter option to the parser. private def example_option(parser, builder) parser.on("-e", "--example STRING", "Run examples whose full nested names include STRING") do |pattern| - Log.debug { "Filtering for examples containing '#{pattern}' (-e '#{pattern}')" } + Log.debug { "Filtering for examples named '#{pattern}' (-e '#{pattern}')" } filter = NameNodeFilter.new(pattern) builder.add_node_filter(filter) end diff --git a/src/spectator/context.cr b/src/spectator/context.cr index 15b9335..c3213e3 100644 --- a/src/spectator/context.cr +++ b/src/spectator/context.cr @@ -4,23 +4,18 @@ # 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 # and that the Crystal compiler instantiates a `#to_s` and/or `#inspect` for each of those types, # an explosion in method instances can be created. # The compile time is drastically reduced by using a dummy string instead. - def to_s(io : IO) : Nil + def to_s(io) io << "Context" end # :ditto: - def inspect(io : IO) : Nil + def inspect(io) io << "Context<" << self.class << '>' end end 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/hooks.cr b/src/spectator/dsl/hooks.cr index 6672e59..3cd4d87 100644 --- a/src/spectator/dsl/hooks.cr +++ b/src/spectator/dsl/hooks.cr @@ -124,21 +124,11 @@ module Spectator::DSL # This means that values defined by `let` and `subject` are available. define_example_hook :before_each - # :ditto: - macro before(&block) - before_each {{block}} - end - # Defines a block of code that will be invoked after every example in the group. # The block will be run in the context of the current running example. # This means that values defined by `let` and `subject` are available. define_example_hook :after_each - # :ditto: - macro after(&block) - after_each {{block}} - end - # Defines a block of code that will be invoked around every example in the group. # The block will be run in the context of the current running example. # This means that values defined by `let` and `subject` are available. @@ -149,11 +139,6 @@ module Spectator::DSL # More code can run afterwards (in the block). define_example_hook :around_each - # :ditto: - macro around(&block) - around_each {{block}} - end - # Defines a block of code that will be invoked before every example in the group. # The block will be run in the context of the current running example. # This means that values defined by `let` and `subject` are available. diff --git a/src/spectator/dsl/matchers.cr b/src/spectator/dsl/matchers.cr index 95e6c5d..b42bc88 100644 --- a/src/spectator/dsl/matchers.cr +++ b/src/spectator/dsl/matchers.cr @@ -790,7 +790,7 @@ module Spectator::DSL # ``` # expect_raises { raise "foobar" } # ``` - macro expect_raises(&block) + macro expect_raises expect {{block}}.to raise_error 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..628fe6b 100644 --- a/src/spectator/dsl/mocks.cr +++ b/src/spectator/dsl/mocks.cr @@ -31,19 +31,19 @@ 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 {{null_double_type_name}}.new(@stubs) end - {{block.body if block}} + {% if block %}{{block.body}}{% end %} end {% 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..4babc2a 100644 --- a/src/spectator/error_result.cr +++ b/src/spectator/error_result.cr @@ -11,12 +11,12 @@ module Spectator end # Calls the `error` method on *visitor*. - def accept(visitor, &) + def accept(visitor) visitor.error(yield self) end # One-word description of the result. - def to_s(io : IO) : Nil + def to_s(io) io << "error" end diff --git a/src/spectator/example.cr b/src/spectator/example.cr index e18676c..8f99e93 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 @@ -104,8 +103,8 @@ module Spectator # Returns the result of the execution. # The result will also be stored in `#result`. def run : Result - Log.debug { "Running example: #{self}" } - Log.warn { "Example already ran: #{self}" } if @finished + Log.debug { "Running example #{self}" } + Log.warn { "Example #{self} already ran" } if @finished if pending? Log.debug { "Skipping example #{self} - marked pending" } @@ -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 @@ -143,10 +142,8 @@ module Spectator group.call_before_each(self) group.call_pre_condition(self) end - Log.trace { "Running example code for: #{self}" } @entrypoint.call(self) @finished = true - Log.trace { "Finished running example code for: #{self}" } if group = @group group.call_post_condition(self) group.call_after_each(self) @@ -164,7 +161,7 @@ module Spectator # The context casted to an instance of *klass* is provided as a block argument. # # 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 +181,7 @@ module Spectator end # Yields this example and all parent groups. - def ascend(&) + def ascend node = self while node yield node @@ -194,7 +191,7 @@ module Spectator # Constructs the full name or description of the example. # This prepends names of groups this example is part of. - def to_s(io : IO) : Nil + def to_s(io) name = @name # Prefix with group's full name if the node belongs to a group. @@ -213,9 +210,9 @@ module Spectator end # Exposes information about the example useful for debugging. - def inspect(io : IO) : Nil + def inspect(io) super - io << " - " << result + io << ' ' << result end # Creates the JSON representation of the example, @@ -279,7 +276,7 @@ module Spectator # The block given to this method will be executed within the test context. # # 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 @@ -289,7 +286,7 @@ module Spectator # Constructs the full name or description of the example. # This prepends names of groups this example is part of. - def to_s(io : IO) : Nil + def to_s(io) : Nil @example.to_s(io) end 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..0481fc4 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -19,14 +19,14 @@ module Spectator protected setter group : ExampleGroup? define_hook before_all : ExampleGroupHook do - Log.trace { "Processing before_all hooks for: #{self}" } + Log.trace { "Processing before_all hooks for #{self}" } @group.try &.call_before_all before_all_hooks.each &.call_once end define_hook after_all : ExampleGroupHook, :prepend do - Log.trace { "Processing after_all hooks for: #{self}" } + Log.trace { "Processing after_all hooks for #{self}" } after_all_hooks.each &.call_once if finished? if group = @group @@ -35,21 +35,21 @@ module Spectator end define_hook before_each : ExampleHook do |example| - Log.trace { "Processing before_each hooks for: #{self}" } + Log.trace { "Processing before_each hooks for #{self}" } @group.try &.call_before_each(example) before_each_hooks.each &.call(example) end define_hook after_each : ExampleHook, :prepend do |example| - Log.trace { "Processing after_each hooks for: #{self}" } + Log.trace { "Processing after_each hooks for #{self}" } after_each_hooks.each &.call(example) @group.try &.call_after_each(example) end define_hook around_each : ExampleProcsyHook do |procsy| - Log.trace { "Processing around_each hooks for: #{self}" } + Log.trace { "Processing around_each hooks for #{self}" } around_each_hooks.reverse_each { |hook| procsy = hook.wrap(procsy) } if group = @group @@ -59,14 +59,14 @@ module Spectator end define_hook pre_condition : ExampleHook do |example| - Log.trace { "Processing pre_condition hooks for: #{self}" } + Log.trace { "Processing pre_condition hooks for #{self}" } @group.try &.call_pre_condition(example) pre_condition_hooks.each &.call(example) end define_hook post_condition : ExampleHook, :prepend do |example| - Log.trace { "Processing post_condition hooks for: #{self}" } + Log.trace { "Processing post_condition hooks for #{self}" } post_condition_hooks.each &.call(example) @group.try &.call_post_condition(example) @@ -79,7 +79,7 @@ module Spectator # This group will be assigned to the parent *group* if it is provided. # 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 @@ -112,15 +112,11 @@ module Spectator # Constructs the full name or description of the example group. # This prepends names of groups this group is part of. - def to_s(io : IO, *, nested = false) : Nil - unless parent = @group - # Display special string when called directly. - io << "" unless nested - return - end - + def to_s(io) # Prefix with group's full name if the node belongs to a group. - parent.to_s(io, nested: true) + return unless parent = @group + + parent.to_s(io) name = @name # Add padding between the node names @@ -130,7 +126,7 @@ module Spectator (parent.name?.is_a?(Symbol) && name.is_a?(String) && (name.starts_with?('#') || name.starts_with?('.'))) - super(io) + super end # Adds the specified *node* to the 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_hook.cr b/src/spectator/example_group_hook.cr index aee357f..bd6bac8 100644 --- a/src/spectator/example_group_hook.cr +++ b/src/spectator/example_group_hook.cr @@ -42,7 +42,7 @@ module Spectator # Produces the string representation of the hook. # Includes the location and label if they're not nil. - def to_s(io : IO) : Nil + def to_s(io) io << "example group hook" if (label = @label) 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/example_hook.cr b/src/spectator/example_hook.cr index 6bc77a0..edebf26 100644 --- a/src/spectator/example_hook.cr +++ b/src/spectator/example_hook.cr @@ -37,7 +37,7 @@ module Spectator # Produces the string representation of the hook. # Includes the location and label if they're not nil. - def to_s(io : IO) : Nil + def to_s(io) io << "example hook" if (label = @label) diff --git a/src/spectator/example_procsy_hook.cr b/src/spectator/example_procsy_hook.cr index 16bc970..8a64f17 100644 --- a/src/spectator/example_procsy_hook.cr +++ b/src/spectator/example_procsy_hook.cr @@ -39,7 +39,7 @@ module Spectator # Produces the string representation of the hook. # Includes the location and label if they're not nil. - def to_s(io : IO) : Nil + def to_s(io) io << "example hook" if (label = @label) diff --git a/src/spectator/expectation.cr b/src/spectator/expectation.cr index 79d8473..bfc0248 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 @@ -205,15 +160,9 @@ module Spectator stubbable._spectator_define_stub(unconstrained_stub) end - # Apply the stub that is expected to be called. stubbable._spectator_define_stub(stub) - - # Check if the stub was invoked after the test completes. matcher = Matchers::ReceiveMatcher.new(stub) - Harness.current.defer { to(matcher, message) } - - # Prevent leaking stubs between tests. - Harness.current.cleanup { stubbable._spectator_remove_stub(stub) } + to_eventually(matcher, message) end # Asserts that some criteria defined by the matcher is eventually satisfied. @@ -241,15 +190,9 @@ module Spectator stubbable._spectator_define_stub(unconstrained_stub) end - # Apply the stub that could be called in case it is. stubbable._spectator_define_stub(stub) - - # Check if the stub was invoked after the test completes. matcher = Matchers::ReceiveMatcher.new(stub) - Harness.current.defer { to_not(matcher, message) } - - # Prevent leaking stubs between tests. - Harness.current.cleanup { stubbable._spectator_remove_stub(stub) } + to_never(matcher, message) end # :ditto: diff --git a/src/spectator/fail_result.cr b/src/spectator/fail_result.cr index 082a0d2..36ea8fb 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 @@ -55,7 +55,7 @@ module Spectator end # One-word description of the result. - def to_s(io : IO) : Nil + def to_s(io) io << "fail" 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/formatting/components/comment.cr b/src/spectator/formatting/components/comment.cr index 30f4293..b398840 100644 --- a/src/spectator/formatting/components/comment.cr +++ b/src/spectator/formatting/components/comment.cr @@ -16,7 +16,7 @@ module Spectator::Formatting::Components end # Writes the comment to the output. - def to_s(io : IO) : Nil + def to_s(io) io << "# " << @content end end diff --git a/src/spectator/formatting/components/error_result_block.cr b/src/spectator/formatting/components/error_result_block.cr index a24784a..353c096 100644 --- a/src/spectator/formatting/components/error_result_block.cr +++ b/src/spectator/formatting/components/error_result_block.cr @@ -7,38 +7,36 @@ module Spectator::Formatting::Components # Displays information about an error result. struct ErrorResultBlock < ResultBlock # Creates the component. - def initialize(example : Example, index : Int32, @error : Exception, subindex = 0) + def initialize(example : Example, index : Int32, @result : ErrorResult, subindex = 0) super(example, index, subindex) end # Content displayed on the second line of the block after the label. private def subtitle - @error.message.try(&.each_line.first) + @result.error.message.try(&.each_line.first) end # Prefix for the second line of the block. private def subtitle_label - case @error - when ExampleFailed then "Failure: " - else "Error: " - end.colorize(:red) + "Error: ".colorize(:red) end # Display error information. private def content(io) # Fetch the error and message. - lines = @error.message.try(&.lines) + error = @result.error + lines = error.message.try(&.lines) # Write the error and message if available. case - when lines.nil? then write_error_class(io) - when lines.size == 1 then write_error_message(io, lines.first) - when lines.size > 1 then write_multiline_error_message(io, lines) - else write_error_class(io) + when lines.nil? then write_error_class(io, error) + when lines.size == 1 then write_error_message(io, error, lines.first) + when lines.size > 1 then write_multiline_error_message(io, error, lines) + else write_error_class(io, error) end # Display the backtrace if it's available. - if backtrace = @error.backtrace? + if backtrace = error.backtrace? indent { write_backtrace(io, backtrace) } end @@ -46,24 +44,24 @@ module Spectator::Formatting::Components end # Display just the error type. - private def write_error_class(io) + private def write_error_class(io, error) line(io) do - io << @error.class.colorize(:red) + io << error.class.colorize(:red) end end # Display the error type and first line of the message. - private def write_error_message(io, message) + private def write_error_message(io, error, message) line(io) do - io << "#{@error.class}: ".colorize(:red) + io << "#{error.class}: ".colorize(:red) io << message end end # Display the error type and its multi-line message. - private def write_multiline_error_message(io, lines) + private def write_multiline_error_message(io, error, lines) # Use the normal formatting for the first line. - write_error_message(io, lines.first) + write_error_message(io, error, lines.first) # Display additional lines after the first. lines.skip(1).each do |entry| diff --git a/src/spectator/formatting/components/example_command.cr b/src/spectator/formatting/components/example_command.cr index b1246db..8b3c0b9 100644 --- a/src/spectator/formatting/components/example_command.cr +++ b/src/spectator/formatting/components/example_command.cr @@ -9,7 +9,7 @@ module Spectator::Formatting::Components end # Produces output for running the previously specified example. - def to_s(io : IO) : Nil + def to_s(io) io << "crystal spec " # Use location for argument if it's available, since it's simpler. diff --git a/src/spectator/formatting/components/failure_command_list.cr b/src/spectator/formatting/components/failure_command_list.cr index 7eab4f5..7da5ed2 100644 --- a/src/spectator/formatting/components/failure_command_list.cr +++ b/src/spectator/formatting/components/failure_command_list.cr @@ -10,7 +10,7 @@ module Spectator::Formatting::Components end # Produces the list of commands to run failed examples. - def to_s(io : IO) : Nil + def to_s(io) io.puts "Failed examples:" io.puts @failures.each do |failure| diff --git a/src/spectator/formatting/components/profile.cr b/src/spectator/formatting/components/profile.cr index b98d8f5..2a56e0d 100644 --- a/src/spectator/formatting/components/profile.cr +++ b/src/spectator/formatting/components/profile.cr @@ -9,7 +9,7 @@ module Spectator::Formatting::Components end # Produces the output containing the profiling information. - def to_s(io : IO) : Nil + def to_s(io) io << "Top " io << @profile.size io << " slowest examples (" diff --git a/src/spectator/formatting/components/result_block.cr b/src/spectator/formatting/components/result_block.cr index ddd9c47..bf4b3d2 100644 --- a/src/spectator/formatting/components/result_block.cr +++ b/src/spectator/formatting/components/result_block.cr @@ -41,7 +41,7 @@ module Spectator::Formatting::Components private abstract def content(io) # Writes the component's output to the specified stream. - def to_s(io : IO) : Nil + def to_s(io) title_line(io) # Ident over to align with the spacing used by the index. indent(index_digit_count + 2) do diff --git a/src/spectator/formatting/components/runtime.cr b/src/spectator/formatting/components/runtime.cr index 9638d93..9f28813 100644 --- a/src/spectator/formatting/components/runtime.cr +++ b/src/spectator/formatting/components/runtime.cr @@ -15,7 +15,7 @@ module Spectator::Formatting::Components # #:##:## # # days #:##:## # ``` - def to_s(io : IO) : Nil + def to_s(io) millis = @span.total_milliseconds return format_micro(io, millis * 1000) if millis < 1 diff --git a/src/spectator/formatting/components/stats.cr b/src/spectator/formatting/components/stats.cr index 47e1063..ab76c7c 100644 --- a/src/spectator/formatting/components/stats.cr +++ b/src/spectator/formatting/components/stats.cr @@ -11,7 +11,7 @@ module Spectator::Formatting::Components end # Displays the stats. - def to_s(io : IO) : Nil + def to_s(io) runtime(io) totals(io) if seed = @report.random_seed? diff --git a/src/spectator/formatting/components/tap_profile.cr b/src/spectator/formatting/components/tap_profile.cr index 4154e07..64c36b0 100644 --- a/src/spectator/formatting/components/tap_profile.cr +++ b/src/spectator/formatting/components/tap_profile.cr @@ -10,7 +10,7 @@ module Spectator::Formatting::Components end # Produces the output containing the profiling information. - def to_s(io : IO) : Nil + def to_s(io) io << "# Top " io << @profile.size io << " slowest examples (" diff --git a/src/spectator/formatting/components/totals.cr b/src/spectator/formatting/components/totals.cr index 4063cae..941b6ee 100644 --- a/src/spectator/formatting/components/totals.cr +++ b/src/spectator/formatting/components/totals.cr @@ -31,7 +31,7 @@ module Spectator::Formatting::Components end # Writes the counts to the output. - def to_s(io : IO) : Nil + def to_s(io) io << @examples << " examples, " << @failures << " failures" if @errors > 0 diff --git a/src/spectator/formatting/summary.cr b/src/spectator/formatting/summary.cr index b866307..50a2b1b 100644 --- a/src/spectator/formatting/summary.cr +++ b/src/spectator/formatting/summary.cr @@ -63,22 +63,15 @@ module Spectator::Formatting # Displays one or more blocks for a failed example. # Each block is a failed expectation or error raised in the example. private def dump_failed_example(example, index) - # Retrieve the ultimate reason for failing. - error = example.result.as?(FailResult).try(&.error) - - # Prevent displaying duplicated output from expectation. - # Display `ExampleFailed` but not `ExpectationFailed`. - error = nil if error.responds_to?(:expectation) - - # Gather all failed expectations. + result = example.result.as?(ErrorResult) failed_expectations = example.result.expectations.select(&.failed?) block_count = failed_expectations.size - block_count += 1 if error # Add an extra block for final error if it's significant. + block_count += 1 if result # Don't use sub-index if there was only one problem. if block_count == 1 - if error - io.puts Components::ErrorResultBlock.new(example, index, error) + if result + io.puts Components::ErrorResultBlock.new(example, index, result) else io.puts Components::FailResultBlock.new(example, index, failed_expectations.first) end @@ -86,7 +79,7 @@ module Spectator::Formatting failed_expectations.each_with_index(1) do |expectation, subindex| io.puts Components::FailResultBlock.new(example, index, expectation, subindex) end - io.puts Components::ErrorResultBlock.new(example, index, error, block_count) if error + io.puts Components::ErrorResultBlock.new(example, index, result, block_count) if result end end end 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/location.cr b/src/spectator/location.cr index 7b06eb9..688fdb6 100644 --- a/src/spectator/location.cr +++ b/src/spectator/location.cr @@ -59,7 +59,7 @@ module Spectator # ```text # FILE:LINE # ``` - def to_s(io : IO) : Nil + def to_s(io) io << path << ':' << line end end diff --git a/src/spectator/matchers/attributes_matcher.cr b/src/spectator/matchers/attributes_matcher.cr index c66ecc4..8ecd714 100644 --- a/src/spectator/matchers/attributes_matcher.cr +++ b/src/spectator/matchers/attributes_matcher.cr @@ -15,7 +15,7 @@ module Spectator::Matchers extend self # Text displayed when a method is undefined. - def inspect(io : IO) : Nil + def inspect(io) io << "" end 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/matchers/receive_matcher.cr b/src/spectator/matchers/receive_matcher.cr index 560cabd..d261db1 100644 --- a/src/spectator/matchers/receive_matcher.cr +++ b/src/spectator/matchers/receive_matcher.cr @@ -81,7 +81,7 @@ module Spectator::Matchers # Short text about the matcher's purpose. def description : String - "received #{@stub.message} #{humanize_count}" + "received #{@stub} #{humanize_count}" end # Actually performs the test against the expression (value or block). @@ -89,10 +89,10 @@ module Spectator::Matchers stubbed = actual.value calls = relevant_calls(stubbed) if @count.includes?(calls.size) - SuccessfulMatchData.new("#{actual.label} received #{@stub.message} #{humanize_count}") + SuccessfulMatchData.new("#{actual.label} received #{@stub} #{humanize_count}") else - FailedMatchData.new("#{actual.label} received #{@stub.message} #{humanize_count}", - "#{actual.label} did not receive #{@stub.message}", values(actual).to_a) + FailedMatchData.new("#{actual.label} received #{@stub} #{humanize_count}", + "#{actual.label} did not receive #{@stub}", values(actual).to_a) end end @@ -106,9 +106,9 @@ module Spectator::Matchers stubbed = actual.value calls = relevant_calls(stubbed) if @count.includes?(calls.size) - FailedMatchData.new("#{actual.label} did not receive #{@stub.message}", "#{actual.label} received #{@stub.message}", negated_values(actual).to_a) + FailedMatchData.new("#{actual.label} did not receive #{@stub}", "#{actual.label} received #{@stub}", negated_values(actual).to_a) else - SuccessfulMatchData.new("#{actual.label} did not receive #{@stub.message} #{humanize_count}") + SuccessfulMatchData.new("#{actual.label} did not receive #{@stub} #{humanize_count}") end end @@ -120,7 +120,7 @@ module Spectator::Matchers # Additional information about the match failure. private def values(actual : Expression(T)) forall T { - expected: @stub.message, + expected: @stub.to_s, actual: method_call_list(actual.value), } end @@ -128,7 +128,7 @@ module Spectator::Matchers # Additional information about the match failure when negated. private def negated_values(actual : Expression(T)) forall T { - expected: "Not #{@stub.message}", + expected: "Not #{@stub}", actual: method_call_list(actual.value), } end diff --git a/src/spectator/matchers/value_matcher.cr b/src/spectator/matchers/value_matcher.cr index a88f457..760578b 100644 --- a/src/spectator/matchers/value_matcher.cr +++ b/src/spectator/matchers/value_matcher.cr @@ -1,5 +1,3 @@ -require "../expression" -require "../value" require "./standard_matcher" module Spectator::Matchers @@ -24,7 +22,7 @@ module Spectator::Matchers # Creates the value matcher. # The expected value is stored for later use. - def initialize(@expected : ::Spectator::Value(ExpectedType)) + def initialize(@expected : Value(ExpectedType)) end # Additional information about the match failure. diff --git a/src/spectator/mocks/abstract_arguments.cr b/src/spectator/mocks/abstract_arguments.cr index 4a6f75f..dacc43f 100644 --- a/src/spectator/mocks/abstract_arguments.cr +++ b/src/spectator/mocks/abstract_arguments.cr @@ -1,61 +1,5 @@ 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) - a.each do |k, v1| - v2 = b.fetch(k) { return false } - return false unless compare_values(v1, v2) - end - true - end - - # Utility method for comparing two arguments considering special types. - # Some types used for case-equality don't work well with unexpected right-hand types. - # This can happen when the right side is a massive union of types. - private def compare_values(a, b) - case a - when Proc - # Using procs as argument matchers isn't supported currently. - # Compare directly instead. - a == b - when Range - # Ranges can only be matched against if their right side is comparable. - # Ensure the right side is comparable, otherwise compare directly. - return a === b if b.is_a?(Comparable(typeof(b))) - a == b - when Tuple, Array - return compare_tuples(a, b) if b.is_a?(Tuple) || b.is_a?(Array) - a === b - when NamedTuple, Hash - return compare_named_tuples(a, b) if b.is_a?(NamedTuple) || b.is_a?(Hash) - a === b - else - a === b - end - end end end diff --git a/src/spectator/mocks/allow.cr b/src/spectator/mocks/allow.cr index 4754690..dd163b2 100644 --- a/src/spectator/mocks/allow.cr +++ b/src/spectator/mocks/allow.cr @@ -1,4 +1,3 @@ -require "../harness" require "./stub" require "./stubbable" require "./stubbed_type" @@ -22,7 +21,6 @@ module Spectator # Applies a stub to the targeted stubbable object. def to(stub : Stub) : Nil @target._spectator_define_stub(stub) - Harness.current?.try &.cleanup { @target._spectator_remove_stub(stub) } end end end diff --git a/src/spectator/mocks/arguments.cr b/src/spectator/mocks/arguments.cr index f15a0ba..264f0fc 100644 --- a/src/spectator/mocks/arguments.cr +++ b/src/spectator/mocks/arguments.cr @@ -4,19 +4,22 @@ module Spectator # Arguments used in a method call. # # Can also be used to match arguments. - # *Args* must be a `Tuple` representing the standard arguments. - # *KWArgs* must be a `NamedTuple` type representing extra keyword arguments. - class Arguments(Args, KWArgs) < AbstractArguments + # *T* must be a `Tuple` type representing the positional arguments. + # *NT* must be a `NamedTuple` type representing the keyword arguments. + class Arguments(T, NT) < AbstractArguments # Positional arguments. - getter args : Args + getter args : T # Keyword arguments. - getter kwargs : KWArgs + getter kwargs : NT # Creates arguments used in a method call. - def initialize(@args : Args, @kwargs : KWArgs) - {% raise "Positional arguments (generic type Args) must be a Tuple" unless Args <= Tuple %} - {% raise "Keyword arguments (generic type KWArgs) must be a NamedTuple" unless KWArgs <= NamedTuple %} + def initialize(@args : T, @kwargs : NT) + end + + # Constructs an instance from literal arguments. + def self.capture(*args, **kwargs) : AbstractArguments + new(args, kwargs).as(AbstractArguments) end # Instance of empty arguments. @@ -27,14 +30,9 @@ module Spectator nil.as(AbstractArguments?) end - # Friendlier constructor for capturing arguments. - def self.capture(*args, **kwargs) - new(args, kwargs) - end - # Returns the positional argument at the specified index. def [](index : Int) - args[index] + @args[index] end # Returns the specified named argument. @@ -42,16 +40,6 @@ module Spectator @kwargs[arg] end - # Returns all arguments and splatted arguments as a tuple. - def positional : Tuple - args - end - - # Returns all named positional and keyword arguments as a named tuple. - def named : NamedTuple - kwargs - end - # Constructs a string representation of the arguments. def to_s(io : IO) : Nil return io << "(no args)" if args.empty? && kwargs.empty? @@ -65,46 +53,39 @@ module Spectator end # Add the keyword arguments. - kwargs.each_with_index(args.size) do |key, value, i| - io << ", " if i > 0 - io << key << ": " - value.inspect(io) + size = args.size + kwargs.size + kwargs.each_with_index(args.size) do |k, v, i| + io << ", " if 0 < i < size + io << k << ": " + v.inspect(io) end io << ')' end # Checks if this set of arguments and another are equal. - def ==(other : AbstractArguments) - positional == other.positional && kwargs == other.kwargs + def ==(other : Arguments) + args == other.args && kwargs == other.kwargs end # Checks if another set of arguments matches this set of arguments. def ===(other : Arguments) - compare_tuples(positional, other.positional) && compare_named_tuples(kwargs, other.kwargs) + args === other.args && named_tuples_match?(kwargs, other.kwargs) end - # :ditto: - def ===(other : FormalArguments) - return false unless compare_named_tuples(kwargs, other.named) + # Checks if two named tuples match. + # + # Uses case equality (`===`) on every key-value pair. + # NamedTuple doesn't have a `===` operator, even though Tuple does. + private def named_tuples_match?(a : NamedTuple, b : NamedTuple) + return false if a.size != b.size - 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] - i += 1 - return false unless compare_values(v1, v2) + a.each do |k, v| + return false unless b.has_key?(k) + return false unless v === b[k] end - other.splat.try &.each do |v2| - v1 = positional.fetch(i) { return false } - i += 1 - return false unless compare_values(v1, v2) - end - - i == positional.size + true end end end diff --git a/src/spectator/mocks/double.cr b/src/spectator/mocks/double.cr index e5e43f4..8562ca6 100644 --- a/src/spectator/mocks/double.cr +++ b/src/spectator/mocks/double.cr @@ -61,7 +61,7 @@ module Spectator end {% end %} - {{block.body if block}} + {% if block %}{{block.body}}{% end %} end end @@ -95,38 +95,16 @@ module Spectator false end - # Simplified string representation of a double. - # Avoids displaying nested content and bloating method instantiation. - def to_s(io : IO) : Nil - io << "#<" + {{@type.name(generic_args: false).stringify}} + " " - io << _spectator_stubbed_name << '>' - end - - # :ditto: - def inspect(io : IO) : Nil - io << "#<" + {{@type.name(generic_args: false).stringify}} + " " - io << _spectator_stubbed_name - - io << ":0x" - object_id.to_s(io, 16) - io << '>' - end - # Defines a stub to change the behavior of a method in this double. # # 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}" } - @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 +134,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 +164,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) @@ -201,13 +179,12 @@ module Spectator # Handle all methods but only respond to configured messages. # Raises an `UnexpectedMessage` error for non-configures messages. macro method_missing(call) - args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{{call.named_args.splat if call.named_args}}) + Log.trace { "Got undefined method `{{call.name}}({{*call.args}}{% if call.named_args %}{% unless call.args.empty? %}, {% end %}{{*call.named_args}}{% end %}){% if call.block %} { ... }{% end %}`" } + args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{% if call.named_args %}{{*call.named_args}}{% end %}) call = ::Spectator::MethodCall.new({{call.name.symbolize}}, args) _spectator_record_call(call) - Log.trace { "#{inspect} 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/exception_stub.cr b/src/spectator/mocks/exception_stub.cr index e7b6cb9..9aee69c 100644 --- a/src/spectator/mocks/exception_stub.cr +++ b/src/spectator/mocks/exception_stub.cr @@ -20,12 +20,6 @@ module Spectator def initialize(method : Symbol, @exception : Exception, constraint : AbstractArguments? = nil, location : Location? = nil) super(method, constraint, location) end - - # String representation of the stub, formatted as a method call. - def to_s(io : IO) : Nil - super - io << " # raises " << @exception - end end module StubModifiers diff --git a/src/spectator/mocks/formal_arguments.cr b/src/spectator/mocks/formal_arguments.cr deleted file mode 100644 index 1c0ca69..0000000 --- a/src/spectator/mocks/formal_arguments.cr +++ /dev/null @@ -1,133 +0,0 @@ -require "./abstract_arguments" - -module Spectator - # Arguments passed into a method. - # - # *Args* must be a `NamedTuple` type representing the standard arguments. - # *Splat* must be a `Tuple` type representing the extra positional arguments. - # *DoubleSplat* must be a `NamedTuple` type representing extra keyword arguments. - class FormalArguments(Args, Splat, DoubleSplat) < AbstractArguments - # Positional arguments. - getter args : Args - - # Additional positional arguments. - getter splat : Splat - - # Keyword arguments. - getter kwargs : DoubleSplat - - # Name of the splat argument, if used. - getter splat_name : Symbol? - - # Creates arguments used in a method call. - def initialize(@args : Args, @splat_name : Symbol?, @splat : Splat, @kwargs : DoubleSplat) - {% raise "Positional arguments (generic type Args) must be a NamedTuple" unless Args <= NamedTuple %} - {% raise "Splat arguments (generic type Splat) must be a Tuple" unless Splat <= Tuple || Splat <= Nil %} - {% raise "Keyword arguments (generic type DoubleSplat) must be a NamedTuple" unless DoubleSplat <= NamedTuple %} - end - - # Creates arguments used in a method call. - def self.new(args : Args, kwargs : DoubleSplat) - new(args, nil, nil, kwargs) - end - - # Captures arguments passed to a call. - def self.build(args = NamedTuple.new, kwargs = NamedTuple.new) - new(args, nil, nil, kwargs) - end - - # :ditto: - def self.build(args : NamedTuple, splat_name : Symbol, splat : Tuple, kwargs = NamedTuple.new) - new(args, splat_name, splat, kwargs) - end - - # Instance of empty arguments. - class_getter none : AbstractArguments = build - - # Returns the positional argument at the specified index. - def [](index : Int) - positional[index] - end - - # Returns the specified named argument. - def [](arg : Symbol) - return @args[arg] if @args.has_key?(arg) - @kwargs[arg] - end - - # Returns all arguments and splatted arguments as a tuple. - def positional : Tuple - if (splat = @splat) - args.values + splat - else - args.values - end - end - - # Returns all named positional and keyword arguments as a named tuple. - def named : NamedTuple - args.merge(kwargs) - end - - # Constructs a string representation of the arguments. - def to_s(io : IO) : Nil - return io << "(no args)" if args.empty? && ((splat = @splat).nil? || splat.empty?) && kwargs.empty? - - io << '(' - - # Add the positional arguments. - {% if Args < NamedTuple %} - # Include argument names. - args.each_with_index do |name, value, i| - io << ", " if i > 0 - io << name << ": " - value.inspect(io) - end - {% else %} - args.each_with_index do |arg, i| - io << ", " if i > 0 - arg.inspect(io) - end - {% end %} - - # Add the splat arguments. - if (splat = @splat) && !splat.empty? - io << ", " unless args.empty? - if splat_name = !args.empty? && @splat_name - io << '*' << splat_name << ": {" - end - splat.each_with_index do |arg, i| - io << ", " if i > 0 - arg.inspect(io) - end - io << '}' if splat_name - end - - # Add the keyword arguments. - offset = args.size - offset += splat.size if (splat = @splat) - kwargs.each_with_index(offset) do |key, value, i| - io << ", " if i > 0 - io << key << ": " - value.inspect(io) - end - - io << ')' - end - - # Checks if this set of arguments and another are equal. - def ==(other : AbstractArguments) - positional == other.positional && kwargs == other.kwargs - end - - # Checks if another set of arguments matches this set of arguments. - def ===(other : Arguments) - compare_tuples(positional, other.positional) && compare_named_tuples(kwargs, other.kwargs) - end - - # :ditto: - def ===(other : FormalArguments) - compare_named_tuples(args, other.args) && compare_tuples(splat, other.splat) && compare_named_tuples(kwargs, other.kwargs) - end - end -end diff --git a/src/spectator/mocks/lazy_double.cr b/src/spectator/mocks/lazy_double.cr index 75fe30c..af3925d 100644 --- a/src/spectator/mocks/lazy_double.cr +++ b/src/spectator/mocks/lazy_double.cr @@ -26,24 +26,15 @@ module Spectator super(_spectator_double_stubs + message_stubs) end - # Defines a stub to change the behavior of a method in this double. - # - # NOTE: Defining a stub for a method not defined in the double's type raises an error. - protected def _spectator_define_stub(stub : Stub) : Nil - return super if Messages.types.has_key?(stub.method) - - raise "Can't define stub #{stub} on lazy double because it wasn't initially defined." - end - # Returns the double's name formatted for user output. 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 @@ -52,13 +43,13 @@ module Spectator # Handles all messages. macro method_missing(call) + Log.trace { "Got undefined method `{{call.name}}({{*call.args}}{% if call.named_args %}{% unless call.args.empty? %}, {% end %}{{*call.named_args}}{% end %}){% if call.block %} { ... }{% end %}`" } + # Capture information about the call. - %args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{{call.named_args.splat if call.named_args}}) + %args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{% if call.named_args %}{{*call.named_args}}{% end %}) %call = ::Spectator::MethodCall.new({{call.name.symbolize}}, %args) _spectator_record_call(%call) - Log.trace { "#{inspect} got undefined method `#{%call}{% if call.block %} { ... }{% end %}`" } - # Attempt to find a stub that satisfies the method call and arguments. if %stub = _spectator_find_stub(%call) # Cast the stub or return value to the expected type. diff --git a/src/spectator/mocks/method_call.cr b/src/spectator/mocks/method_call.cr index 9c5fd01..7094f4a 100644 --- a/src/spectator/mocks/method_call.cr +++ b/src/spectator/mocks/method_call.cr @@ -1,6 +1,5 @@ require "./abstract_arguments" require "./arguments" -require "./formal_arguments" module Spectator # Stores information about a call to a method. @@ -17,26 +16,13 @@ module Spectator # Creates a method call by splatting its arguments. def self.capture(method : Symbol, *args, **kwargs) - arguments = Arguments.capture(*args, **kwargs).as(AbstractArguments) - new(method, arguments) - end - - # Creates a method call from within a method. - # Takes the same arguments as `FormalArguments.build` but with the method name first. - def self.build(method : Symbol, *args, **kwargs) - arguments = FormalArguments.build(*args, **kwargs).as(AbstractArguments) + arguments = Arguments.new(args, kwargs).as(AbstractArguments) new(method, arguments) end # 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..174d183 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,18 @@ module Spectator end {% end %} - 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 +69,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 %} @@ -113,7 +80,7 @@ module Spectator macro finished stub_type {{mocked_type.id}} - {{block.body if block}} + {% if block %}{{block.body}}{% end %} end end {% end %} @@ -149,7 +116,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 +125,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 +139,7 @@ module Spectator entry.stubs end - 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 +161,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 +170,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 %} @@ -218,7 +181,7 @@ module Spectator macro finished stub_type {{type_name.id}} - {{block.body if block}} + {% if block %}{{block.body}}{% end %} end end {% end %} 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/mocked.cr b/src/spectator/mocks/mocked.cr index 280eef8..be25ef0 100644 --- a/src/spectator/mocks/mocked.cr +++ b/src/spectator/mocks/mocked.cr @@ -26,10 +26,6 @@ module Spectator _spectator_stubs.unshift(stub) end - def _spectator_remove_stub(stub : Stub) : Nil - _spectator_stubs.delete(stub) - end - def _spectator_clear_stubs : Nil _spectator_stubs.clear end diff --git a/src/spectator/mocks/null_double.cr b/src/spectator/mocks/null_double.cr index 587f4ab..978418c 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,22 +42,22 @@ 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 # Handles all undefined messages. # Returns stubbed values if available, otherwise delegates to `#_spectator_abstract_stub_fallback`. macro method_missing(call) + Log.trace { "Got undefined method `{{call.name}}({{*call.args}}{% if call.named_args %}{% unless call.args.empty? %}, {% end %}{{*call.named_args}}{% end %}){% if call.block %} { ... }{% end %}`" } + # Capture information about the call. - %args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{{call.named_args.splat if call.named_args}}) + %args = ::Spectator::Arguments.capture({{call.args.splat(", ")}}{% if call.named_args %}{{*call.named_args}}{% end %}) %call = ::Spectator::MethodCall.new({{call.name.symbolize}}, %args) _spectator_record_call(%call) - Log.trace { "#{inspect} got undefined method `#{%call}{% if call.block %} { ... }{% end %}`" } - self end end diff --git a/src/spectator/mocks/reference_mock_registry.cr b/src/spectator/mocks/reference_mock_registry.cr index 84227d1..20481f0 100644 --- a/src/spectator/mocks/reference_mock_registry.cr +++ b/src/spectator/mocks/reference_mock_registry.cr @@ -25,12 +25,6 @@ module Spectator @entries[key] end - # Retrieves all stubs defined for a mocked object or nil if the object isn't mocked yet. - def []?(object : Reference) - key = Box.box(object) - @entries[key]? - end - # Retrieves all stubs defined for a mocked object. # # Yields to the block on the first retrieval. diff --git a/src/spectator/mocks/stub.cr b/src/spectator/mocks/stub.cr index 606894d..e28c431 100644 --- a/src/spectator/mocks/stub.cr +++ b/src/spectator/mocks/stub.cr @@ -22,23 +22,6 @@ module Spectator def initialize(@method : Symbol, @constraint : AbstractArguments? = nil, @location : Location? = nil) end - # String representation of the stub, formatted as a method call. - def message(io : IO) : Nil - io << "#" << method << (constraint || "(any args)") - end - - # String representation of the stub, formatted as a method call. - def message - String.build do |str| - message(str) - end - end - - # String representation of the stub, formatted as a method definition. - def to_s(io : IO) : Nil - message(io) - end - # Checks if a method call should receive the response from this stub. def ===(call : MethodCall) return false if method != call.method @@ -46,5 +29,10 @@ module Spectator constraint === call.arguments end + + # String representation of the stub, formatted as a method call. + def to_s(io : IO) : Nil + io << "#" << method << (constraint || "(any args)") + end end end diff --git a/src/spectator/mocks/stubbable.cr b/src/spectator/mocks/stubbable.cr index 3385ad4..aa02976 100644 --- a/src/spectator/mocks/stubbable.cr +++ b/src/spectator/mocks/stubbable.cr @@ -1,5 +1,5 @@ require "../dsl/reserved" -require "./formal_arguments" +require "./arguments" require "./method_call" require "./stub" require "./typed_stub" @@ -28,9 +28,6 @@ module Spectator # Defines a stub to change the behavior of a method. abstract def _spectator_define_stub(stub : Stub) : Nil - # Removes a specific, previously defined stub. - abstract def _spectator_remove_stub(stub : Stub) : Nil - # Clears all previously defined stubs. abstract def _spectator_clear_stubs : Nil @@ -118,7 +115,7 @@ module Spectator {% raise "Default stub cannot be an abstract method" if method.abstract? %} {% raise "Cannot stub method with reserved keyword as name - #{method.name}" if method.name.starts_with?("_spectator") || ::Spectator::DSL::RESERVED_KEYWORDS.includes?(method.name.symbolize) %} - {{visibility.id if visibility != :public}} def {{"#{method.receiver}.".id if method.receiver}}{{method.name}}( + {{visibility.id if visibility != :public}} def {{method.receiver && "#{method.receiver}.".id}}{{method.name}}( {% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %} {% if method.double_splat %}**{{method.double_splat}}, {% end %} {% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %} @@ -126,64 +123,25 @@ 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). # This chunk of code must reconstruct the method signature exactly as it was originally. # If it doesn't match, it doesn't override the method and the stubbing won't work. %} - {{visibility.id if visibility != :public}} def {{"#{method.receiver}.".id if method.receiver}}{{method.name}}( + {{visibility.id if visibility != :public}} def {{method.receiver && "#{method.receiver}.".id}}{{method.name}}( {% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %} {% 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 %} # Capture information about the call. - %call = ::Spectator::MethodCall.build( - {{method.name.symbolize}}, - ::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 %} - ::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}}) + %args = ::Spectator::Arguments.capture( + {% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg.internal_name}}, {% end %} + {% if method.double_splat %}**{{method.double_splat}}{% end %} ) + %call = ::Spectator::MethodCall.new({{method.name.symbolize}}, %args) _spectator_record_call(%call) # Attempt to find a stub that satisfies the method call and arguments. @@ -192,24 +150,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 }}) @@ -267,7 +211,7 @@ module Spectator %} {% unless method.abstract? %} - {{visibility.id if visibility != :public}} def {{"#{method.receiver}.".id if method.receiver}}{{method.name}}( + {{visibility.id if visibility != :public}} def {{method.receiver && "#{method.receiver}.".id}}{{method.name}}( {% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %} {% if method.double_splat %}**{{method.double_splat}}, {% end %} {% if method.block_arg %}&{{method.block_arg}}{% elsif method.accepts_block? %}&{% end %} @@ -275,42 +219,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. @@ -318,23 +227,18 @@ module Spectator # This chunk of code must reconstruct the method signature exactly as it was originally. # If it doesn't match, it doesn't override the method and the stubbing won't work. %} - {{visibility.id if visibility != :public}} def {{"#{method.receiver}.".id if method.receiver}}{{method.name}}( + {{visibility.id if visibility != :public}} def {{method.receiver && "#{method.receiver}.".id}}{{method.name}}( {% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg}}, {% end %} {% 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 %} # Capture information about the call. - %call = ::Spectator::MethodCall.build( - {{method.name.symbolize}}, - ::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 %} - ::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}}) + %args = ::Spectator::Arguments.capture( + {% for arg, i in method.args %}{% if i == method.splat_index %}*{% end %}{{arg.internal_name}}, {% end %} + {% if method.double_splat %}**{{method.double_splat}}{% end %} ) + %call = ::Spectator::MethodCall.new({{method.name.symbolize}}, %args) _spectator_record_call(%call) # Attempt to find a stub that satisfies the method call and arguments. @@ -342,25 +246,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,96 +325,67 @@ 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 && "#{method.receiver}.".id}}{{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 && "#{method.receiver}.".id}}{{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. + # Utility macro for casting a stub (and it's return value) to the correct type. # # *stub* is the variable holding the stub. # *call* is the variable holding the captured method call. @@ -530,38 +395,49 @@ module Spectator # - `:raise` - raise a `TypeCastError`. # - `:no_return` - raise as no value should be returned. private macro _spectator_cast_stub_value(stub, call, type, fail_cast = :nil) - {% if fail_cast == :no_return %} - {{stub}}.call({{call}}) - raise TypeCastError.new("#{_spectator_stubbed_name} received message #{ {{call}} } and is attempting to return a value, but it shouldn't have returned (`NoReturn`).") - {% else %} - # 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 stub to the method's return type. + # If successful, return the value of the stub. + # This is a common usage where the return type is simple and matches the stub type exactly. + if %typed = {{stub}}.as?(::Spectator::TypedStub({{type}})) + %typed.call({{call}}) + else + # The stub couldn't be easily cast to match the return type. - # 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}}) + # Even though all stubs will have a `#call` method, the compiler doesn't seem to agree. + # Assert that it will (this should never fail). + raise TypeCastError.new("Stub has no value") unless {{stub}}.responds_to?(:call) - {% if fail_cast == :nil %} - %cast - {% elsif fail_cast == :raise %} - # Check if nil was returned by the stub and if its okay to return it. - if %value.nil? && %type.nilable? - # Value was nil and nil is allowed to be returned. - %type.cast(%cast) - elsif %cast.nil? - # The stubbed value was something else entirely and cannot be cast to the return type. - raise TypeCastError.new("#{_spectator_stubbed_name} received message #{ {{call}} } and is attempting to return a `#{%value.class}`, but returned type must be `#{%type}`.") - else - # Types match and value can be returned as cast type. - %cast - end + {% if fail_cast == :no_return %} + {{stub}}.call({{call}}) + raise TypeCastError.new("#{_spectator_stubbed_name} received message #{ {{call}} } and is attempting to return a value, but it shouldn't have returned (`NoReturn`).") {% else %} - {% raise "fail_cast must be :nil, :raise, or :no_return, but got: #{fail_cast}" %} + # 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}}) + + # 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. + if %cast = %value.as?({{type}}) + %cast + 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. + # 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 end end end diff --git a/src/spectator/mocks/stubbed_type.cr b/src/spectator/mocks/stubbed_type.cr index 5362b84..a5588ee 100644 --- a/src/spectator/mocks/stubbed_type.cr +++ b/src/spectator/mocks/stubbed_type.cr @@ -20,10 +20,6 @@ module Spectator _spectator_stubs.unshift(stub) end - def _spectator_remove_stub(stub : Stub) : Nil - _spectator_stubs.delete(stub) - end - def _spectator_clear_stubs : Nil _spectator_stubs.clear end diff --git a/src/spectator/mocks/typed_stub.cr b/src/spectator/mocks/typed_stub.cr index eabbcb9..5067215 100644 --- a/src/spectator/mocks/typed_stub.cr +++ b/src/spectator/mocks/typed_stub.cr @@ -9,11 +9,5 @@ module Spectator abstract class TypedStub(T) < Stub # Invokes the stubbed implementation. abstract def call(call : MethodCall) : T - - # String representation of the stub, formatted as a method call. - def to_s(io : IO) : Nil - super - io << " : " << T - end end end diff --git a/src/spectator/mocks/value_mock_registry.cr b/src/spectator/mocks/value_mock_registry.cr index 1efd0b0..5763509 100644 --- a/src/spectator/mocks/value_mock_registry.cr +++ b/src/spectator/mocks/value_mock_registry.cr @@ -29,12 +29,6 @@ module Spectator @entries[key] end - # Retrieves all stubs defined for a mocked object or nil if the object isn't mocked yet. - def []?(object : T) - key = value_bytes(object) - @entries[key]? - end - # Retrieves all stubs defined for a mocked object. # # Yields to the block on the first retrieval. diff --git a/src/spectator/mocks/value_stub.cr b/src/spectator/mocks/value_stub.cr index 7a84d19..464c38b 100644 --- a/src/spectator/mocks/value_stub.cr +++ b/src/spectator/mocks/value_stub.cr @@ -20,13 +20,6 @@ module Spectator def initialize(method : Symbol, @value : T, constraint : AbstractArguments? = nil, location : Location? = nil) super(method, constraint, location) end - - # String representation of the stub, formatted as a method call and return value. - def to_s(io : IO) : Nil - super - io << " # => " - @value.inspect(io) - end end module StubModifiers diff --git a/src/spectator/name_node_filter.cr b/src/spectator/name_node_filter.cr index c404246..6d4e64a 100644 --- a/src/spectator/name_node_filter.cr +++ b/src/spectator/name_node_filter.cr @@ -9,7 +9,7 @@ module Spectator # Checks whether the node satisfies the filter. def includes?(node) : Bool - node.to_s.includes?(@name) + @name == node.to_s end end end diff --git a/src/spectator/node.cr b/src/spectator/node.cr index 6a5d068..c5a64b6 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. @@ -76,12 +66,12 @@ module Spectator # Constructs the full name or description of the node. # This prepends names of groups this node is part of. - def to_s(io : IO) : Nil + def to_s(io) display_name.to_s(io) end # Exposes information about the node useful for debugging. - def inspect(io : IO) : Nil + def inspect(io) # Full node name. io << '"' << self << '"' diff --git a/src/spectator/pass_result.cr b/src/spectator/pass_result.cr index 21ed6c5..2b62383 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 @@ -24,7 +24,7 @@ module Spectator end # One-word description of the result. - def to_s(io : IO) : Nil + def to_s(io) io << "pass" 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..03700d9 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 @@ -43,7 +43,7 @@ module Spectator end # One-word description of the result. - def to_s(io : IO) : Nil + def to_s(io) io << "pending" 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/system_exit.cr b/src/spectator/system_exit.cr index d94711e..95a2b22 100644 --- a/src/spectator/system_exit.cr +++ b/src/spectator/system_exit.cr @@ -20,9 +20,6 @@ class Process # Replace the typically used exit method with a method that raises. # This allows tests to catch attempts to exit the application. def self.exit(status = 0) : NoReturn - # Re-enable log that is disabled from at-exit handlers. - ::Log.setup_from_env(default_level: :none) - raise ::Spectator::SystemExit.new(status: status) end 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..5c79910 100644 --- a/src/spectator/wrapper.cr +++ b/src/spectator/wrapper.cr @@ -13,13 +13,13 @@ 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 + Box(T).unbox(@pointer) end # Retrieves the previously wrapped value. @@ -34,20 +34,7 @@ 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 + Box(T).unbox(@pointer) 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