mirror of
https://gitea.invidious.io/iv-org/shard-spectator.git
synced 2024-08-15 00:53:35 +00:00
Merge branch 'change-matchers' into 'release/0.8'
Change matchers See merge request arctic-fox/spectator!10
This commit is contained in:
commit
454455d601
8 changed files with 514 additions and 36 deletions
|
@ -306,7 +306,7 @@ Items not marked as completed may have partial implementations.
|
|||
- [ ] Misc. matchers
|
||||
- [X] `match`
|
||||
- [ ] `satisfy`
|
||||
- [ ] `change[.by|.from[.to]|.to|.by_at_least|.by_at_most]`
|
||||
- [X] `change[.by|.from[.to]|.to|.by_at_least|.by_at_most]`
|
||||
- [X] `have_attributes`
|
||||
- [ ] Compound - `and`, `or`
|
||||
- [ ] Runner
|
||||
|
|
|
@ -55,9 +55,6 @@ module Spectator::DSL
|
|||
{% raise "Argument or block must be provided to expect" %}
|
||||
{% end %}
|
||||
|
||||
# Create a proc to capture the block.
|
||||
%proc = ->({{block.args.splat}}) {{block}}
|
||||
|
||||
# Check if the short-hand method syntax is used.
|
||||
# This is a hack, since macros don't get this as a "literal" or something similar.
|
||||
# The Crystal compiler will translate:
|
||||
|
@ -73,16 +70,19 @@ module Spectator::DSL
|
|||
# Extract the method name to make it clear to the user what is tested.
|
||||
# The raw block can't be used because it's not clear to the user.
|
||||
{% method_name = block.body.id.split('.')[1..-1].join('.') %}
|
||||
%partial = %proc.partial(subject)
|
||||
test_block = ::Spectator::TestBlock.create(%partial, {{"#" + method_name}})
|
||||
{% else %}
|
||||
%proc = ->{ subject.{{method_name.id}} }
|
||||
%test_block = ::Spectator::TestBlock.create(%proc, {{"#" + method_name}})
|
||||
{% elsif block.args.empty? %}
|
||||
# In this case, it looks like the short-hand method syntax wasn't used.
|
||||
# Just drop in the proc as-is.
|
||||
test_block = ::Spectator::TestBlock.create(%proc, {{"`" + block.body.stringify + "`"}})
|
||||
# Capture the block as a proc and pass along.
|
||||
%proc = ->{{block}}
|
||||
%test_block = ::Spectator::TestBlock.create(%proc, {{"`" + block.body.stringify + "`"}})
|
||||
{% else %}
|
||||
{% raise "Unexpected block arguments in expect call" %}
|
||||
{% end %}
|
||||
|
||||
source = ::Spectator::Source.new({{_source_file}}, {{_source_line}})
|
||||
::Spectator::Expectations::ExpectationPartial.new(test_block, source)
|
||||
%source = ::Spectator::Source.new({{_source_file}}, {{_source_line}})
|
||||
::Spectator::Expectations::ExpectationPartial.new(%test_block, %source)
|
||||
end
|
||||
|
||||
# Starts an expectation.
|
||||
|
|
|
@ -14,7 +14,8 @@ module Spectator::DSL
|
|||
# expect(1 + 2).to eq(3)
|
||||
# ```
|
||||
macro eq(expected)
|
||||
::Spectator::Matchers::EqualityMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}}))
|
||||
%test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}})
|
||||
::Spectator::Matchers::EqualityMatcher.new(%test_value)
|
||||
end
|
||||
|
||||
# Indicates that some value should not equal another.
|
||||
|
@ -26,7 +27,8 @@ module Spectator::DSL
|
|||
# expect(1 + 2).to ne(5)
|
||||
# ```
|
||||
macro ne(expected)
|
||||
::Spectator::Matchers::InequalityMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}}))
|
||||
%test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}})
|
||||
::Spectator::Matchers::InequalityMatcher.new(%test_value)
|
||||
end
|
||||
|
||||
# Indicates that some value when compared to another satisfies an operator.
|
||||
|
@ -60,7 +62,8 @@ module Spectator::DSL
|
|||
# expect(obj.dup).to_not be(obj)
|
||||
# ```
|
||||
macro be(expected)
|
||||
::Spectator::Matchers::ReferenceMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}}))
|
||||
%test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}})
|
||||
::Spectator::Matchers::ReferenceMatcher.new(%test_value)
|
||||
end
|
||||
|
||||
# Indicates that some value should be of a specified type.
|
||||
|
@ -117,7 +120,8 @@ module Spectator::DSL
|
|||
# expect(3 - 1).to be_lt(3)
|
||||
# ```
|
||||
macro be_lt(expected)
|
||||
::Spectator::Matchers::LessThanMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}}))
|
||||
%test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}})
|
||||
::Spectator::Matchers::LessThanMatcher.new(%test_value)
|
||||
end
|
||||
|
||||
# Indicates that some value should be less than or equal to another.
|
||||
|
@ -129,7 +133,8 @@ module Spectator::DSL
|
|||
# expect(3 - 1).to be_le(3)
|
||||
# ```
|
||||
macro be_le(expected)
|
||||
::Spectator::Matchers::LessThanEqualMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}}))
|
||||
%test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}})
|
||||
::Spectator::Matchers::LessThanEqualMatcher.new(%test_value)
|
||||
end
|
||||
|
||||
# Indicates that some value should be greater than another.
|
||||
|
@ -141,7 +146,8 @@ module Spectator::DSL
|
|||
# expect(3 + 1).to be_gt(3)
|
||||
# ```
|
||||
macro be_gt(expected)
|
||||
::Spectator::Matchers::GreaterThanMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}}))
|
||||
%test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}})
|
||||
::Spectator::Matchers::GreaterThanMatcher.new(%test_value)
|
||||
end
|
||||
|
||||
# Indicates that some value should be greater than or equal to another.
|
||||
|
@ -153,7 +159,8 @@ module Spectator::DSL
|
|||
# expect(3 + 1).to be_ge(3)
|
||||
# ```
|
||||
macro be_ge(expected)
|
||||
::Spectator::Matchers::GreaterThanEqualMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}}))
|
||||
%test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}})
|
||||
::Spectator::Matchers::GreaterThanEqualMatcher.new(%test_value)
|
||||
end
|
||||
|
||||
# Indicates that some value should match another.
|
||||
|
@ -170,7 +177,8 @@ module Spectator::DSL
|
|||
# expect({:foo, 5}).to match({Symbol, Int32})
|
||||
# ```
|
||||
macro match(expected)
|
||||
::Spectator::Matchers::CaseMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}}))
|
||||
%test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}})
|
||||
::Spectator::Matchers::CaseMatcher.new(%test_value)
|
||||
end
|
||||
|
||||
# Indicates that some value should be true.
|
||||
|
@ -260,7 +268,8 @@ module Spectator::DSL
|
|||
# NOTE: Do not attempt to mix the two use cases.
|
||||
# It likely won't work and will result in a compilation error.
|
||||
macro be_within(expected)
|
||||
::Spectator::Matchers::CollectionMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}}))
|
||||
%test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}})
|
||||
::Spectator::Matchers::CollectionMatcher.new(%test_value)
|
||||
end
|
||||
|
||||
# Indicates that some value should be between a lower and upper-bound.
|
||||
|
@ -280,10 +289,10 @@ module Spectator::DSL
|
|||
# expect(100).to be_between(97, 101).exclusive # 97, 98, 99, or 100 (not 101)
|
||||
# ```
|
||||
macro be_between(min, max)
|
||||
:Spectator::Matchers::RangeMatcher.new(
|
||||
::Spectator::TestValue.new(Range.new({{min}}, {{max}})),
|
||||
[{{min.stringify}}, {{max.stringify}}].join(" to ")
|
||||
)
|
||||
%range = Range.new({{min}}, {{max}}))
|
||||
%label = [{{min.stringify}}, {{max.stringify}}].join(" to ")
|
||||
%test_value = ::Spectator::TestValue.new(%range, %label)
|
||||
:Spectator::Matchers::RangeMatcher.new(%test_value)
|
||||
end
|
||||
|
||||
# Indicates that some value should be within a delta of an expected value.
|
||||
|
@ -341,7 +350,8 @@ module Spectator::DSL
|
|||
# expect(%w[foo bar]).to start_with(/foo/)
|
||||
# ```
|
||||
macro start_with(expected)
|
||||
::Spectator::Matchers::StartWithMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}}))
|
||||
%test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}})
|
||||
::Spectator::Matchers::StartWithMatcher.new(%test_value)
|
||||
end
|
||||
|
||||
# Indicates that some value or set should end with another value.
|
||||
|
@ -363,7 +373,8 @@ module Spectator::DSL
|
|||
# expect(%w[foo bar]).to end_with(/bar/)
|
||||
# ```
|
||||
macro end_with(expected)
|
||||
::Spectator::Matchers::EndWithMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}}))
|
||||
%test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}})
|
||||
::Spectator::Matchers::EndWithMatcher.new(%test_value)
|
||||
end
|
||||
|
||||
# Indicates that some value or set should contain another value.
|
||||
|
@ -386,7 +397,8 @@ module Spectator::DSL
|
|||
# expect(%i[a b c]).to contain(:a, :b)
|
||||
# ```
|
||||
macro contain(*expected)
|
||||
::Spectator::Matchers::ContainMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.splat.stringify}}))
|
||||
%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.
|
||||
|
@ -415,7 +427,8 @@ module Spectator::DSL
|
|||
# expect(%w[FOO BAR BAZ]).to have(/foo/i, String)
|
||||
# ```
|
||||
macro have(*expected)
|
||||
::Spectator::Matchers::HaveMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.splat.stringify}}))
|
||||
%test_value = ::Spectator::TestValue.new({{expected}}, {{expected.splat.stringify}})
|
||||
::Spectator::Matchers::HaveMatcher.new(%test_value)
|
||||
end
|
||||
|
||||
# Indicates that some set, such as a `Hash`, has a given key.
|
||||
|
@ -427,7 +440,8 @@ module Spectator::DSL
|
|||
# expect({"lucky" => 7}).to have_key("lucky")
|
||||
# ```
|
||||
macro have_key(expected)
|
||||
::Spectator::Matchers::HaveKeyMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}}))
|
||||
%test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}})
|
||||
::Spectator::Matchers::HaveKeyMatcher.new(%test_value)
|
||||
end
|
||||
|
||||
# ditto
|
||||
|
@ -444,7 +458,8 @@ module Spectator::DSL
|
|||
# expect({"lucky" => 7}).to have_value(7)
|
||||
# ```
|
||||
macro have_value(expected)
|
||||
::Spectator::Matchers::HaveValueMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}}))
|
||||
%test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}})
|
||||
::Spectator::Matchers::HaveValueMatcher.new(%test_value)
|
||||
end
|
||||
|
||||
# ditto
|
||||
|
@ -459,7 +474,8 @@ module Spectator::DSL
|
|||
# expect([1, 2, 3]).to contain_exactly(1, 2, 3)
|
||||
# ```
|
||||
macro contain_exactly(*expected)
|
||||
::Spectator::Matchers::ArrayMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}}))
|
||||
%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.
|
||||
|
@ -469,7 +485,8 @@ module Spectator::DSL
|
|||
# expect([1, 2, 3]).to match_array([1, 2, 3])
|
||||
# ```
|
||||
macro match_array(expected)
|
||||
::Spectator::Matchers::ArrayMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}}))
|
||||
%test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}})
|
||||
::Spectator::Matchers::ArrayMatcher.new(%test_value)
|
||||
end
|
||||
|
||||
# Indicates that some set should have a specified size.
|
||||
|
@ -479,7 +496,8 @@ module Spectator::DSL
|
|||
# expect([1, 2, 3]).to have_size(3)
|
||||
# ```
|
||||
macro have_size(expected)
|
||||
::Spectator::Matchers::SizeMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.stringify}}))
|
||||
%test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}})
|
||||
::Spectator::Matchers::SizeMatcher.new(%test_value)
|
||||
end
|
||||
|
||||
# Indicates that some set should have the same size (number of elements) as another set.
|
||||
|
@ -489,7 +507,8 @@ module Spectator::DSL
|
|||
# expect([1, 2, 3]).to have_size_of(%i[x y z])
|
||||
# ```
|
||||
macro have_size_of(expected)
|
||||
::Spectator::Matchers::SizeOfMatcher.new(::Spectator::TestValue.new(({{expected}}), {{expected.stringify}}))
|
||||
%test_value = ::Spectator::TestValue.new(({{expected}}), {{expected.stringify}})
|
||||
::Spectator::Matchers::SizeOfMatcher.new(%test_value)
|
||||
end
|
||||
|
||||
# Indicates that some value should have a set of attributes matching some conditions.
|
||||
|
@ -503,7 +522,77 @@ module Spectator::DSL
|
|||
# expect(%i[a b c]).to have_attributes(size: 1..5, first: Symbol)
|
||||
# ```
|
||||
macro have_attributes(**expected)
|
||||
::Spectator::Matchers::AttributesMatcher.new(::Spectator::TestValue.new({{expected}}, {{expected.double_splat.stringify}}))
|
||||
%test_value = ::Spectator::TestValue.new({{expected}}, {{expected.double_splat.stringify}})
|
||||
::Spectator::Matchers::AttributesMatcher.new(%test_value)
|
||||
end
|
||||
|
||||
# Indicates that some expression's value should change after taking an action.
|
||||
#
|
||||
# Examples:
|
||||
# ```
|
||||
# i = 0
|
||||
# expect { i += 1 }.to change { i }
|
||||
# expect { i += 0 }.to_not change { i }
|
||||
# ```
|
||||
#
|
||||
# ```
|
||||
# i = 0
|
||||
# expect { i += 5 }.to change { i }.from(0).to(5)
|
||||
# ```
|
||||
#
|
||||
# ```
|
||||
# i = 0
|
||||
# expect { i += 5 }.to change { i }.to(5)
|
||||
# ```
|
||||
#
|
||||
# ```
|
||||
# i = 0
|
||||
# expect { i += 5 }.to change { i }.from(0)
|
||||
# ```
|
||||
#
|
||||
# ```
|
||||
# i = 0
|
||||
# expect { i += 42 }.to change { i }.by(42)
|
||||
# ```
|
||||
#
|
||||
# The block short-hand syntax can be used here.
|
||||
# It will reference the current subject.
|
||||
#
|
||||
# ```
|
||||
# expect { subject << :foo }.to change(&.size).by(1)
|
||||
# ```
|
||||
macro change(&expression)
|
||||
{% if expression.is_a?(Nop) %}
|
||||
{% raise "Block must be provided to change matcher" %}
|
||||
{% end %}
|
||||
|
||||
# Check if the short-hand method syntax is used.
|
||||
# This is a hack, since macros don't get this as a "literal" or something similar.
|
||||
# The Crystal compiler will translate:
|
||||
# ```
|
||||
# &.foo
|
||||
# ```
|
||||
# to:
|
||||
# ```
|
||||
# { |__arg0| __arg0.foo }
|
||||
# ```
|
||||
# The hack used here is to check if it looks like a compiler-generated block.
|
||||
{% if expression.args == ["__arg0".id] && expression.body.is_a?(Call) && expression.body.id =~ /^__arg0\./ %}
|
||||
# Extract the method name to make it clear to the user what is tested.
|
||||
# The raw block can't be used because it's not clear to the user.
|
||||
{% method_name = expression.body.id.split('.')[1..-1].join('.') %}
|
||||
%proc = ->{ subject.{{method_name.id}} }
|
||||
%test_block = ::Spectator::TestBlock.create(%proc, {{"#" + method_name}})
|
||||
{% elsif expression.args.empty? %}
|
||||
# In this case, it looks like the short-hand method syntax wasn't used.
|
||||
# Capture the block as a proc and pass along.
|
||||
%proc = ->{{expression}}
|
||||
%test_block = ::Spectator::TestBlock.create(%proc, {{"`" + expression.body.stringify + "`"}})
|
||||
{% else %}
|
||||
{% raise "Unexpected block arguments in change matcher" %}
|
||||
{% end %}
|
||||
|
||||
::Spectator::Matchers::ChangeMatcher.new(%test_block)
|
||||
end
|
||||
|
||||
# Indicates that some block should raise an error.
|
||||
|
@ -622,7 +711,8 @@ module Spectator::DSL
|
|||
{% end %}
|
||||
label << ')'
|
||||
{% end %}
|
||||
::Spectator::Matchers::{{matcher.id}}.new(::Spectator::TestValue.new(descriptor, label.to_s))
|
||||
test_value = ::Spectator::TestValue.new(descriptor, label.to_s)
|
||||
::Spectator::Matchers::{{matcher.id}}.new(test_value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
84
src/spectator/matchers/change_exact_matcher.cr
Normal file
84
src/spectator/matchers/change_exact_matcher.cr
Normal file
|
@ -0,0 +1,84 @@
|
|||
require "./failed_match_data"
|
||||
require "./matcher"
|
||||
require "./successful_match_data"
|
||||
|
||||
module Spectator::Matchers
|
||||
# Matcher that tests whether an expression changed from and to specific values.
|
||||
struct ChangeExactMatcher(ExpressionType, FromType, ToType) < Matcher
|
||||
# The expression that is expected to (not) change.
|
||||
private getter expression
|
||||
|
||||
# The expected value of the expression before the change.
|
||||
private getter expected_before
|
||||
|
||||
# The expected value of the expression after the change.
|
||||
private getter expected_after
|
||||
|
||||
# Creates a new change matcher.
|
||||
def initialize(@expression : TestBlock(ExpressionType), @expected_before : FromType, @expected_after : ToType)
|
||||
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.
|
||||
def description
|
||||
"changes #{expression.label} from #{expected_before.inspect} to #{expected_after.inspect}"
|
||||
end
|
||||
|
||||
# Actually performs the test against the expression.
|
||||
def match(actual : TestExpression(T)) : MatchData forall T
|
||||
before, after = change(actual)
|
||||
if expected_before == before
|
||||
if before == after
|
||||
FailedMatchData.new("#{actual.label} did not change #{expression.label}",
|
||||
before: before.inspect,
|
||||
after: after.inspect
|
||||
)
|
||||
elsif expected_after == after
|
||||
SuccessfulMatchData.new
|
||||
else
|
||||
FailedMatchData.new("#{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}",
|
||||
expected: expected_before.inspect,
|
||||
actual: before.inspect,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# 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
|
||||
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}",
|
||||
before: before.inspect,
|
||||
after: after.inspect
|
||||
)
|
||||
else
|
||||
SuccessfulMatchData.new
|
||||
end
|
||||
else
|
||||
FailedMatchData.new("#{expression.label} was not initially #{expected_before.inspect}",
|
||||
expected: expected_before.inspect,
|
||||
actual: before.inspect,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Performs the change and reports the before and after values.
|
||||
private def change(actual)
|
||||
before = expression.value # Retrieve the expression's initial value.
|
||||
actual.value # Invoke action that might change the expression's value.
|
||||
after = expression.value # Retrieve the expression's value again.
|
||||
|
||||
{before, after}
|
||||
end
|
||||
end
|
||||
end
|
84
src/spectator/matchers/change_from_matcher.cr
Normal file
84
src/spectator/matchers/change_from_matcher.cr
Normal file
|
@ -0,0 +1,84 @@
|
|||
require "./change_exact_matcher"
|
||||
require "./failed_match_data"
|
||||
require "./matcher"
|
||||
require "./successful_match_data"
|
||||
|
||||
module Spectator::Matchers
|
||||
# Matcher that tests whether an expression changed from a specific value.
|
||||
struct ChangeFromMatcher(ExpressionType, FromType) < Matcher
|
||||
# The expression that is expected to (not) change.
|
||||
private getter expression
|
||||
|
||||
# The expected value of the expression before the change.
|
||||
private getter expected
|
||||
|
||||
# Creates a new change matcher.
|
||||
def initialize(@expression : TestBlock(ExpressionType), @expected : FromType)
|
||||
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.
|
||||
def description
|
||||
"changes #{expression.label} from #{expected}"
|
||||
end
|
||||
|
||||
# Actually performs the test against the expression.
|
||||
def match(actual : TestExpression(T)) : MatchData forall T
|
||||
before, after = change(actual)
|
||||
if expected != before
|
||||
FailedMatchData.new("#{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}",
|
||||
before: before.inspect,
|
||||
after: after.inspect,
|
||||
expected: "Not #{expected.inspect}"
|
||||
)
|
||||
else
|
||||
SuccessfulMatchData.new
|
||||
end
|
||||
end
|
||||
|
||||
# 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
|
||||
before, after = change(actual)
|
||||
if expected != before
|
||||
FailedMatchData.new("#{expression.label} was not initially #{expected}",
|
||||
expected: expected.inspect,
|
||||
actual: before.inspect
|
||||
)
|
||||
elsif before == after
|
||||
SuccessfulMatchData.new
|
||||
else
|
||||
FailedMatchData.new("#{actual.label} changed #{expression.label} from #{expected}",
|
||||
before: before.inspect,
|
||||
after: after.inspect,
|
||||
expected: expected.inspect
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Specifies what the resulting value of the expression must be.
|
||||
def to(value : T) forall T
|
||||
ChangeExactMatcher.new(@expression, @expected, value)
|
||||
end
|
||||
|
||||
# Specifies what the resulting value of the expression should change by.
|
||||
def by(amount : T) forall T
|
||||
ChangeExactMatcher.new(@expression, @expected, @expected + value)
|
||||
end
|
||||
|
||||
# Performs the change and reports the before and after values.
|
||||
private def change(actual)
|
||||
before = expression.value # Retrieve the expression's initial value.
|
||||
actual.value # Invoke action that might change the expression's value.
|
||||
after = expression.value # Retrieve the expression's value again.
|
||||
|
||||
{before, after}
|
||||
end
|
||||
end
|
||||
end
|
85
src/spectator/matchers/change_matcher.cr
Normal file
85
src/spectator/matchers/change_matcher.cr
Normal file
|
@ -0,0 +1,85 @@
|
|||
require "./change_from_matcher"
|
||||
require "./change_to_matcher"
|
||||
require "./failed_match_data"
|
||||
require "./matcher"
|
||||
require "./successful_match_data"
|
||||
|
||||
module Spectator::Matchers
|
||||
# Matcher that tests whether an expression changed.
|
||||
struct ChangeMatcher(ExpressionType) < Matcher
|
||||
# The expression that is expected to (not) change.
|
||||
private getter expression
|
||||
|
||||
# Creates a new change matcher.
|
||||
def initialize(@expression : TestBlock(ExpressionType))
|
||||
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.
|
||||
def description
|
||||
"changes #{expression.label}"
|
||||
end
|
||||
|
||||
# Actually performs the test against the expression.
|
||||
def match(actual : TestExpression(T)) : MatchData forall T
|
||||
before, after = change(actual)
|
||||
if before == after
|
||||
FailedMatchData.new("#{actual.label} did not change #{expression.label}",
|
||||
before: before.inspect,
|
||||
after: after.inspect
|
||||
)
|
||||
else
|
||||
SuccessfulMatchData.new
|
||||
end
|
||||
end
|
||||
|
||||
# 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
|
||||
before, after = change(actual)
|
||||
if before == after
|
||||
SuccessfulMatchData.new
|
||||
else
|
||||
FailedMatchData.new("#{actual.label} changed #{expression.label}",
|
||||
before: before.inspect,
|
||||
after: after.inspect
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Specifies what the initial value of the expression must be.
|
||||
def from(value : T) forall T
|
||||
ChangeFromMatcher.new(@expression, value)
|
||||
end
|
||||
|
||||
# Specifies what the resulting value of the expression must be.
|
||||
def to(value : T) forall T
|
||||
ChangeToMatcher.new(@expression, value)
|
||||
end
|
||||
|
||||
# Specifies that t he resulting value must be some amount different.
|
||||
def by(amount : T) forall T
|
||||
ChangeRelativeMatcher.new(@expression, "by #{amount}") { |before, after| amount == after - before }
|
||||
end
|
||||
|
||||
# Specifies that the resulting value must be at least some amount different.
|
||||
def by_at_least(minimum : T) forall T
|
||||
ChangeRelativeMatcher.new(@expression, "by at least #{minimum}") { |before, after| minimum <= after - before }
|
||||
end
|
||||
|
||||
# Specifies that the resulting value must be at most some amount different.
|
||||
def by_at_most(maximum : T) forall T
|
||||
ChangeRelativeMatcher.new(@expression, "by at most #{maximum}") { |before, after| maximum >= after - before }
|
||||
end
|
||||
|
||||
# Performs the change and reports the before and after values.
|
||||
private def change(actual)
|
||||
before = expression.value # Retrieve the expression's initial value.
|
||||
actual.value # Invoke action that might change the expression's value.
|
||||
after = expression.value # Retrieve the expression's value again.
|
||||
|
||||
{before, after}
|
||||
end
|
||||
end
|
||||
end
|
57
src/spectator/matchers/change_relative_matcher.cr
Normal file
57
src/spectator/matchers/change_relative_matcher.cr
Normal file
|
@ -0,0 +1,57 @@
|
|||
require "./failed_match_data"
|
||||
require "./matcher"
|
||||
require "./successful_match_data"
|
||||
|
||||
module Spectator::Matchers
|
||||
# Matcher that tests whether an expression changed by an amount.
|
||||
struct ChangeRelativeMatcher(ExpressionType) < Matcher
|
||||
# The expression that is expected to (not) change.
|
||||
private getter expression
|
||||
|
||||
# Creates a new change matcher.
|
||||
def initialize(@expression : TestBlock(ExpressionType), @relativity : String,
|
||||
&evaluator : ExpressionType, ExpressionType -> Bool)
|
||||
@evaluator = evaluator
|
||||
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.
|
||||
def description
|
||||
"changes #{expression.label} #{@relativity}"
|
||||
end
|
||||
|
||||
# Actually performs the test against the expression.
|
||||
def match(actual : TestExpression(T)) : MatchData forall T
|
||||
before, after = change(actual)
|
||||
if before == after
|
||||
FailedMatchData.new("#{actual.label} did not change #{expression.label}",
|
||||
before: before.inspect,
|
||||
after: after.inspect
|
||||
)
|
||||
elsif @evaluator.call(before, after)
|
||||
SuccessfulMatchData.new
|
||||
else
|
||||
FailedMatchData.new("#{actual.label} did not change #{expression.label} #{@relativity}",
|
||||
before: before.inspect,
|
||||
after: after.inspect
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# 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
|
||||
{% raise "The `expect { }.to_not change { }.by_...()` syntax is not supported (ambiguous)." %}
|
||||
end
|
||||
|
||||
# Performs the change and reports the before and after values.
|
||||
private def change(actual)
|
||||
before = expression.value # Retrieve the expression's initial value.
|
||||
actual.value # Invoke action that might change the expression's value.
|
||||
after = expression.value # Retrieve the expression's value again.
|
||||
|
||||
{before, after}
|
||||
end
|
||||
end
|
||||
end
|
78
src/spectator/matchers/change_to_matcher.cr
Normal file
78
src/spectator/matchers/change_to_matcher.cr
Normal file
|
@ -0,0 +1,78 @@
|
|||
require "./change_exact_matcher"
|
||||
require "./failed_match_data"
|
||||
require "./matcher"
|
||||
require "./successful_match_data"
|
||||
|
||||
module Spectator::Matchers
|
||||
# Matcher that tests whether an expression changed to a specific value.
|
||||
struct ChangeToMatcher(ExpressionType, ToType) < Matcher
|
||||
# The expression that is expected to (not) change.
|
||||
private getter expression
|
||||
|
||||
# The expected value of the expression after the change.
|
||||
private getter expected
|
||||
|
||||
# Creates a new change matcher.
|
||||
def initialize(@expression : TestBlock(ExpressionType), @expected : ToType)
|
||||
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.
|
||||
def description
|
||||
"changes #{expression.label} to #{expected}"
|
||||
end
|
||||
|
||||
# Actually performs the test against the expression.
|
||||
def match(actual : TestExpression(T)) : MatchData forall T
|
||||
before, after = change(actual)
|
||||
if before == after
|
||||
FailedMatchData.new("#{actual.label} did not change #{expression.label}",
|
||||
before: before.inspect,
|
||||
after: after.inspect,
|
||||
expected: expected.inspect
|
||||
)
|
||||
elsif expected == after
|
||||
SuccessfulMatchData.new
|
||||
else
|
||||
FailedMatchData.new("#{actual.label} did not change #{expression.label} to #{expected}",
|
||||
before: before.inspect,
|
||||
after: after.inspect,
|
||||
expected: expected.inspect
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Negated matching for this matcher is not supported.
|
||||
# Attempting to call this method will result in a compilation error.
|
||||
#
|
||||
# This syntax has a logical problem.
|
||||
# "The action does not change the expression to some value."
|
||||
# Is it a failure if the value is not changed,
|
||||
# but it is the expected value?
|
||||
#
|
||||
# RSpec doesn't support this syntax either.
|
||||
def negated_match(actual : TestExpression(T)) : MatchData forall T
|
||||
{% raise "The `expect { }.to_not change { }.to()` syntax is not supported (ambiguous)." %}
|
||||
end
|
||||
|
||||
# Specifies what the initial value of the expression must be.
|
||||
def from(value : T) forall T
|
||||
ChangeExactMatcher.new(@expression, value, @expected)
|
||||
end
|
||||
|
||||
# Specifies how much the initial value should change by.
|
||||
def by(amount : T) forall T
|
||||
ChangeExactMatcher.new(@expression, @expected - amount, @expected)
|
||||
end
|
||||
|
||||
# Performs the change and reports the before and after values.
|
||||
private def change(actual)
|
||||
before = expression.value # Retrieve the expression's initial value.
|
||||
actual.value # Invoke action that might change the expression's value.
|
||||
after = expression.value # Retrieve the expression's value again.
|
||||
|
||||
{before, after}
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue