diff --git a/.guardian.yml b/.guardian.yml new file mode 100644 index 0000000..ad67b38 --- /dev/null +++ b/.guardian.yml @@ -0,0 +1,5 @@ +files: ./**/*.cr +run: time crystal spec --error-trace +--- +files: ./shard.yml +run: shards diff --git a/shard.yml b/shard.yml index 3b3e779..661dd1b 100644 --- a/shard.yml +++ b/shard.yml @@ -1,12 +1,12 @@ name: spectator -version: 0.9.1 +version: 0.9.2 description: | A feature-rich spec testing framework for Crystal with similarities to RSpec. authors: - Michael Miller -crystal: 0.31.1 +crystal: 0.32.1 license: MIT diff --git a/spec/matchers/equality_matcher_spec.cr b/spec/matchers/equality_matcher_spec.cr new file mode 100644 index 0000000..3b6b83b --- /dev/null +++ b/spec/matchers/equality_matcher_spec.cr @@ -0,0 +1,32 @@ +require "../spec_helper" + +Spectator.describe "eq matcher" do + it "is true for equal values" do + expect(42).to eq(42) + end + + it "is false for inequal values" do + expect(42).to_not eq(24) + end + + it "is true for identical references" do + string = "foobar" + expect(string).to eq(string) + end + + it "is false for different references" do + string1 = "foo" + string2 = "bar" + expect(string1).to_not eq(string2) + end + + double(:fake) do + stub instance.==(other) { true } + end + + it "uses the == operator" do + dbl = double(:fake) + expect(42).to eq(dbl) + expect(dbl).to have_received(:==).with(42).once + end +end diff --git a/spec/rspec/expectations/all_matcher_spec.cr b/spec/rspec/expectations/all_matcher_spec.cr new file mode 100644 index 0000000..35ad484 --- /dev/null +++ b/spec/rspec/expectations/all_matcher_spec.cr @@ -0,0 +1,39 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/all-matcher +# and modified to fit Spectator and Crystal. +Spectator.describe "`all` matcher" do + context "array usage" do + describe [1, 3, 5] do + it { is_expected.to all(be_odd) } + it { is_expected.to all(be_an(Int32)) } # Changed to Int32 to satisfy compiler. + it { is_expected.to all(be < 10) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.to all(be_even) } + xit { is_expected.to all(be_a(String)) } + xit { is_expected.to all(be > 2) } + end + end + + context "compound matcher usage" do + # Changed `include` to `contain` to match our own. + # `include` is a keyword and can't be used as a method name in Crystal. + + # TODO: Add support for compound matchers. + describe ["anything", "everything", "something"] do + xit { is_expected.to all(be_a(String)) } # .and contain("thing") ) } + xit { is_expected.to all(be_a(String)) } # .and end_with("g") ) } + xit { is_expected.to all(start_with("s")) } # .or contain("y") ) } + + # deliberate failures + # TODO: Add support for expected failures. + # TODO: Add support for compound matchers. + xit { is_expected.to all(contain("foo")) } # .and contain("bar") ) } + xit { is_expected.to all(be_a(String)) } # .and start_with("a") ) } + xit { is_expected.to all(start_with("a")) } # .or contain("z") ) } + end + end +end diff --git a/spec/rspec/expectations/be_matchers_spec.cr b/spec/rspec/expectations/be_matchers_spec.cr new file mode 100644 index 0000000..79636f2 --- /dev/null +++ b/spec/rspec/expectations/be_matchers_spec.cr @@ -0,0 +1,70 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/be-matchers +# and modified to fit Spectator and Crystal. +Spectator.describe "`be` matchers" do + context "be_truthy matcher" do + specify { expect(true).to be_truthy } + specify { expect(7).to be_truthy } + specify { expect("foo").to be_truthy } + specify { expect(nil).not_to be_truthy } + specify { expect(false).not_to be_truthy } + + # deliberate failures + # TODO: Add support for expected failures. + pending { expect(true).not_to be_truthy } + pending { expect(7).not_to be_truthy } + pending { expect("foo").not_to be_truthy } + pending { expect(nil).to be_truthy } + pending { expect(false).to be_truthy } + end + + context "be_falsey matcher" do + specify { expect(nil).to be_falsey } + specify { expect(false).to be_falsey } + specify { expect(true).not_to be_falsey } + specify { expect(7).not_to be_falsey } + specify { expect("foo").not_to be_falsey } + + # deliberate failures + # TODO: Add support for expected failures. + pending { expect(nil).not_to be_falsey } + pending { expect(false).not_to be_falsey } + pending { expect(true).to be_falsey } + pending { expect(7).to be_falsey } + pending { expect("foo").to be_falsey } + end + + context "be_nil matcher" do + specify { expect(nil).to be_nil } + specify { expect(false).not_to be_nil } + specify { expect(true).not_to be_nil } + specify { expect(7).not_to be_nil } + specify { expect("foo").not_to be_nil } + + # deliberate failures + # TODO: Add support for expected failures. + pending { expect(nil).not_to be_nil } + pending { expect(false).to be_nil } + pending { expect(true).to be_nil } + pending { expect(7).to be_nil } + pending { expect("foo").to be_nil } + end + + context "be matcher" do + specify { expect(true).to be } + specify { expect(7).to be } + specify { expect("foo").to be } + specify { expect(nil).not_to be } + specify { expect(false).not_to be } + + # deliberate failures + # TODO: Add support for expected failures. + pending { expect(true).not_to be } + pending { expect(7).not_to be } + pending { expect("foo").not_to be } + pending { expect(nil).to be } + pending { expect(false).to be } + end +end diff --git a/spec/rspec/expectations/be_within_matcher_spec.cr b/spec/rspec/expectations/be_within_matcher_spec.cr new file mode 100644 index 0000000..0307d1b --- /dev/null +++ b/spec/rspec/expectations/be_within_matcher_spec.cr @@ -0,0 +1,25 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/be-within-matcher +# and modified to fit Spectator and Crystal. +Spectator.describe "`be_within` matcher" do + context "basic usage" do + describe 27.5 do + it { is_expected.to be_within(0.5).of(27.9) } + it { is_expected.to be_within(0.5).of(28.0) } + it { is_expected.to be_within(0.5).of(27.1) } + it { is_expected.to be_within(0.5).of(27.0) } + + it { is_expected.not_to be_within(0.5).of(28.1) } + it { is_expected.not_to be_within(0.5).of(26.9) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to be_within(0.5).of(28) } + xit { is_expected.not_to be_within(0.5).of(27) } + xit { is_expected.to be_within(0.5).of(28.1) } + xit { is_expected.to be_within(0.5).of(26.9) } + end + end +end diff --git a/spec/rspec/expectations/change_matcher_spec.cr b/spec/rspec/expectations/change_matcher_spec.cr new file mode 100644 index 0000000..c59423e --- /dev/null +++ b/spec/rspec/expectations/change_matcher_spec.cr @@ -0,0 +1,49 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/change-matcher +# and modified to fit Spectator and Crystal. +Spectator.describe "`change` matcher" do + # Modified this example type to work in Crystal. + module Counter + extend self + + @@count = 0 + + def increment + @@count += 1 + end + + def count + @@count + end + end + + context "expect change" do + describe "Counter#increment" do # TODO: Allow multiple arguments to context/describe. + it "should increment the count" do + expect { Counter.increment }.to change { Counter.count }.from(0).to(1) + end + + # deliberate failure + # TODO: Add support for expected failures. + xit "should increment the count by 2" do + expect { Counter.increment }.to change { Counter.count }.by(2) + end + end + end + + context "expect no change" do + describe "Counter#increment" do # TODO: Allow multiple arguments to context/describe. + # deliberate failures + # TODO: Add support for expected failures. + xit "should not increment the count by 1 (using not_to)" do + expect { Counter.increment }.not_to change { Counter.count } + end + + xit "should not increment the count by 1 (using to_not)" do + expect { Counter.increment }.to_not change { Counter.count } + end + end + end +end diff --git a/spec/rspec/expectations/comparison_matchers_spec.cr b/spec/rspec/expectations/comparison_matchers_spec.cr new file mode 100644 index 0000000..aef4194 --- /dev/null +++ b/spec/rspec/expectations/comparison_matchers_spec.cr @@ -0,0 +1,47 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/comparison-matchers +# and modified to fit Spectator and Crystal. +Spectator.describe "Comparison matchers" do + context "numeric operator matchers" do + describe 18 do + it { is_expected.to be < 20 } + it { is_expected.to be > 15 } + it { is_expected.to be <= 19 } + it { is_expected.to be >= 17 } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.to be < 15 } + xit { is_expected.to be > 20 } + xit { is_expected.to be <= 17 } + xit { is_expected.to be >= 19 } + # it { is_expected.to be < 'a' } # Removed because Crystal doesn't support Int32#<(Char) + end + + describe 'a' do + it { is_expected.to be < 'b' } + + # deliberate failures + # TODO: Add support for expected failures. + # it { is_expected.to be < 18 } # Removed because Crystal doesn't support Char#<(Int32) + end + end + + context "string operator matchers" do + describe "Strawberry" do + it { is_expected.to be < "Tomato" } + it { is_expected.to be > "Apple" } + it { is_expected.to be <= "Turnip" } + it { is_expected.to be >= "Banana" } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.to be < "Cranberry" } + xit { is_expected.to be > "Zuchini" } + xit { is_expected.to be <= "Potato" } + xit { is_expected.to be >= "Tomato" } + end + end +end diff --git a/spec/rspec/expectations/contain_exactly_matcher_spec.cr b/spec/rspec/expectations/contain_exactly_matcher_spec.cr new file mode 100644 index 0000000..14d3c33 --- /dev/null +++ b/spec/rspec/expectations/contain_exactly_matcher_spec.cr @@ -0,0 +1,32 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/contain-exactly-matcher +# and modified to fit Spectator and Crystal. +Spectator.describe "`contain_exactly` matcher" do + context "Array is expected to contain every value" do + describe [1, 2, 3] do + it { is_expected.to contain_exactly(1, 2, 3) } + it { is_expected.to contain_exactly(1, 3, 2) } + it { is_expected.to contain_exactly(2, 1, 3) } + it { is_expected.to contain_exactly(2, 3, 1) } + it { is_expected.to contain_exactly(3, 1, 2) } + it { is_expected.to contain_exactly(3, 2, 1) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.to contain_exactly(1, 2, 1) } + end + end + + context "Array is not expected to contain every value" do + describe [1, 2, 3] do + it { is_expected.to_not contain_exactly(1, 2, 3, 4) } + it { is_expected.to_not contain_exactly(1, 2) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.to_not contain_exactly(1, 3, 2) } + end + end +end diff --git a/spec/rspec/expectations/contain_matcher_spec.cr b/spec/rspec/expectations/contain_matcher_spec.cr new file mode 100644 index 0000000..a75897c --- /dev/null +++ b/spec/rspec/expectations/contain_matcher_spec.cr @@ -0,0 +1,99 @@ +require "../../spec_helper" + +# In Ruby, this is the `include` matcher. +# However, `include` is a reserved keyword in Crystal. +# So instead, it is `contain` in Spectator. +Spectator.describe "`contain` matcher" do + context "array usage" do + describe [1, 3, 7] do + it { is_expected.to contain(1) } + it { is_expected.to contain(3) } + it { is_expected.to contain(7) } + it { is_expected.to contain(1, 7) } + it { is_expected.to contain(1, 3, 7) } + + # Utility matcher method `a_kind_of` is not supported. + # it { is_expected.to contain(a_kind_of(Int)) } + + # TODO: Compound matchers aren't supported. + # it { is_expected.to contain(be_odd.and be < 10) } + + # TODO: Fix behavior and cleanup output. + # This syntax is allowed, but produces a wrong result and bad output. + xit { is_expected.to contain(be_odd) } + xit { is_expected.not_to contain(be_even) } + + it { is_expected.not_to contain(17) } + it { is_expected.not_to contain(43, 100) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.to contain(4) } + xit { is_expected.to contain(be_even) } + xit { is_expected.not_to contain(1) } + xit { is_expected.not_to contain(3) } + xit { is_expected.not_to contain(7) } + xit { is_expected.not_to contain(1, 3, 7) } + + # both of these should fail since it contains 1 but not 9 + xit { is_expected.to contain(1, 9) } + xit { is_expected.not_to contain(1, 9) } + end + end + + context "string usage" do + describe "a string" do + it { is_expected.to contain("str") } + it { is_expected.to contain("a", "str", "ng") } + it { is_expected.not_to contain("foo") } + it { is_expected.not_to contain("foo", "bar") } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.to contain("foo") } + xit { is_expected.not_to contain("str") } + xit { is_expected.to contain("str", "foo") } + xit { is_expected.not_to contain("str", "foo") } + end + end + + context "hash usage" do + # A hash can't be described inline here for some reason. + # So it is placed in the subject instead. + describe ":a => 7, :b => 5" do + subject { {:a => 7, :b => 5} } + + # Hash syntax is changed here from `:a => 7` to `a: 7`. + xit { is_expected.to contain(:a) } + xit { is_expected.to contain(:b, :a) } + + # TODO: This hash-like syntax isn't supported. + # it { is_expected.to contain(a: 7) } + # it { is_expected.to contain(b: 5, a: 7) } + xit { is_expected.not_to contain(:c) } + xit { is_expected.not_to contain(:c, :d) } + # it { is_expected.not_to contain(d: 2) } + # it { is_expected.not_to contain(a: 5) } + # it { is_expected.not_to contain(b: 7, a: 5) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to contain(:a) } + xit { is_expected.not_to contain(:b, :a) } + # it { is_expected.not_to contain(a: 7) } + # it { is_expected.not_to contain(a: 7, b: 5) } + xit { is_expected.to contain(:c) } + xit { is_expected.to contain(:c, :d) } + # it { is_expected.to contain(d: 2) } + # it { is_expected.to contain(a: 5) } + # it { is_expected.to contain(a: 5, b: 7) } + + # Mixed cases--the hash contains one but not the other. + # All 4 of these cases should fail. + xit { is_expected.to contain(:a, :d) } + xit { is_expected.not_to contain(:a, :d) } + # it { is_expected.to contain(a: 7, d: 3) } + # it { is_expected.not_to contain(a: 7, d: 3) } + end + end +end diff --git a/spec/rspec/expectations/cover_matcher_spec.cr b/spec/rspec/expectations/cover_matcher_spec.cr new file mode 100644 index 0000000..fb1a74a --- /dev/null +++ b/spec/rspec/expectations/cover_matcher_spec.cr @@ -0,0 +1,30 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/cover-matcher +# and modified to fit Spectator and Crystal. +Spectator.describe "`cover` matcher" do + context "range usage" do + describe (1..10) do + it { is_expected.to cover(4) } + it { is_expected.to cover(6) } + it { is_expected.to cover(8) } + it { is_expected.to cover(4, 6) } + it { is_expected.to cover(4, 6, 8) } + it { is_expected.not_to cover(11) } + it { is_expected.not_to cover(11, 12) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.to cover(11) } + xit { is_expected.not_to cover(4) } + xit { is_expected.not_to cover(6) } + xit { is_expected.not_to cover(8) } + xit { is_expected.not_to cover(4, 6, 8) } + + # both of these should fail since it covers 5 but not 11 + xit { is_expected.to cover(5, 11) } + xit { is_expected.not_to cover(5, 11) } + end + end +end diff --git a/spec/rspec/expectations/end_with_matcher_spec.cr b/spec/rspec/expectations/end_with_matcher_spec.cr new file mode 100644 index 0000000..c19720f --- /dev/null +++ b/spec/rspec/expectations/end_with_matcher_spec.cr @@ -0,0 +1,33 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/end-with-matcher +# and modified to fit Spectator and Crystal. +Spectator.describe "`end_with` matcher" do + context "string usage" do + describe "this string" do + it { is_expected.to end_with "string" } + it { is_expected.not_to end_with "stringy" } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to end_with "string" } + xit { is_expected.to end_with "stringy" } + end + end + + context "array usage" do + describe [0, 1, 2, 3, 4] do + it { is_expected.to end_with 4 } + # TODO: Add support for multiple items at the end of an array. + # it { is_expected.to end_with 3, 4 } + it { is_expected.not_to end_with 3 } + # it { is_expected.not_to end_with 0, 1, 2, 3, 4, 5 } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to end_with 4 } + xit { is_expected.to end_with 3 } + end + end +end diff --git a/spec/rspec/expectations/equality_matchers_spec.cr b/spec/rspec/expectations/equality_matchers_spec.cr new file mode 100644 index 0000000..9aa923c --- /dev/null +++ b/spec/rspec/expectations/equality_matchers_spec.cr @@ -0,0 +1,64 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/equality-matchers +# and modified to fit Spectator and Crystal. +Spectator.describe "Equality matchers" do + context "compare using eq (==)" do + describe "a string" do + it "is equal to another string of the same value" do + expect("this string").to eq("this string") + end + + it "is not equal to another string of a different value" do + expect("this string").not_to eq("a different string") + end + end + + describe "an integer" do + it "is equal to a float for the same value" do + expect(5).to eq(5.0) + end + end + end + + context "compare using ==" do + describe "a string" do + it "is equal to another string of the same value" do + expect("this string").to be == "this string" + end + + it "is not equal to another string of a different value" do + expect("this string").not_to be == "a different string" + end + end + + describe "an integer" do + it "is equal to a float of the same value" do + expect(5).to be == 5.0 + end + end + end + + # There are no #eql? and #equal? methods in Crystal, so these tests are skipped. + + context "compare using be (same?)" do + it "is equal to itself" do + string = "this string" + expect(string).to be(string) + end + + it "is not equal to another reference of the same value" do + # Strings with identical contents are the same reference in Crystal. + # This test is modified to reflect that. + # expect("this string").not_to be("this string") + box1 = Box.new("this string") + box2 = Box.new("this string") + expect(box1).not_to be(box2) + end + + it "is not equal to another string of a different value" do + expect("this string").not_to be("a different string") + end + end +end diff --git a/spec/rspec/expectations/have_attributes_matcher_spec.cr b/spec/rspec/expectations/have_attributes_matcher_spec.cr new file mode 100644 index 0000000..dfa273b --- /dev/null +++ b/spec/rspec/expectations/have_attributes_matcher_spec.cr @@ -0,0 +1,37 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/have-attributes-matcher +# and modified to fit Spectator and Crystal. +Spectator.describe "`have_attributes` matcher" do + context "basic usage" do + # Use `record` instead of `Struct.new`. + record Person, name : String, age : Int32 + + describe Person.new("Jim", 32) do + # Changed some syntax for Ruby hashes to Crystal named tuples. + + # Spectator doesn't support helper matchers like `a_string_starting_with` and `a_value <`. + # But maybe in the future it will. + it { is_expected.to have_attributes(name: "Jim") } + # it { is_expected.to have_attributes(name: a_string_starting_with("J") ) } + it { is_expected.to have_attributes(age: 32) } + # it { is_expected.to have_attributes(age: (a_value > 30) ) } + it { is_expected.to have_attributes(name: "Jim", age: 32) } + # it { is_expected.to have_attributes(name: a_string_starting_with("J"), age: (a_value > 30) ) } + it { is_expected.not_to have_attributes(name: "Bob") } + it { is_expected.not_to have_attributes(age: 10) } + # it { is_expected.not_to have_attributes(age: (a_value < 30) ) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.to have_attributes(name: "Bob") } + xit { is_expected.to have_attributes(name: 10) } + + # fails if any of the attributes don't match + xit { is_expected.to have_attributes(name: "Bob", age: 32) } + xit { is_expected.to have_attributes(name: "Jim", age: 10) } + xit { is_expected.to have_attributes(name: "Bob", age: 10) } + end + end +end diff --git a/spec/rspec/expectations/match_matcher_spec.cr b/spec/rspec/expectations/match_matcher_spec.cr new file mode 100644 index 0000000..28b4f72 --- /dev/null +++ b/spec/rspec/expectations/match_matcher_spec.cr @@ -0,0 +1,30 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/match-matcher +# and modified to fit Spectator and Crystal. +Spectator.describe "`match` matcher" do + context "string usage" do + describe "a string" do + it { is_expected.to match(/str/) } + it { is_expected.not_to match(/foo/) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to match(/str/) } + xit { is_expected.to match(/foo/) } + end + end + + context "regular expression usage" do + describe /foo/ do + it { is_expected.to match("food") } + it { is_expected.not_to match("drinks") } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to match("food") } + xit { is_expected.to match("drinks") } + end + end +end diff --git a/spec/rspec/expectations/predicate_matchers_spec.cr b/spec/rspec/expectations/predicate_matchers_spec.cr new file mode 100644 index 0000000..f2dac5c --- /dev/null +++ b/spec/rspec/expectations/predicate_matchers_spec.cr @@ -0,0 +1,86 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/predicate-matchers +# and modified to fit Spectator and Crystal. +Spectator.describe "Predicate matchers" do + context "should be_zero (based on Int#zero?)" do + describe 0 do + it { is_expected.to be_zero } + end + + describe 7 do + # deliberate failure + # TODO: Add support for expected failures. + xit { is_expected.to be_zero } + end + end + + context "should_not be_empty (based on Array#empty?)" do + describe [1, 2, 3] do + it { is_expected.not_to be_empty } + end + + describe [] of Int32 do + # deliberate failure + # TODO: Add support for expected failures. + xit { is_expected.not_to be_empty } + end + end + + context "should have_key (based on Hash#has_key?)" do + describe Hash do + subject { {:foo => 7} } + + it { is_expected.to have_key(:foo) } + + # deliberate failure + # TODO: Add support for expected failures. + xit { is_expected.to have_key(:bar) } + end + end + + context "should_not have_all_string_keys (based on custom #has_all_string_keys? method)" do + class ::Hash(K, V) + def has_all_string_keys? + keys.all? { |k| String === k } + end + end + + describe Hash do + context "with symbol keys" do + subject { {:foo => 7, :bar => 5} } + + it { is_expected.not_to have_all_string_keys } + end + + context "with string keys" do + subject { {"foo" => 7, "bar" => 5} } + + # deliberate failure + # TODO: Add support for expected failures. + xit { is_expected.not_to have_all_string_keys } + end + end + end + + context "matcher arguments are passed on to the predicate method" do + struct ::Int + def multiple_of?(x) + (self % x).zero? + end + end + + describe 12 do + it { is_expected.to be_multiple_of(3) } + it { is_expected.not_to be_multiple_of(7) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to be_multiple_of(4) } + xit { is_expected.to be_multiple_of(5) } + end + end + + # The examples using private methods cause a compilation error in Crystal, and can't be used here. +end diff --git a/spec/rspec/expectations/start_with_matcher_spec.cr b/spec/rspec/expectations/start_with_matcher_spec.cr new file mode 100644 index 0000000..c2942cc --- /dev/null +++ b/spec/rspec/expectations/start_with_matcher_spec.cr @@ -0,0 +1,33 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/start-with-matcher +# and modified to fit Spectator and Crystal. +Spectator.describe "`start_with` matcher" do + context "with a string" do + describe "this string" do + it { is_expected.to start_with "this" } + it { is_expected.not_to start_with "that" } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to start_with "this" } + xit { is_expected.to start_with "that" } + end + end + + context "with an array" do + describe [0, 1, 2, 3, 4] do + it { is_expected.to start_with 0 } + # TODO: Add support for multiple items at the beginning of an array. + # it { is_expected.to start_with(0, 1) } + it { is_expected.not_to start_with(2) } + # it { is_expected.not_to start_with(0, 1, 2, 3, 4, 5) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to start_with 0 } + xit { is_expected.to start_with 3 } + end + end +end diff --git a/spec/rspec/expectations/type_matchers_spec.cr b/spec/rspec/expectations/type_matchers_spec.cr new file mode 100644 index 0000000..c2b1a2d --- /dev/null +++ b/spec/rspec/expectations/type_matchers_spec.cr @@ -0,0 +1,101 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/type-matchers +# and modified to fit Spectator and Crystal. +Spectator.describe "Type matchers" do + context "be_(a_)kind_of matcher" do + # The docs use Float as an example. + # This doesn't work with the Crystal compiler, + # so a custom hierarchy is used instead. + # "Error: can't use Number as generic type argument yet, use a more specific type" + + module MyModule; end + + class Base; end + + class Derived < Base + include MyModule + end + + describe Derived do + # the actual class + it { is_expected.to be_kind_of(Derived) } + it { is_expected.to be_a_kind_of(Derived) } + it { is_expected.to be_a(Derived) } + + # the superclass + it { is_expected.to be_kind_of(Base) } + it { is_expected.to be_a_kind_of(Base) } + it { is_expected.to be_an(Base) } + + # an included module + it { is_expected.to be_kind_of(MyModule) } + it { is_expected.to be_a_kind_of(MyModule) } + it { is_expected.to be_a(MyModule) } + + # negative passing case + it { is_expected.not_to be_kind_of(String) } + it { is_expected.not_to be_a_kind_of(String) } + it { is_expected.not_to be_a(String) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to be_kind_of(Derived) } + xit { is_expected.not_to be_a_kind_of(Derived) } + xit { is_expected.not_to be_a(Derived) } + xit { is_expected.not_to be_kind_of(Base) } + xit { is_expected.not_to be_a_kind_of(Base) } + xit { is_expected.not_to be_an(Base) } + xit { is_expected.not_to be_kind_of(MyModule) } + xit { is_expected.not_to be_a_kind_of(MyModule) } + xit { is_expected.not_to be_a(MyModule) } + xit { is_expected.to be_kind_of(String) } + xit { is_expected.to be_a_kind_of(String) } + xit { is_expected.to be_a(String) } + end + + context "be_(an_)instance_of matcher" do + # The docs use Float as an example. + # This doesn't work with the Crystal compiler, + # so a custom hierarchy is used instead. + # "Error: can't use Number as generic type argument yet, use a more specific type" + + module MyModule; end + + class Base; end + + class Derived < Base + include MyModule + end + + describe Derived do + # the actual class + it { is_expected.to be_instance_of(Derived) } + it { is_expected.to be_an_instance_of(Derived) } + + # the superclass + it { is_expected.not_to be_instance_of(Base) } + it { is_expected.not_to be_an_instance_of(Base) } + + # an included module + it { is_expected.not_to be_instance_of(MyModule) } + it { is_expected.not_to be_an_instance_of(MyModule) } + + # another class with no relation to the subject's hierarchy + it { is_expected.not_to be_instance_of(String) } + it { is_expected.not_to be_an_instance_of(String) } + + # deliberate failures + xit { is_expected.not_to be_instance_of(Derived) } + xit { is_expected.not_to be_an_instance_of(Derived) } + xit { is_expected.to be_instance_of(Base) } + xit { is_expected.to be_an_instance_of(Base) } + xit { is_expected.to be_instance_of(MyModule) } + xit { is_expected.to be_an_instance_of(MyModule) } + xit { is_expected.to be_instance_of(String) } + xit { is_expected.to be_an_instance_of(String) } + end + end + end +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 30b0ee7..bca1453 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -1,4 +1 @@ require "../src/spectator" - -# Prevent Spectator from trying to run tests on its own. -Spectator.autorun = false diff --git a/src/spectator.cr b/src/spectator.cr index 1d43e65..273ef10 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -6,7 +6,7 @@ module Spectator extend self # Current version of the Spectator library. - VERSION = "0.9.1" + VERSION = "0.9.2" # Top-level describe method. # All specs in a file must be wrapped in this call. diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 041b1a8..b356ea5 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -3,7 +3,7 @@ require "../spec_builder" module Spectator module DSL - macro it(description, _source_file = __FILE__, _source_line = __LINE__, &block) + macro it(description = nil, _source_file = __FILE__, _source_line = __LINE__, &block) {% if block.is_a?(Nop) %} {% if description.is_a?(Call) %} def %run @@ -20,17 +20,17 @@ module Spectator %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) ::Spectator::SpecBuilder.add_example( - {{description.is_a?(StringLiteral) ? description : description.stringify}}, + {{description.is_a?(StringLiteral) || description.is_a?(NilLiteral) ? description : description.stringify}}, %source, {{@type.name}} ) { |test| test.as({{@type.name}}).%run } end - macro specify(description, &block) + macro specify(description = nil, &block) it({{description}}) {{block}} end - macro pending(description, _source_file = __FILE__, _source_line = __LINE__, &block) + macro pending(description = nil, _source_file = __FILE__, _source_line = __LINE__, &block) {% if block.is_a?(Nop) %} {% if description.is_a?(Call) %} def %run @@ -47,17 +47,17 @@ module Spectator %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) ::Spectator::SpecBuilder.add_pending_example( - {{description.is_a?(StringLiteral) ? description : description.stringify}}, + {{description.is_a?(StringLiteral) || description.is_a?(NilLiteral) ? description : description.stringify}}, %source, {{@type.name}} ) { |test| test.as({{@type.name}}).%run } end - macro skip(description, &block) + macro skip(description = nil, &block) pending({{description}}) {{block}} end - macro xit(description, &block) + macro xit(description = nil, &block) pending({{description}}) {{block}} end end diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 8153b0e..bce9215 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -19,7 +19,7 @@ module Spectator %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) ::Spectator::SpecBuilder.start_group({{description}}, %source) - {% if what.is_a?(Path) || what.is_a?(Generic) %} + {% if (what.is_a?(Path) || what.is_a?(Generic)) && what.resolve? %} macro described_class {{what}} end @@ -27,6 +27,10 @@ module Spectator def subject(*args) described_class.new(*args) end + {% else %} + def subject + {{what}} + end {% end %} {{block.body}} diff --git a/src/spectator/dsl/matchers.cr b/src/spectator/dsl/matchers.cr index 0e779c3..2108a88 100644 --- a/src/spectator/dsl/matchers.cr +++ b/src/spectator/dsl/matchers.cr @@ -94,6 +94,60 @@ module Spectator be_a({{expected}}) end + # Indicates that some value should be of a specified type. + # The `Object#is_a?` method is used for this check. + # A type name or type union should be used for *expected*. + # This method is identical to `#be_a`, + # and exists just to improve grammar. + # + # Examples: + # ``` + # expect(123).to be_kind_of(Int) + # ``` + macro be_kind_of(expected) + be_a({{expected}}) + end + + # Indicates that some value should be of a specified type. + # The `Object#is_a?` method is used for this check. + # A type name or type union should be used for *expected*. + # This method is identical to `#be_a`, + # and exists just to improve grammar. + # + # Examples: + # ``` + # expect(123).to be_a_kind_of(Int) + # ``` + macro be_a_kind_of(expected) + be_a({{expected}}) + end + + # Indicates that some value should be of a specified type. + # The value's runtime class is checked. + # A type name or type union should be used for *expected*. + # + # Examples: + # ``` + # expect(123).to be_instance_of(Int32) + # ``` + macro be_instance_of(expected) + ::Spectator::Matchers::InstanceMatcher({{expected}}).new + end + + # Indicates that some value should be of a specified type. + # The value's runtime class is checked. + # A type name or type union should be used for *expected*. + # This method is identical to `#be_an_instance_of`, + # and exists just to improve grammar. + # + # Examples: + # ``` + # expect(123).to be_an_instance_of(Int32) + # ``` + macro be_an_instance_of(expected) + be_instance_of({{expected}}) + end + # Indicates that some value should respond to a method call. # One or more method names can be provided. # @@ -400,6 +454,27 @@ module Spectator ::Spectator::Matchers::ContainMatcher.new(%test_value) end + # Indicates that some range (or collection) should contain another value. + # This is typically used on a `Range` (although any `Enumerable` works). + # The `includes?` method is used. + # + # Examples: + # ``` + # expect(1..10).to contain(5) + # expect((1..)).to contain(100) + # expect(..100).to contain(50) + # ``` + # + # Additionally, multiple arguments can be specified. + # ``` + # expect(1..10).to contain(2, 3) + # expect(..100).to contain(0, 50) + # ``` + macro cover(*expected) + %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.splat.stringify}}) + ::Spectator::Matchers::ContainMatcher.new(%test_value) + end + # Indicates that some value or set should contain another value. # This is similar to `#contain`, but uses a different method for matching. # Typically a `String` or `Array` (any `Enumerable` works) is checked against. @@ -466,22 +541,23 @@ module Spectator have_value({{expected}}) end - # Indicates that some set should contain some values in exact order. + # Indicates that some set should contain some values in any order. # # Example: # ``` - # expect([1, 2, 3]).to contain_exactly(1, 2, 3) + # expect([1, 2, 3]).to contain_exactly(3, 2, 1) # ``` macro contain_exactly(*expected) %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) ::Spectator::Matchers::ArrayMatcher.new(%test_value) end - # Indicates that some set should contain the same values in exact order as another set. + # Indicates that some set should contain the same values in any order as another set. + # This is the same as `#contain_exactly`, but takes an array as an argument. # # Example: # ``` - # expect([1, 2, 3]).to match_array([1, 2, 3]) + # expect([1, 2, 3]).to match_array([3, 2, 1]) # ``` macro match_array(expected) %test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}}) @@ -716,8 +792,8 @@ module Spectator {% raise "Undefined local variable or method '#{call}'" %} {% end %} - descriptor = { {{method_name}}: Tuple.new({{call.args.splat}}) } - label = String::Builder.new({{method_name.stringify}}) + descriptor = { {{method_name}}: ::Tuple.new({{call.args.splat}}) } + label = ::String::Builder.new({{method_name.stringify}}) {% unless call.args.empty? %} label << '(' {% for arg, index in call.args %} diff --git a/src/spectator/dsl/values.cr b/src/spectator/dsl/values.cr index ea9fdf5..6b1ba51 100644 --- a/src/spectator/dsl/values.cr +++ b/src/spectator/dsl/values.cr @@ -19,11 +19,18 @@ module Spectator end macro let!(name, &block) - # TODO: Doesn't work with late-defined values (let). - @%value = {{yield}} + @%wrapper : ::Spectator::ValueWrapper? + + def %wrapper + {{block.body}} + end + + before_each do + @%wrapper = ::Spectator::TypedValueWrapper.new(%wrapper) + end def {{name.id}} - @%value + @%wrapper.as(::Spectator::TypedValueWrapper(typeof(%wrapper))).value end end diff --git a/src/spectator/example.cr b/src/spectator/example.cr index e437bc4..cc955d1 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -6,6 +6,9 @@ module Spectator # Concrete types must implement the `#run_impl` method. abstract class Example < ExampleComponent @finished = false + @description : String? = nil + + protected setter description # Indicates whether the example has already been run. def finished? : Bool @@ -24,10 +27,12 @@ module Spectator end def description : String | Symbol - @test_wrapper.description + @description || @test_wrapper.description end def symbolic? : Bool + return false unless @test_wrapper.description? + description = @test_wrapper.description description.starts_with?('#') || description.starts_with?('.') end diff --git a/src/spectator/expectations/expectation.cr b/src/spectator/expectations/expectation.cr index 300f134..72d3dbd 100644 --- a/src/spectator/expectations/expectation.cr +++ b/src/spectator/expectations/expectation.cr @@ -44,6 +44,10 @@ module Spectator::Expectations values?.not_nil! end + def description + @match_data.description + end + # Creates the JSON representation of the expectation. def to_json(json : ::JSON::Builder) json.object do diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 33ed2f0..5268edc 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -46,6 +46,7 @@ module Spectator # Reports the outcome of an expectation. # An exception will be raised when a failing result is given. def report_expectation(expectation : Expectations::Expectation) : Nil + @example.description = expectation.description unless @example.test_wrapper.description? @reporter.report(expectation) end diff --git a/src/spectator/matchers/all_matcher.cr b/src/spectator/matchers/all_matcher.cr index bf6231d..d99ade4 100644 --- a/src/spectator/matchers/all_matcher.cr +++ b/src/spectator/matchers/all_matcher.cr @@ -26,7 +26,7 @@ module Spectator::Matchers match_data = matcher.match(element) break match_data unless match_data.matched? end - found || SuccessfulMatchData.new + found || SuccessfulMatchData.new(description) end # Negated matching for this matcher is not supported. diff --git a/src/spectator/matchers/array_matcher.cr b/src/spectator/matchers/array_matcher.cr index 314a31e..92fb01a 100644 --- a/src/spectator/matchers/array_matcher.cr +++ b/src/spectator/matchers/array_matcher.cr @@ -5,7 +5,7 @@ require "./unordered_array_matcher" module Spectator::Matchers # Matcher for checking that the contents of one array (or similar type) - # has the exact same contents as another and in the same order. + # has the exact same contents as another but may be in any order. struct ArrayMatcher(ExpectedType) < Matcher # Expected value and label. private getter expected @@ -25,15 +25,19 @@ module Spectator::Matchers def match(actual : TestExpression(T)) : MatchData forall T actual_elements = actual.value.to_a expected_elements = expected.value.to_a - index = compare_arrays(expected_elements, actual_elements) + missing, extra = compare_arrays(expected_elements, actual_elements) - case index - when Int # Content differs. - failed_content_mismatch(expected_elements, actual_elements, index, actual.label) - when true # Contents are identical. - SuccessfulMatchData.new - else # Size differs. - failed_size_mismatch(expected_elements, actual_elements, actual.label) + if missing.empty? && extra.empty? + # Contents are identical. + SuccessfulMatchData.new(description) + else + # Content differs. + FailedMatchData.new(description, "#{actual.label} does not contain exactly #{expected.label}", + expected: expected_elements.inspect, + actual: actual_elements.inspect, + missing: missing.empty? ? "None" : missing.inspect, + extra: extra.empty? ? "None" : extra.inspect + ) end end @@ -42,14 +46,17 @@ module Spectator::Matchers def negated_match(actual : TestExpression(T)) : MatchData forall T actual_elements = actual.value.to_a expected_elements = expected.value.to_a + missing, extra = compare_arrays(expected_elements, actual_elements) - case compare_arrays(expected_elements, actual_elements) - when Int # Contents differ. - SuccessfulMatchData.new - when true # Contents are identical. - failed_content_identical(expected_elements, actual_elements, actual.label) - else # Size differs. - SuccessfulMatchData.new + if missing.empty? && extra.empty? + # Contents are identical. + FailedMatchData.new(description, "#{actual.label} contains exactly #{expected.label}", + expected: "Not #{expected_elements.inspect}", + actual: actual_elements.inspect + ) + else + # Content differs. + SuccessfulMatchData.new(description) end end @@ -65,49 +72,41 @@ module Spectator::Matchers UnorderedArrayMatcher.new(expected) end - # Compares two arrays to determine whether they contain the same elements, and in the same order. - # If the arrays are the same, then `true` is returned. - # If they are different, `false` or an integer is returned. - # `false` is returned when the sizes of the arrays don't match. - # An integer is returned, that is the index of the mismatched elements in the arrays. + # Compares two arrays to determine whether they contain the same elements, but in any order. + # A tuple of two arrays is returned. + # The first array is the missing elements (present in expected, missing in actual). + # The second array array is the extra elements (not present in expected, present in actual). private def compare_arrays(expected_elements, actual_elements) - if expected_elements.size == actual_elements.size - index = 0 - expected_elements.zip(actual_elements) do |expected_element, actual_element| - return index unless expected_element == actual_element - index += 1 + # Produce hashes where the array elements are the keys, and the values are the number of occurances. + expected_hash = expected_elements.group_by(&.itself).map { |k, v| {k, v.size} }.to_h + actual_hash = actual_elements.group_by(&.itself).map { |k, v| {k, v.size} }.to_h + + { + hash_count_difference(expected_hash, actual_hash), + hash_count_difference(actual_hash, expected_hash), + } + end + + # Expects two hashes, with values as counts for keys. + # Produces an array of differences with elements repeated if needed. + private def hash_count_difference(first, second) + # Subtract the number of occurances from the other array. + # A duplicate hash is used here because the original can't be modified, + # since it there's a two-way comparison. + # + # Then reject elements that have zero (or less) occurances. + # Lastly, expand to the correct number of elements. + first.map do |element, count| + if second_count = second[element]? + {element, count - second_count} + else + {element, count} end - true - else - false - end - end - - # Produces match data for a failure when the array sizes differ. - private def failed_size_mismatch(expected_elements, actual_elements, actual_label) - FailedMatchData.new("#{actual_label} does not contain exactly #{expected.label} (size mismatch)", - expected: expected_elements.inspect, - actual: actual_elements.inspect, - "expected size": expected_elements.size.to_s, - "actual size": actual_elements.size.to_s - ) - end - - # Produces match data for a failure when the array content is mismatched. - private def failed_content_mismatch(expected_elements, actual_elements, index, actual_label) - FailedMatchData.new("#{actual_label} does not contain exactly #{expected.label} (element mismatch)", - expected: expected_elements[index].inspect, - actual: actual_elements[index].inspect, - index: index.to_s - ) - end - - # Produces match data for a failure when the arrays are identical, but they shouldn't be (negation). - private def failed_content_identical(expected_elements, actual_elements, actual_label) - FailedMatchData.new("#{actual_label} contains exactly #{expected.label}", - expected: "Not #{expected_elements.inspect}", - actual: actual_elements.inspect - ) + end.reject do |(_, count)| + count <= 0 + end.map do |(element, count)| + Array.new(count, element) + end.flatten end end end diff --git a/src/spectator/matchers/attributes_matcher.cr b/src/spectator/matchers/attributes_matcher.cr index 93cd62a..2a72d5b 100644 --- a/src/spectator/matchers/attributes_matcher.cr +++ b/src/spectator/matchers/attributes_matcher.cr @@ -28,9 +28,9 @@ module Spectator::Matchers def match(actual : TestExpression(T)) : MatchData forall T snapshot = snapshot_values(actual.value) if match?(snapshot) - SuccessfulMatchData.new + SuccessfulMatchData.new(description) else - FailedMatchData.new("#{actual.label} does not have attributes #{expected.label}", **values(snapshot)) + FailedMatchData.new(description, "#{actual.label} does not have attributes #{expected.label}", **values(snapshot)) end end @@ -39,9 +39,9 @@ module Spectator::Matchers def negated_match(actual : TestExpression(T)) : MatchData forall T snapshot = snapshot_values(actual.value) if match?(snapshot) - FailedMatchData.new("#{actual.label} has attributes #{expected.label}", **negated_values(snapshot)) + FailedMatchData.new(description, "#{actual.label} has attributes #{expected.label}", **negated_values(snapshot)) else - SuccessfulMatchData.new + SuccessfulMatchData.new(description) end end diff --git a/src/spectator/matchers/case_matcher.cr b/src/spectator/matchers/case_matcher.cr index e7f0891..fcb5c23 100644 --- a/src/spectator/matchers/case_matcher.cr +++ b/src/spectator/matchers/case_matcher.cr @@ -16,6 +16,12 @@ module Spectator::Matchers expected.value === actual.value end + # Overload that takes a regex so that the operands are flipped. + # This mimics RSpec's behavior. + private def match?(actual : TestExpression(Regex)) : Bool forall T + actual.value === expected.value + end + # Message displayed when the matcher isn't satisifed. # # This is only called when `#match?` returns false. diff --git a/src/spectator/matchers/change_exact_matcher.cr b/src/spectator/matchers/change_exact_matcher.cr index 7245386..6aa562d 100644 --- a/src/spectator/matchers/change_exact_matcher.cr +++ b/src/spectator/matchers/change_exact_matcher.cr @@ -30,21 +30,21 @@ module Spectator::Matchers before, after = change(actual) if expected_before == before if before == after - FailedMatchData.new("#{actual.label} did not change #{expression.label}", + FailedMatchData.new(description, "#{actual.label} did not change #{expression.label}", before: before.inspect, after: after.inspect ) elsif expected_after == after - SuccessfulMatchData.new + SuccessfulMatchData.new(description) else - FailedMatchData.new("#{actual.label} did not change #{expression.label} to #{expected_after.inspect}", + FailedMatchData.new(description, "#{actual.label} did not change #{expression.label} to #{expected_after.inspect}", before: before.inspect, after: after.inspect, expected: expected_after.inspect ) end else - FailedMatchData.new("#{expression.label} was not initially #{expected_before.inspect}", + FailedMatchData.new(description, "#{expression.label} was not initially #{expected_before.inspect}", expected: expected_before.inspect, actual: before.inspect, ) @@ -57,15 +57,15 @@ module Spectator::Matchers before, after = change(actual) if expected_before == before if expected_after == after - FailedMatchData.new("#{actual.label} changed #{expression.label} from #{expected_before.inspect} to #{expected_after.inspect}", + FailedMatchData.new(description, "#{actual.label} changed #{expression.label} from #{expected_before.inspect} to #{expected_after.inspect}", before: before.inspect, after: after.inspect ) else - SuccessfulMatchData.new + SuccessfulMatchData.new(description) end else - FailedMatchData.new("#{expression.label} was not initially #{expected_before.inspect}", + FailedMatchData.new(description, "#{expression.label} was not initially #{expected_before.inspect}", expected: expected_before.inspect, actual: before.inspect, ) diff --git a/src/spectator/matchers/change_from_matcher.cr b/src/spectator/matchers/change_from_matcher.cr index fa3504b..c0e08c1 100644 --- a/src/spectator/matchers/change_from_matcher.cr +++ b/src/spectator/matchers/change_from_matcher.cr @@ -27,18 +27,18 @@ module Spectator::Matchers def match(actual : TestExpression(T)) : MatchData forall T before, after = change(actual) if expected != before - FailedMatchData.new("#{expression.label} was not initially #{expected}", + FailedMatchData.new(description, "#{expression.label} was not initially #{expected}", expected: expected.inspect, actual: before.inspect, ) elsif before == after - FailedMatchData.new("#{actual.label} did not change #{expression.label} from #{expected}", + FailedMatchData.new(description, "#{actual.label} did not change #{expression.label} from #{expected}", before: before.inspect, after: after.inspect, expected: "Not #{expected.inspect}" ) else - SuccessfulMatchData.new + SuccessfulMatchData.new(description) end end @@ -47,14 +47,14 @@ module Spectator::Matchers def negated_match(actual : TestExpression(T)) : MatchData forall T before, after = change(actual) if expected != before - FailedMatchData.new("#{expression.label} was not initially #{expected}", + FailedMatchData.new(description, "#{expression.label} was not initially #{expected}", expected: expected.inspect, actual: before.inspect ) elsif before == after - SuccessfulMatchData.new + SuccessfulMatchData.new(description) else - FailedMatchData.new("#{actual.label} changed #{expression.label} from #{expected}", + FailedMatchData.new(description, "#{actual.label} changed #{expression.label} from #{expected}", before: before.inspect, after: after.inspect, expected: expected.inspect diff --git a/src/spectator/matchers/change_matcher.cr b/src/spectator/matchers/change_matcher.cr index 5de2415..a60891b 100644 --- a/src/spectator/matchers/change_matcher.cr +++ b/src/spectator/matchers/change_matcher.cr @@ -25,12 +25,12 @@ module Spectator::Matchers def match(actual : TestExpression(T)) : MatchData forall T before, after = change(actual) if before == after - FailedMatchData.new("#{actual.label} did not change #{expression.label}", + FailedMatchData.new(description, "#{actual.label} did not change #{expression.label}", before: before.inspect, after: after.inspect ) else - SuccessfulMatchData.new + SuccessfulMatchData.new(description) end end @@ -39,9 +39,9 @@ module Spectator::Matchers def negated_match(actual : TestExpression(T)) : MatchData forall T before, after = change(actual) if before == after - SuccessfulMatchData.new + SuccessfulMatchData.new(description) else - FailedMatchData.new("#{actual.label} changed #{expression.label}", + FailedMatchData.new(description, "#{actual.label} changed #{expression.label}", before: before.inspect, after: after.inspect ) diff --git a/src/spectator/matchers/change_relative_matcher.cr b/src/spectator/matchers/change_relative_matcher.cr index 2f170eb..4ac6e38 100644 --- a/src/spectator/matchers/change_relative_matcher.cr +++ b/src/spectator/matchers/change_relative_matcher.cr @@ -25,14 +25,14 @@ module Spectator::Matchers def match(actual : TestExpression(T)) : MatchData forall T before, after = change(actual) if before == after - FailedMatchData.new("#{actual.label} did not change #{expression.label}", + FailedMatchData.new(description, "#{actual.label} did not change #{expression.label}", before: before.inspect, after: after.inspect ) elsif @evaluator.call(before, after) - SuccessfulMatchData.new + SuccessfulMatchData.new(description) else - FailedMatchData.new("#{actual.label} did not change #{expression.label} #{@relativity}", + FailedMatchData.new(description, "#{actual.label} did not change #{expression.label} #{@relativity}", before: before.inspect, after: after.inspect ) diff --git a/src/spectator/matchers/change_to_matcher.cr b/src/spectator/matchers/change_to_matcher.cr index e29d23b..fb43204 100644 --- a/src/spectator/matchers/change_to_matcher.cr +++ b/src/spectator/matchers/change_to_matcher.cr @@ -27,15 +27,15 @@ module Spectator::Matchers def match(actual : TestExpression(T)) : MatchData forall T before, after = change(actual) if before == after - FailedMatchData.new("#{actual.label} did not change #{expression.label}", + FailedMatchData.new(description, "#{actual.label} did not change #{expression.label}", before: before.inspect, after: after.inspect, expected: expected.inspect ) elsif expected == after - SuccessfulMatchData.new + SuccessfulMatchData.new(description) else - FailedMatchData.new("#{actual.label} did not change #{expression.label} to #{expected}", + FailedMatchData.new(description, "#{actual.label} did not change #{expression.label} to #{expected}", before: before.inspect, after: after.inspect, expected: expected.inspect diff --git a/src/spectator/matchers/contain_matcher.cr b/src/spectator/matchers/contain_matcher.cr index 26d3287..c009851 100644 --- a/src/spectator/matchers/contain_matcher.cr +++ b/src/spectator/matchers/contain_matcher.cr @@ -18,6 +18,13 @@ module Spectator::Matchers end end + # If the expectation is negated, then this method is called instead of `#match?`. + private def does_not_match?(actual : TestExpression(T)) : Bool forall T + !expected.value.any? do |item| + actual.value.includes?(item) + end + end + # Message displayed when the matcher isn't satisifed. # # This is only called when `#match?` returns false. @@ -25,7 +32,7 @@ module Spectator::Matchers # The message should typically only contain the test expression labels. # Actual values should be returned by `#values`. private def failure_message(actual) : String - "#{actual.label} does not match #{expected.label}" + "#{actual.label} does not contain #{expected.label}" end # Message displayed when the matcher isn't satisifed and is negated. diff --git a/src/spectator/matchers/end_with_matcher.cr b/src/spectator/matchers/end_with_matcher.cr index cc28e9a..319c559 100644 --- a/src/spectator/matchers/end_with_matcher.cr +++ b/src/spectator/matchers/end_with_matcher.cr @@ -23,7 +23,8 @@ module Spectator::Matchers # Actually performs the test against the expression. def match(actual : TestExpression(T)) : MatchData forall T - if (value = actual.value).responds_to?(:ends_with?) + value = actual.value + if value.is_a?(String) || value.responds_to?(:ends_with?) match_ends_with(value, actual.label) else match_last(value, actual.label) @@ -33,10 +34,11 @@ module Spectator::Matchers # Performs the test against the expression, but inverted. # A successful match with `#match` should normally fail for this method, and vice-versa. def negated_match(actual : TestExpression(T)) : MatchData forall T - if actual.value.responds_to?(:ends_with?) - negated_match_ends_with(actual) + value = actual.value + if value.is_a?(String) || value.responds_to?(:ends_with?) + negated_match_ends_with(value, actual.label) else - negated_match_last(actual) + negated_match_last(value, actual.label) end end @@ -44,9 +46,9 @@ module Spectator::Matchers # This method expects (and uses) the `#ends_with?` method on the value. private def match_ends_with(actual_value, actual_label) if actual_value.ends_with?(expected.value) - SuccessfulMatchData.new + SuccessfulMatchData.new(description) else - FailedMatchData.new("#{actual_label} does not end with #{expected.label} (using #ends_with?)", + FailedMatchData.new(description, "#{actual_label} does not end with #{expected.label} (using #ends_with?)", expected: expected.value.inspect, actual: actual_value.inspect ) @@ -60,9 +62,9 @@ module Spectator::Matchers last = list.last if expected.value === last - SuccessfulMatchData.new + SuccessfulMatchData.new(description) else - FailedMatchData.new("#{actual_label} does not end with #{expected.label} (using expected === last)", + FailedMatchData.new(description, "#{actual_label} does not end with #{expected.label} (using expected === last)", expected: expected.value.inspect, actual: last.inspect, list: list.inspect @@ -72,31 +74,31 @@ module Spectator::Matchers # Checks whether the actual value does not end with the expected value. # This method expects (and uses) the `#ends_with?` method on the value. - private def negated_match_ends_with(actual) - if actual.value.ends_with?(expected.value) - FailedMatchData.new("#{actual.label} ends with #{expected.label} (using #ends_with?)", - expected: expected.value.inspect, - actual: actual.value.inspect + private def negated_match_ends_with(actual_value, actual_label) + if actual_value.ends_with?(expected.value) + FailedMatchData.new(description, "#{actual_label} ends with #{expected.label} (using #ends_with?)", + expected: "Not #{expected.value.inspect}", + actual: actual_value.inspect ) else - SuccessfulMatchData.new + SuccessfulMatchData.new(description) end end # Checks whether the last element of the value is not the expected value. # This method expects that the actual value is a set (enumerable). - private def negated_match_last(actual) - list = actual.value.to_a + private def negated_match_last(actual_value, actual_label) + list = actual_value.to_a last = list.last if expected.value === last - FailedMatchData.new("#{actual.label} ends with #{expected.label} (using expected === last)", - expected: expected.value.inspect, + FailedMatchData.new(description, "#{actual_label} ends with #{expected.label} (using expected === last)", + expected: "Not #{expected.value.inspect}", actual: last.inspect, list: list.inspect ) else - SuccessfulMatchData.new + SuccessfulMatchData.new(description) end end end diff --git a/src/spectator/matchers/exception_matcher.cr b/src/spectator/matchers/exception_matcher.cr index 6455df5..82d67ab 100644 --- a/src/spectator/matchers/exception_matcher.cr +++ b/src/spectator/matchers/exception_matcher.cr @@ -33,16 +33,16 @@ module Spectator::Matchers def match(actual : TestExpression(T)) : MatchData forall T exception = capture_exception { actual.value } if exception.nil? - FailedMatchData.new("#{actual.label} did not raise", expected: ExceptionType.inspect) + FailedMatchData.new(description, "#{actual.label} did not raise", expected: ExceptionType.inspect) else if exception.is_a?(ExceptionType) if (value = expected.value).nil? - SuccessfulMatchData.new + SuccessfulMatchData.new(description) else if value === exception.message - SuccessfulMatchData.new + SuccessfulMatchData.new(description) else - FailedMatchData.new("#{actual.label} raised #{exception.class}, but the message is not #{expected.label}", + FailedMatchData.new(description, "#{actual.label} raised #{exception.class}, but the message is not #{expected.label}", "expected type": ExceptionType.inspect, "actual type": exception.class.inspect, "expected message": value.inspect, @@ -51,7 +51,7 @@ module Spectator::Matchers end end else - FailedMatchData.new("#{actual.label} did not raise #{ExceptionType}", + FailedMatchData.new(description, "#{actual.label} did not raise #{ExceptionType}", expected: ExceptionType.inspect, actual: exception.class.inspect ) @@ -64,32 +64,37 @@ module Spectator::Matchers def negated_match(actual : TestExpression(T)) : MatchData forall T exception = capture_exception { actual.value } if exception.nil? - SuccessfulMatchData.new + SuccessfulMatchData.new(description) else if exception.is_a?(ExceptionType) if (value = expected.value).nil? - FailedMatchData.new("#{actual.label} raised #{exception.class}", + FailedMatchData.new(description, "#{actual.label} raised #{exception.class}", expected: "Not #{ExceptionType}", actual: exception.class.inspect ) else if value === exception.message - FailedMatchData.new("#{actual.label} raised #{exception.class} with message matching #{expected.label}", + FailedMatchData.new(description, "#{actual.label} raised #{exception.class} with message matching #{expected.label}", "expected type": ExceptionType.inspect, "actual type": exception.class.inspect, "expected message": value.inspect, "actual message": exception.message.to_s ) else - SuccessfulMatchData.new + SuccessfulMatchData.new(description) end end else - SuccessfulMatchData.new + SuccessfulMatchData.new(description) end end end + def with_message(message : T) forall T + value = TestValue.new(message) + ExceptionMatcher(ExceptionType, T).new(value) + end + # Runs a block of code and returns the exception it threw. # If no exception was thrown, *nil* is returned. private def capture_exception diff --git a/src/spectator/matchers/failed_match_data.cr b/src/spectator/matchers/failed_match_data.cr index 205b31c..537cf95 100644 --- a/src/spectator/matchers/failed_match_data.cr +++ b/src/spectator/matchers/failed_match_data.cr @@ -15,11 +15,13 @@ module Spectator::Matchers getter values : Array(Tuple(Symbol, String)) # Creates the match data. - def initialize(@failure_message, @values) + def initialize(description, @failure_message, @values) + super(description) end # Creates the match data. - def initialize(@failure_message, **values) + def initialize(description, @failure_message, **values) + super(description) @values = values.to_a end end diff --git a/src/spectator/matchers/have_predicate_matcher.cr b/src/spectator/matchers/have_predicate_matcher.cr index 93bcca6..7b23cd0 100644 --- a/src/spectator/matchers/have_predicate_matcher.cr +++ b/src/spectator/matchers/have_predicate_matcher.cr @@ -7,13 +7,6 @@ module Spectator::Matchers # Each key in the tuple is a predicate (without the '?' and 'has_' prefix) to test. # Each value is a a `Tuple` of arguments to pass to the predicate method. struct HavePredicateMatcher(ExpectedType) < ValueMatcher(ExpectedType) - # Expected value and label. - private getter expected - - # Creates the matcher with a expected values. - def initialize(@expected : TestValue(ExpectedType)) - end - # 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. @@ -25,9 +18,9 @@ module Spectator::Matchers def match(actual : TestExpression(T)) : MatchData forall T snapshot = snapshot_values(actual.value) if match?(snapshot) - SuccessfulMatchData.new + SuccessfulMatchData.new(description) else - FailedMatchData.new("#{actual.label} does not have #{expected.label}", **values(snapshot)) + FailedMatchData.new(description, "#{actual.label} does not have #{expected.label}", **values(snapshot)) end end @@ -36,9 +29,9 @@ module Spectator::Matchers def negated_match(actual : TestExpression(T)) : MatchData forall T snapshot = snapshot_values(actual.value) if match?(snapshot) - FailedMatchData.new("#{actual.label} has #{expected.label}", **values(snapshot)) + FailedMatchData.new(description, "#{actual.label} has #{expected.label}", **values(snapshot)) else - SuccessfulMatchData.new + SuccessfulMatchData.new(description) end end diff --git a/src/spectator/matchers/instance_matcher.cr b/src/spectator/matchers/instance_matcher.cr new file mode 100644 index 0000000..c77531e --- /dev/null +++ b/src/spectator/matchers/instance_matcher.cr @@ -0,0 +1,57 @@ +require "./matcher" + +module Spectator::Matchers + # Matcher that tests a value is of a specified type. + struct InstanceMatcher(Expected) < StandardMatcher + # 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 an instance of #{Expected}" + end + + # Checks whether the matcher is satisifed with the expression given to it. + private def match?(actual : TestExpression(T)) : Bool forall T + actual.value.class == Expected + end + + # Message displayed when the matcher isn't satisifed. + # + # This is only called when `#match?` returns false. + # + # The message should typically only contain the test expression labels. + # Actual values should be returned by `#values`. + private def failure_message(actual) : String + "#{actual.label} is not an instance of #{Expected}" + end + + # Message displayed when the matcher isn't satisifed and is negated. + # This is essentially what would satisfy the matcher if it wasn't negated. + # + # This is only called when `#does_not_match?` returns false. + # + # The message should typically only contain the test expression labels. + # Actual values should be returned by `#values`. + private def failure_message_when_negated(actual) : String + "#{actual.label} is an instance of #{Expected}" + end + + # Additional information about the match failure. + # The return value is a NamedTuple with Strings for each value. + private def values(actual) + { + expected: Expected.to_s, + actual: actual.value.class.inspect, + } + end + + # Additional information about the match failure when negated. + # The return value is a NamedTuple with Strings for each value. + private def negated_values(actual) + { + expected: "Not #{Expected}", + actual: actual.value.class.inspect, + } + end + end +end diff --git a/src/spectator/matchers/match_data.cr b/src/spectator/matchers/match_data.cr index 47b444c..31454f5 100644 --- a/src/spectator/matchers/match_data.cr +++ b/src/spectator/matchers/match_data.cr @@ -3,5 +3,10 @@ module Spectator::Matchers abstract struct MatchData # Indicates whether the match as successful or not. abstract def matched? : Bool + + getter description : String + + def initialize(@description : String) + end end end diff --git a/src/spectator/matchers/predicate_matcher.cr b/src/spectator/matchers/predicate_matcher.cr index 5c43c60..bacfac0 100644 --- a/src/spectator/matchers/predicate_matcher.cr +++ b/src/spectator/matchers/predicate_matcher.cr @@ -24,9 +24,9 @@ module Spectator::Matchers def match(actual : TestExpression(T)) : MatchData forall T snapshot = snapshot_values(actual.value) if match?(snapshot) - SuccessfulMatchData.new + SuccessfulMatchData.new(description) else - FailedMatchData.new("#{actual.label} is not #{expected.label}", **values(snapshot)) + FailedMatchData.new(description, "#{actual.label} is not #{expected.label}", **values(snapshot)) end end @@ -35,9 +35,9 @@ module Spectator::Matchers def negated_match(actual : TestExpression(T)) : MatchData forall T snapshot = snapshot_values(actual.value) if match?(snapshot) - FailedMatchData.new("#{actual.label} is #{expected.label}", **values(snapshot)) + FailedMatchData.new(description, "#{actual.label} is #{expected.label}", **values(snapshot)) else - SuccessfulMatchData.new + SuccessfulMatchData.new(description) end end diff --git a/src/spectator/matchers/respond_matcher.cr b/src/spectator/matchers/respond_matcher.cr index ce69a7e..3f26041 100644 --- a/src/spectator/matchers/respond_matcher.cr +++ b/src/spectator/matchers/respond_matcher.cr @@ -17,9 +17,9 @@ module Spectator::Matchers def match(actual : TestExpression(T)) : MatchData forall T snapshot = snapshot_values(actual.value) if match?(snapshot) - SuccessfulMatchData.new + SuccessfulMatchData.new(description) else - FailedMatchData.new("#{actual.label} does not respond to #{label}", **values(snapshot)) + FailedMatchData.new(description, "#{actual.label} does not respond to #{label}", **values(snapshot)) end end @@ -28,9 +28,9 @@ module Spectator::Matchers def negated_match(actual : TestExpression(T)) : MatchData forall T snapshot = snapshot_values(actual.value) if match?(snapshot) - FailedMatchData.new("#{actual.label} responds to #{label}", **values(snapshot)) + FailedMatchData.new(description, "#{actual.label} responds to #{label}", **values(snapshot)) else - SuccessfulMatchData.new + SuccessfulMatchData.new(description) end end diff --git a/src/spectator/matchers/standard_matcher.cr b/src/spectator/matchers/standard_matcher.cr index 2a01ab3..52cdc67 100644 --- a/src/spectator/matchers/standard_matcher.cr +++ b/src/spectator/matchers/standard_matcher.cr @@ -25,9 +25,9 @@ module Spectator::Matchers # Additionally, `#failure_message` and `#values` are called for a failed match. def match(actual : TestExpression(T)) : MatchData forall T if match?(actual) - SuccessfulMatchData.new + SuccessfulMatchData.new(description) else - FailedMatchData.new(failure_message(actual), values(actual).to_a) + FailedMatchData.new(description, failure_message(actual), values(actual).to_a) end end @@ -39,10 +39,11 @@ module Spectator::Matchers # Otherwise, a `FailedMatchData` instance is returned. # Additionally, `#failure_message_when_negated` and `#negated_values` are called for a failed match. def negated_match(actual : TestExpression(T)) : MatchData forall T + # TODO: Invert description. if does_not_match?(actual) - SuccessfulMatchData.new + SuccessfulMatchData.new(description) else - FailedMatchData.new(failure_message_when_negated(actual), negated_values(actual).to_a) + FailedMatchData.new(description, failure_message_when_negated(actual), negated_values(actual).to_a) end end diff --git a/src/spectator/matchers/start_with_matcher.cr b/src/spectator/matchers/start_with_matcher.cr index 29da717..b459bb4 100644 --- a/src/spectator/matchers/start_with_matcher.cr +++ b/src/spectator/matchers/start_with_matcher.cr @@ -22,7 +22,8 @@ module Spectator::Matchers # Actually performs the test against the expression. def match(actual : TestExpression(T)) : MatchData forall T - if (value = actual.value).responds_to?(:starts_with?) + value = actual.value + if value.is_a?(String) || value.responds_to?(:starts_with?) match_starts_with(value, actual.label) else match_first(value, actual.label) @@ -32,7 +33,8 @@ module Spectator::Matchers # Performs the test against the expression, but inverted. # A successful match with `#match` should normally fail for this method, and vice-versa. def negated_match(actual : TestExpression(T)) : MatchData forall T - if (value = actual.value).responds_to?(:starts_with?) + value = actual.value + if value.is_a?(String) || value.responds_to?(:starts_with?) negated_match_starts_with(value, actual.label) else negated_match_first(value, actual.label) @@ -43,9 +45,9 @@ module Spectator::Matchers # This method expects (and uses) the `#starts_with?` method on the value. private def match_starts_with(actual_value, actual_label) if actual_value.starts_with?(expected.value) - SuccessfulMatchData.new + SuccessfulMatchData.new(description) else - FailedMatchData.new("#{actual_label} does not start with #{expected.label} (using #starts_with?)", + FailedMatchData.new(description, "#{actual_label} does not start with #{expected.label} (using #starts_with?)", expected: expected.value.inspect, actual: actual_value.inspect ) @@ -59,9 +61,9 @@ module Spectator::Matchers first = list.first if expected.value === first - SuccessfulMatchData.new + SuccessfulMatchData.new(description) else - FailedMatchData.new("#{actual_label} does not start with #{expected.label} (using expected === first)", + FailedMatchData.new(description, "#{actual_label} does not start with #{expected.label} (using expected === first)", expected: expected.value.inspect, actual: first.inspect, list: list.inspect @@ -73,12 +75,12 @@ module Spectator::Matchers # This method expects (and uses) the `#starts_with?` method on the value. private def negated_match_starts_with(actual_value, actual_label) if actual_value.starts_with?(expected.value) - FailedMatchData.new("#{actual_label} starts with #{expected.label} (using #starts_with?)", - expected: expected.value.inspect, + FailedMatchData.new(description, "#{actual_label} starts with #{expected.label} (using #starts_with?)", + expected: "Not #{expected.value.inspect}", actual: actual_value.inspect ) else - SuccessfulMatchData.new + SuccessfulMatchData.new(description) end end @@ -89,13 +91,13 @@ module Spectator::Matchers first = list.first if expected.value === first - FailedMatchData.new("#{actual_label} starts with #{expected.label} (using expected === first)", - expected: expected.value.inspect, + FailedMatchData.new(description, "#{actual_label} starts with #{expected.label} (using expected === first)", + expected: "Not #{expected.value.inspect}", actual: first.inspect, list: list.inspect ) else - SuccessfulMatchData.new + SuccessfulMatchData.new(description) end end end diff --git a/src/spectator/matchers/type_matcher.cr b/src/spectator/matchers/type_matcher.cr index 4b7aff3..f8401d4 100644 --- a/src/spectator/matchers/type_matcher.cr +++ b/src/spectator/matchers/type_matcher.cr @@ -8,7 +8,7 @@ module Spectator::Matchers # This explains what condition satisfies the matcher. # The description is used when the one-liner syntax is used. def description : String - "is as #{Expected}" + "is a #{Expected}" end # Checks whether the matcher is satisifed with the expression given to it. diff --git a/src/spectator/matchers/unordered_array_matcher.cr b/src/spectator/matchers/unordered_array_matcher.cr index 5bbbdf1..81125a7 100644 --- a/src/spectator/matchers/unordered_array_matcher.cr +++ b/src/spectator/matchers/unordered_array_matcher.cr @@ -25,9 +25,9 @@ module Spectator::Matchers missing, extra = array_diff(expected_elements, actual_elements) if missing.empty? && extra.empty? - SuccessfulMatchData.new + SuccessfulMatchData.new(description) else - FailedMatchData.new("#{actual_label} does not contain #{expected.label} (unordered)", + FailedMatchData.new(description, "#{actual_label} does not contain #{expected.label} (unordered)", expected: expected_elements.inspect, actual: actual_elements.inspect, missing: missing.inspect, @@ -44,12 +44,12 @@ module Spectator::Matchers missing, extra = array_diff(expected_elements, actual_elements) if missing.empty? && extra.empty? - FailedMatchData.new("#{actual_label} contains #{expected.label} (unordered)", + FailedMatchData.new(description, "#{actual_label} contains #{expected.label} (unordered)", expected: "Not #{expected_elements.inspect}", actual: actual_elements.inspect, ) else - SuccessfulMatchData.new + SuccessfulMatchData.new(description) end end diff --git a/src/spectator/mocks/double.cr b/src/spectator/mocks/double.cr index 45071ac..ef17e84 100644 --- a/src/spectator/mocks/double.cr +++ b/src/spectator/mocks/double.cr @@ -17,13 +17,28 @@ module Spectator::Mocks named = false name = definition.name.id params = definition.args + + # Possibly a weird compiler bug, but syntax like this: + # stub instance.==(other) { true } + # Results in `other` being the call `other { true }`. + # This works around the issue by pulling out the block + # and setting the parameter to just the name. + if params.last.is_a?(Call) + body = params.last.block + params[-1] = params.last.name + end + args = params.map do |p| n = p.is_a?(TypeDeclaration) ? p.var : p.id r = named ? "#{n}: #{n}".id : n named = true if n.starts_with?('*') r end - body = definition.block.is_a?(Nop) ? block : definition.block + + # The unless is here because `||=` can't be used in macros @_@ + unless body + body = definition.block.is_a?(Nop) ? block : definition.block + end elsif definition.is_a?(TypeDeclaration) # stub foo : Symbol name = definition.var params = [] of MacroId diff --git a/src/spectator/mocks/stubs.cr b/src/spectator/mocks/stubs.cr index 374cc78..00677f8 100644 --- a/src/spectator/mocks/stubs.cr +++ b/src/spectator/mocks/stubs.cr @@ -12,13 +12,19 @@ module Spectator::Mocks named = false name = definition.name.id params = definition.args + if params.last.is_a?(Call) + body = params.last.block + params[-1] = params.last.name + end args = params.map do |p| n = p.is_a?(TypeDeclaration) ? p.var : p.id r = named ? "#{n}: #{n}".id : n named = true if n.starts_with?('*') r end - body = definition.block.is_a?(Nop) ? block : definition.block + unless body + body = definition.block.is_a?(Nop) ? block : definition.block + end elsif definition.is_a?(TypeDeclaration) # stub foo : Symbol name = definition.var params = [] of MacroId diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 9e199c2..39834be 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -42,7 +42,7 @@ module Spectator # Adds an example type to the current group. # The class name of the example should be passed as an argument. # The example will be instantiated later. - def add_example(description : String, source : Source, + def add_example(description : String?, source : Source, example_type : ::SpectatorTest.class, &runner : ::SpectatorTest ->) : Nil builder = ->(values : TestValues) { example_type.new(values).as(::SpectatorTest) } factory = RunnableExampleBuilder.new(description, source, builder, runner) @@ -52,7 +52,7 @@ module Spectator # Adds an example type to the current group. # The class name of the example should be passed as an argument. # The example will be instantiated later. - def add_pending_example(description : String, source : Source, + def add_pending_example(description : String?, source : Source, example_type : ::SpectatorTest.class, &runner : ::SpectatorTest ->) : Nil builder = ->(values : TestValues) { example_type.new(values).as(::SpectatorTest) } factory = PendingExampleBuilder.new(description, source, builder, runner) diff --git a/src/spectator/spec_builder/example_builder.cr b/src/spectator/spec_builder/example_builder.cr index 44f23fe..378c024 100644 --- a/src/spectator/spec_builder/example_builder.cr +++ b/src/spectator/spec_builder/example_builder.cr @@ -6,7 +6,7 @@ module Spectator::SpecBuilder abstract class ExampleBuilder alias FactoryMethod = TestValues -> ::SpectatorTest - def initialize(@description : String, @source : Source, @builder : FactoryMethod, @runner : TestMethod) + def initialize(@description : String?, @source : Source, @builder : FactoryMethod, @runner : TestMethod) end abstract def build(group) : ExampleComponent diff --git a/src/spectator/test_wrapper.cr b/src/spectator/test_wrapper.cr index 756f287..79d5265 100644 --- a/src/spectator/test_wrapper.cr +++ b/src/spectator/test_wrapper.cr @@ -8,13 +8,19 @@ module Spectator # Used to instantiate tests and run them. struct TestWrapper # Description the user provided for the test. - getter description + def description + @description || @source.to_s + end # Location of the test in source code. getter source # Creates a wrapper for the test. - def initialize(@description : String, @source : Source, @test : ::SpectatorTest, @runner : TestMethod) + def initialize(@description : String?, @source : Source, @test : ::SpectatorTest, @runner : TestMethod) + end + + def description? + !@description.nil? end def run