Merge branch 'have-matcher' into 'release/0.8'

Implement have predicate matcher

See merge request arctic-fox/spectator!6
This commit is contained in:
Mike Miller 2019-06-02 05:46:27 +00:00
commit bc4d0117a3
6 changed files with 200 additions and 29 deletions

View File

@ -302,8 +302,8 @@ Items not marked as completed may have partial implementations.
- [X] Error matchers - `raise_error`
- [ ] Yield matchers - `yield_control[.times]`, `yield_with_args[.times]`, `yield_with_no_args[.times]`, `yield_successive_args`
- [ ] Output matchers - `output[.to_stdout|.to_stderr]`
- [X] Predicate matchers - `be_x`, `have_x`
- [ ] Misc. matchers
- [ ] `exist`
- [X] `match`
- [ ] `satisfy`
- [ ] `change[.by|.from[.to]|.to|.by_at_least|.by_at_most]`

View File

@ -0,0 +1,87 @@
require "../spec_helper"
describe Spectator::Matchers::HavePredicateMatcher do
describe "#match" do
context "returned MatchData" do
describe "#match?" do
context "with a true predicate" do
it "is true" do
value = "foo\\bar"
partial = new_partial(value)
matcher = Spectator::Matchers::HavePredicateMatcher.new({back_references: Tuple.new}, "back_references")
match_data = matcher.match(partial)
match_data.matched?.should be_true
end
end
context "with a false predicate" do
it "is false" do
value = "foobar"
partial = new_partial(value)
matcher = Spectator::Matchers::HavePredicateMatcher.new({back_references: Tuple.new}, "back_references")
match_data = matcher.match(partial)
match_data.matched?.should be_false
end
end
end
describe "#values" do
it "contains a key for each expected attribute" do
value = "foobar"
partial = new_partial(value)
matcher = Spectator::Matchers::HavePredicateMatcher.new({back_references: Tuple.new}, "back_references")
match_data = matcher.match(partial)
match_data_has_key?(match_data.values, :back_references).should be_true
end
it "has the actual values" do
value = "foobar"
partial = new_partial(value)
matcher = Spectator::Matchers::HavePredicateMatcher.new({back_references: Tuple.new}, "back_references")
match_data = matcher.match(partial)
match_data_value_sans_prefix(match_data.values, :back_references)[:value].should eq(value.has_back_references?)
end
end
describe "#message" do
it "contains the actual label" do
value = "foobar"
label = "blah"
partial = new_partial(value, label)
matcher = Spectator::Matchers::HavePredicateMatcher.new({back_references: Tuple.new}, "back_references")
match_data = matcher.match(partial)
match_data.message.should contain(label)
end
it "contains the expected label" do
value = "foobar"
label = "blah"
partial = new_partial(value)
matcher = Spectator::Matchers::HavePredicateMatcher.new({back_references: Tuple.new}, label)
match_data = matcher.match(partial)
match_data.message.should contain(label)
end
end
describe "#negated_message" do
it "contains the actual label" do
value = "foobar"
label = "blah"
partial = new_partial(value, label)
matcher = Spectator::Matchers::HavePredicateMatcher.new({back_references: Tuple.new}, "back_references")
match_data = matcher.match(partial)
match_data.negated_message.should contain(label)
end
it "contains the expected label" do
value = "foobar"
label = "blah"
partial = new_partial(value)
matcher = Spectator::Matchers::HavePredicateMatcher.new({back_references: Tuple.new}, label)
match_data = matcher.match(partial)
match_data.negated_message.should contain(label)
end
end
end
end
end

View File

@ -8,7 +8,7 @@ describe Spectator::Matchers::PredicateMatcher do
it "is true" do
value = "foobar"
partial = new_partial(value)
matcher = Spectator::Matchers::PredicateMatcher(NamedTuple(ascii_only: Nil)).new
matcher = Spectator::Matchers::PredicateMatcher.new({ascii_only: Tuple.new}, "ascii_only")
match_data = matcher.match(partial)
match_data.matched?.should be_true
end
@ -18,7 +18,7 @@ describe Spectator::Matchers::PredicateMatcher do
it "is false" do
value = "foobar"
partial = new_partial(value)
matcher = Spectator::Matchers::PredicateMatcher(NamedTuple(empty: Nil)).new
matcher = Spectator::Matchers::PredicateMatcher.new({empty: Tuple.new}, "empty")
match_data = matcher.match(partial)
match_data.matched?.should be_false
end
@ -29,7 +29,7 @@ describe Spectator::Matchers::PredicateMatcher do
it "contains a key for each expected attribute" do
value = "foobar"
partial = new_partial(value)
matcher = Spectator::Matchers::PredicateMatcher(NamedTuple(empty: Nil, ascii_only: Nil)).new
matcher = Spectator::Matchers::PredicateMatcher.new({empty: Tuple.new, ascii_only: Tuple.new}, "empty, ascii_only")
match_data = matcher.match(partial)
match_data_has_key?(match_data.values, :empty).should be_true
match_data_has_key?(match_data.values, :ascii_only).should be_true
@ -38,7 +38,7 @@ describe Spectator::Matchers::PredicateMatcher do
it "has the actual values" do
value = "foobar"
partial = new_partial(value)
matcher = Spectator::Matchers::PredicateMatcher(NamedTuple(empty: Nil, ascii_only: Nil)).new
matcher = Spectator::Matchers::PredicateMatcher.new({empty: Tuple.new, ascii_only: Tuple.new}, "empty, ascii_only")
match_data = matcher.match(partial)
match_data_value_sans_prefix(match_data.values, :empty)[:value].should eq(value.empty?)
match_data_value_sans_prefix(match_data.values, :ascii_only)[:value].should eq(value.ascii_only?)
@ -50,17 +50,18 @@ describe Spectator::Matchers::PredicateMatcher do
value = "foobar"
label = "blah"
partial = new_partial(value, label)
matcher = Spectator::Matchers::PredicateMatcher(NamedTuple(ascii_only: Nil)).new
matcher = Spectator::Matchers::PredicateMatcher.new({ascii_only: Tuple.new}, "ascii_only")
match_data = matcher.match(partial)
match_data.message.should contain(label)
end
it "contains stringified form of predicate" do
it "contains the expected label" do
value = "foobar"
label = "blah"
partial = new_partial(value)
matcher = Spectator::Matchers::PredicateMatcher(NamedTuple(ascii_only: Nil)).new
matcher = Spectator::Matchers::PredicateMatcher.new({ascii_only: Tuple.new}, label)
match_data = matcher.match(partial)
match_data.message.should contain("ascii_only")
match_data.message.should contain(label)
end
end
@ -69,17 +70,18 @@ describe Spectator::Matchers::PredicateMatcher do
value = "foobar"
label = "blah"
partial = new_partial(value, label)
matcher = Spectator::Matchers::PredicateMatcher(NamedTuple(ascii_only: Nil)).new
matcher = Spectator::Matchers::PredicateMatcher.new({ascii_only: Tuple.new}, "ascii_only")
match_data = matcher.match(partial)
match_data.negated_message.should contain(label)
end
it "contains stringified form of predicate" do
it "contains the expected label" do
value = "foobar"
label = "blah"
partial = new_partial(value)
matcher = Spectator::Matchers::PredicateMatcher(NamedTuple(ascii_only: Nil)).new
matcher = Spectator::Matchers::PredicateMatcher.new({ascii_only: Tuple.new}, label)
match_data = matcher.match(partial)
match_data.negated_message.should contain("ascii_only")
match_data.negated_message.should contain(label)
end
end
end

View File

@ -561,7 +561,7 @@ module Spectator::DSL
end
# Used to create predicate matchers.
# Any missing method that starts with 'be_' will be handled.
# Any missing method that starts with 'be_' or 'have_' will be handled.
# All other method names will be ignored and raise a compile-time error.
#
# This can be used to simply check a predicate method that ends in '?'.
@ -570,14 +570,37 @@ module Spectator::DSL
# expect("foobar").to be_ascii_only
# # Is equivalent to:
# expect("foobar".ascii_only?).to be_true
#
# expect("foobar").to_not have_back_references
# # Is equivalent to:
# expect("foobar".has_back_references?).to_not be_true
# ```
macro method_missing(call)
{% if call.name.starts_with?("be_") %}
{% method_name = call.name[3..-1] %} # Remove be_ prefix.
::Spectator::Matchers::PredicateMatcher(NamedTuple({{method_name}}: Nil)).new
# Remove `be_` prefix.
{% method_name = call.name[3..-1] %}
{% matcher = "PredicateMatcher" %}
{% elsif call.name.starts_with?("have_") %}
# Remove `have_` prefix.
{% method_name = call.name[5..-1] %}
{% matcher = "HavePredicateMatcher" %}
{% else %}
{% raise "Undefined local variable or method '#{call}'" %}
{% end %}
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 %}
label << {{arg}}
{% if index < call.args.size - 1 %}
label << ", "
{% end %}
{% end %}
label << ')'
{% end %}
::Spectator::Matchers::{{matcher.id}}.new(descriptor, label.to_s)
end
end
end

View File

@ -0,0 +1,64 @@
require "./value_matcher"
module Spectator::Matchers
# Matcher that tests one or more "has" predicates
# (methods ending in '?' and starting with 'has_').
# The `ExpectedType` type param should be a `NamedTuple`.
# 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)
# Determines whether the matcher is satisfied with the value given to it.
private def match?(values)
# Test each predicate and immediately return false if one is false.
{% for attribute in ExpectedType.keys %}
return false unless values[{{attribute.symbolize}}]
{% end %}
# All checks passed if this point is reached.
true
end
# Determines whether the matcher is satisfied with the partial given to it.
# `MatchData` is returned that contains information about the match.
def match(partial) : MatchData
values = snapshot_values(partial.actual)
MatchData.new(match?(values), values, partial.label, label)
end
# Captures all of the actual values.
# A `NamedTuple` is returned,
# with each key being the attribute.
private def snapshot_values(actual)
{% begin %}
{
{% for attribute in ExpectedType.keys %}
{{attribute}}: actual.has_{{attribute}}?(*@expected[{{attribute.symbolize}}]),
{% end %}
}
{% end %}
end
# Match data specific to this matcher.
private struct MatchData(ActualType) < MatchData
# Creates the match data.
def initialize(matched, @named_tuple : ActualType, @actual_label : String, @expected_label : String)
super(matched)
end
# Information about the match.
getter named_tuple
# Describes the condition that satisfies the matcher.
# This is informational and displayed to the end-user.
def message
"#{@actual_label} has #{@expected_label}"
end
# Describes the condition that won't satsify the matcher.
# This is informational and displayed to the end-user.
def negated_message
"#{@actual_label} does not have #{@expected_label}"
end
end
end
end

View File

@ -4,13 +4,8 @@ module Spectator::Matchers
# Matcher that tests one or more predicates (methods ending in '?').
# The `ExpectedType` type param should be a `NamedTuple`.
# Each key in the tuple is a predicate (without the '?') to test.
struct PredicateMatcher(ExpectedType) < Matcher
# Textual representation of what the matcher expects.
# Constructs the label from the type parameters.
def label
{{ExpectedType.keys.splat.stringify}}
end
# Each value is a a `Tuple` of arguments to pass to the predicate method.
struct PredicateMatcher(ExpectedType) < ValueMatcher(ExpectedType)
# Determines whether the matcher is satisfied with the value given to it.
private def match?(values)
# Test each predicate and immediately return false if one is false.
@ -26,7 +21,7 @@ module Spectator::Matchers
# `MatchData` is returned that contains information about the match.
def match(partial) : MatchData
values = snapshot_values(partial.actual)
MatchData.new(match?(values), values, partial.label)
MatchData.new(match?(values), values, partial.label, label)
end
# Captures all of the actual values.
@ -36,7 +31,7 @@ module Spectator::Matchers
{% begin %}
{
{% for attribute in ExpectedType.keys %}
{{attribute}}: actual.{{attribute}}?,
{{attribute}}: actual.{{attribute}}?(*@expected[{{attribute.symbolize}}]),
{% end %}
}
{% end %}
@ -45,7 +40,7 @@ module Spectator::Matchers
# Match data specific to this matcher.
private struct MatchData(ActualType) < MatchData
# Creates the match data.
def initialize(matched, @named_tuple : ActualType, @actual_label : String)
def initialize(matched, @named_tuple : ActualType, @actual_label : String, @expected_label : String)
super(matched)
end
@ -55,13 +50,13 @@ module Spectator::Matchers
# Describes the condition that satisfies the matcher.
# This is informational and displayed to the end-user.
def message
"#{@actual_label} is " + {{ActualType.keys.splat.stringify}}
"#{@actual_label} is #{@expected_label}"
end
# Describes the condition that won't satsify the matcher.
# This is informational and displayed to the end-user.
def negated_message
"#{@actual_label} is not " + {{ActualType.keys.splat.stringify}}
"#{@actual_label} is not #{@expected_label}"
end
end
end