mirror of
https://gitea.invidious.io/iv-org/shard-spectator.git
synced 2024-08-15 00:53:35 +00:00
Initial work on assertions
This commit is contained in:
parent
122395837f
commit
096c31d7f5
7 changed files with 263 additions and 226 deletions
15
src/spectator/assertion.cr
Normal file
15
src/spectator/assertion.cr
Normal file
|
@ -0,0 +1,15 @@
|
|||
require "./block"
|
||||
require "./expression"
|
||||
|
||||
module Spectator
|
||||
class Assertion
|
||||
struct Target(T)
|
||||
@expression : Expression(T) | Block(T)
|
||||
@source : Source?
|
||||
|
||||
def initialize(@expression : Expression(T) | Block(T), @source)
|
||||
puts "TARGET: #{@expression} @ #{@source}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
15
src/spectator/assertion_failed.cr
Normal file
15
src/spectator/assertion_failed.cr
Normal file
|
@ -0,0 +1,15 @@
|
|||
require "./source"
|
||||
|
||||
module Spectator
|
||||
# Exception that indicates an assertion failed.
|
||||
# When raised within a test, the test should abort.
|
||||
class AssertionFailed < Exception
|
||||
# Location where the assertion failed and the exception raised.
|
||||
getter source : Source
|
||||
|
||||
# Creates the exception.
|
||||
def initialize(@source : Source, message : String? = nil, cause : Exception? = nil)
|
||||
super(message, cause)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,4 +1,5 @@
|
|||
# require "./dsl/*"
|
||||
require "./dsl/assertions"
|
||||
require "./dsl/builder"
|
||||
require "./dsl/examples"
|
||||
require "./dsl/groups"
|
||||
|
|
|
@ -1,216 +1,241 @@
|
|||
require "../expectations/expectation_partial"
|
||||
require "../assertion"
|
||||
require "../assertion_failed"
|
||||
require "../expression"
|
||||
require "../source"
|
||||
require "../test_block"
|
||||
require "../test_value"
|
||||
|
||||
module Spectator
|
||||
module DSL
|
||||
# Starts an expectation.
|
||||
# This should be followed up with `Spectator::Expectations::ExpectationPartial#to`
|
||||
# or `Spectator::Expectations::ExpectationPartial#to_not`.
|
||||
# The value passed in will be checked
|
||||
# to see if it satisfies the conditions specified.
|
||||
#
|
||||
# This method should be used like so:
|
||||
# ```
|
||||
# expect(actual).to eq(expected)
|
||||
# ```
|
||||
# Where the actual value is returned by the system-under-test,
|
||||
# and the expected value is what the actual value should be to satisfy the condition.
|
||||
macro expect(actual, _source_file = __FILE__, _source_line = __LINE__)
|
||||
%test_value = ::Spectator::TestValue.new({{actual}}, {{actual.stringify}})
|
||||
%source = ::Spectator::Source.new({{_source_file}}, {{_source_line}})
|
||||
::Spectator::Expectations::ExpectationPartial.new(%test_value, %source)
|
||||
module Spectator::DSL
|
||||
# Methods and macros for asserting that conditions are met.
|
||||
module Assertions
|
||||
# Checks that the specified condition is true.
|
||||
# Raises `AssertionFailed` if *condition* is false.
|
||||
# The *message* is passed to the exception.
|
||||
def assert(condition, message, *, _file = __FILE__, _line = __LINE__)
|
||||
raise AssertionFailed.new(Source.new(_file, _line), message) unless condition
|
||||
end
|
||||
|
||||
# Starts an expectation on a block of code.
|
||||
# This should be followed up with `Spectator::Expectations::ExpectationPartial#to`
|
||||
# or `Spectator::Expectations::ExpectationPartial#to_not`.
|
||||
# The block passed in, or its return value, will be checked
|
||||
# to see if it satisfies the conditions specified.
|
||||
#
|
||||
# This method should be used like so:
|
||||
# ```
|
||||
# expect { raise "foo" }.to raise_error
|
||||
# ```
|
||||
# The block of code is passed along for validation to the matchers.
|
||||
#
|
||||
# The short, one argument syntax used for passing methods to blocks can be used.
|
||||
# So instead of doing this:
|
||||
# ```
|
||||
# expect(subject.size).to eq(5)
|
||||
# ```
|
||||
# The following syntax can be used instead:
|
||||
# ```
|
||||
# expect(&.size).to eq(5)
|
||||
# ```
|
||||
# The method passed will always be evaluated on the subject.
|
||||
macro expect(_source_file = __FILE__, _source_line = __LINE__, &block)
|
||||
{% if block.is_a?(Nop) %}
|
||||
{% raise "Argument or block must be provided to expect" %}
|
||||
{% 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 block.args.size == 1 && block.args[0] =~ /^__arg\d+$/ && block.body.is_a?(Call) && block.body.id =~ /^__arg\d+\./ %}
|
||||
# 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('.') %}
|
||||
%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.
|
||||
# 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)
|
||||
# Checks that the specified condition is true.
|
||||
# Raises `AssertionFailed` if *condition* is false.
|
||||
# The message of the exception is the *condition*.
|
||||
macro assert(condition)
|
||||
assert({{condition}}, {{condition.stringify}}, _file: {{condition.filename}}, _line: {{condition.line_number}})
|
||||
end
|
||||
|
||||
# Starts an expectation.
|
||||
# This should be followed up with `Spectator::Expectations::ExpectationPartial#to`
|
||||
# or `Spectator::Expectations::ExpectationPartial#to_not`.
|
||||
# The value passed in will be checked
|
||||
# to see if it satisfies the conditions specified.
|
||||
#
|
||||
# This method is identical to `#expect`,
|
||||
# but is grammatically correct for the one-liner syntax.
|
||||
# It can be used like so:
|
||||
# ```
|
||||
# it expects(actual).to eq(expected)
|
||||
# ```
|
||||
# Where the actual value is returned by the system-under-test,
|
||||
# and the expected value is what the actual value should be to satisfy the condition.
|
||||
macro expects(actual)
|
||||
expect({{actual}})
|
||||
end
|
||||
macro expect(actual)
|
||||
%actual = begin
|
||||
{{actual}}
|
||||
end
|
||||
|
||||
# Starts an expectation on a block of code.
|
||||
# This should be followed up with `Spectator::Expectations::ExpectationPartial#to`
|
||||
# or `Spectator::Expectations::ExpectationPartial#to_not`.
|
||||
# The block passed in, or its return value, will be checked
|
||||
# to see if it satisfies the conditions specified.
|
||||
#
|
||||
# This method is identical to `#expect`,
|
||||
# but is grammatically correct for the one-liner syntax.
|
||||
# It can be used like so:
|
||||
# ```
|
||||
# it expects { 5 / 0 }.to raise_error
|
||||
# ```
|
||||
# The block of code is passed along for validation to the matchers.
|
||||
#
|
||||
# The short, one argument syntax used for passing methods to blocks can be used.
|
||||
# So instead of doing this:
|
||||
# ```
|
||||
# it expects(subject.size).to eq(5)
|
||||
# ```
|
||||
# The following syntax can be used instead:
|
||||
# ```
|
||||
# it expects(&.size).to eq(5)
|
||||
# ```
|
||||
# The method passed will always be evaluated on the subject.
|
||||
macro expects(&block)
|
||||
expect {{block}}
|
||||
end
|
||||
|
||||
# Short-hand for expecting something of the subject.
|
||||
# These two are functionally equivalent:
|
||||
# ```
|
||||
# expect(subject).to eq("foo")
|
||||
# is_expected.to eq("foo")
|
||||
# ```
|
||||
macro is_expected
|
||||
expect(subject)
|
||||
end
|
||||
|
||||
# Short-hand form of `#is_expected` that can be used for one-liner syntax.
|
||||
# For instance:
|
||||
# ```
|
||||
# it "is 42" do
|
||||
# expect(subject).to eq(42)
|
||||
# end
|
||||
# ```
|
||||
# Can be shortened to:
|
||||
# ```
|
||||
# it is(42)
|
||||
# ```
|
||||
#
|
||||
# These three are functionally equivalent:
|
||||
# ```
|
||||
# expect(subject).to eq("foo")
|
||||
# is_expected.to eq("foo")
|
||||
# is("foo")
|
||||
# ```
|
||||
#
|
||||
# See also: `#is_not`
|
||||
macro is(expected)
|
||||
is_expected.to eq({{expected}})
|
||||
end
|
||||
|
||||
# Short-hand, negated form of `#is_expected` that can be used for one-liner syntax.
|
||||
# For instance:
|
||||
# ```
|
||||
# it "is not 42" do
|
||||
# expect(subject).to_not eq(42)
|
||||
# end
|
||||
# ```
|
||||
# Can be shortened to:
|
||||
# ```
|
||||
# it is_not(42)
|
||||
# ```
|
||||
#
|
||||
# These three are functionally equivalent:
|
||||
# ```
|
||||
# expect(subject).to_not eq("foo")
|
||||
# is_expected.to_not eq("foo")
|
||||
# is_not("foo")
|
||||
# ```
|
||||
#
|
||||
# See also: `#is`
|
||||
macro is_not(expected)
|
||||
is_expected.to_not eq({{expected}})
|
||||
end
|
||||
|
||||
macro should(matcher)
|
||||
is_expected.to({{matcher}})
|
||||
end
|
||||
|
||||
macro should_not(matcher)
|
||||
is_expected.to_not({{matcher}})
|
||||
end
|
||||
|
||||
macro should_eventually(matcher)
|
||||
is_expected.to_eventually({{matcher}})
|
||||
end
|
||||
|
||||
macro should_never(matcher)
|
||||
is_expected.to_never({{matcher}})
|
||||
end
|
||||
|
||||
# Immediately fail the current test.
|
||||
# A reason can be passed,
|
||||
# which is reported in the output.
|
||||
def fail(reason : String)
|
||||
raise ExampleFailed.new(reason)
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
@[AlwaysInline]
|
||||
def fail
|
||||
fail("Example failed")
|
||||
%expression = ::Spectator::Expression.new(%actual, {{actual.stringify}})
|
||||
%source = ::Spectator::Source.new({{actual.filename}}, {{actual.line_number}})
|
||||
::Spectator::Assertion::Target.new(%expression, %source)
|
||||
end
|
||||
end
|
||||
|
||||
# Starts an expectation.
|
||||
# This should be followed up with `Spectator::Expectations::ExpectationPartial#to`
|
||||
# or `Spectator::Expectations::ExpectationPartial#to_not`.
|
||||
# The value passed in will be checked
|
||||
# to see if it satisfies the conditions specified.
|
||||
#
|
||||
# This method should be used like so:
|
||||
# ```
|
||||
# expect(actual).to eq(expected)
|
||||
# ```
|
||||
# Where the actual value is returned by the system-under-test,
|
||||
# and the expected value is what the actual value should be to satisfy the condition.
|
||||
macro expect(actual, _source_file = __FILE__, _source_line = __LINE__)
|
||||
%test_value = ::Spectator::TestValue.new({{actual}}, {{actual.stringify}})
|
||||
%source = ::Spectator::Source.new({{_source_file}}, {{_source_line}})
|
||||
::Spectator::Expectations::ExpectationPartial.new(%test_value, %source)
|
||||
end
|
||||
|
||||
# Starts an expectation on a block of code.
|
||||
# This should be followed up with `Spectator::Expectations::ExpectationPartial#to`
|
||||
# or `Spectator::Expectations::ExpectationPartial#to_not`.
|
||||
# The block passed in, or its return value, will be checked
|
||||
# to see if it satisfies the conditions specified.
|
||||
#
|
||||
# This method should be used like so:
|
||||
# ```
|
||||
# expect { raise "foo" }.to raise_error
|
||||
# ```
|
||||
# The block of code is passed along for validation to the matchers.
|
||||
#
|
||||
# The short, one argument syntax used for passing methods to blocks can be used.
|
||||
# So instead of doing this:
|
||||
# ```
|
||||
# expect(subject.size).to eq(5)
|
||||
# ```
|
||||
# The following syntax can be used instead:
|
||||
# ```
|
||||
# expect(&.size).to eq(5)
|
||||
# ```
|
||||
# The method passed will always be evaluated on the subject.
|
||||
macro expect(_source_file = __FILE__, _source_line = __LINE__, &block)
|
||||
{% if block.is_a?(Nop) %}
|
||||
{% raise "Argument or block must be provided to expect" %}
|
||||
{% 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 block.args.size == 1 && block.args[0] =~ /^__arg\d+$/ && block.body.is_a?(Call) && block.body.id =~ /^__arg\d+\./ %}
|
||||
# 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('.') %}
|
||||
%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.
|
||||
# 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)
|
||||
end
|
||||
|
||||
# Starts an expectation.
|
||||
# This should be followed up with `Spectator::Expectations::ExpectationPartial#to`
|
||||
# or `Spectator::Expectations::ExpectationPartial#to_not`.
|
||||
# The value passed in will be checked
|
||||
# to see if it satisfies the conditions specified.
|
||||
#
|
||||
# This method is identical to `#expect`,
|
||||
# but is grammatically correct for the one-liner syntax.
|
||||
# It can be used like so:
|
||||
# ```
|
||||
# it expects(actual).to eq(expected)
|
||||
# ```
|
||||
# Where the actual value is returned by the system-under-test,
|
||||
# and the expected value is what the actual value should be to satisfy the condition.
|
||||
macro expects(actual)
|
||||
expect({{actual}})
|
||||
end
|
||||
|
||||
# Starts an expectation on a block of code.
|
||||
# This should be followed up with `Spectator::Expectations::ExpectationPartial#to`
|
||||
# or `Spectator::Expectations::ExpectationPartial#to_not`.
|
||||
# The block passed in, or its return value, will be checked
|
||||
# to see if it satisfies the conditions specified.
|
||||
#
|
||||
# This method is identical to `#expect`,
|
||||
# but is grammatically correct for the one-liner syntax.
|
||||
# It can be used like so:
|
||||
# ```
|
||||
# it expects { 5 / 0 }.to raise_error
|
||||
# ```
|
||||
# The block of code is passed along for validation to the matchers.
|
||||
#
|
||||
# The short, one argument syntax used for passing methods to blocks can be used.
|
||||
# So instead of doing this:
|
||||
# ```
|
||||
# it expects(subject.size).to eq(5)
|
||||
# ```
|
||||
# The following syntax can be used instead:
|
||||
# ```
|
||||
# it expects(&.size).to eq(5)
|
||||
# ```
|
||||
# The method passed will always be evaluated on the subject.
|
||||
macro expects(&block)
|
||||
expect {{block}}
|
||||
end
|
||||
|
||||
# Short-hand for expecting something of the subject.
|
||||
# These two are functionally equivalent:
|
||||
# ```
|
||||
# expect(subject).to eq("foo")
|
||||
# is_expected.to eq("foo")
|
||||
# ```
|
||||
macro is_expected
|
||||
expect(subject)
|
||||
end
|
||||
|
||||
# Short-hand form of `#is_expected` that can be used for one-liner syntax.
|
||||
# For instance:
|
||||
# ```
|
||||
# it "is 42" do
|
||||
# expect(subject).to eq(42)
|
||||
# end
|
||||
# ```
|
||||
# Can be shortened to:
|
||||
# ```
|
||||
# it is(42)
|
||||
# ```
|
||||
#
|
||||
# These three are functionally equivalent:
|
||||
# ```
|
||||
# expect(subject).to eq("foo")
|
||||
# is_expected.to eq("foo")
|
||||
# is("foo")
|
||||
# ```
|
||||
#
|
||||
# See also: `#is_not`
|
||||
macro is(expected)
|
||||
is_expected.to eq({{expected}})
|
||||
end
|
||||
|
||||
# Short-hand, negated form of `#is_expected` that can be used for one-liner syntax.
|
||||
# For instance:
|
||||
# ```
|
||||
# it "is not 42" do
|
||||
# expect(subject).to_not eq(42)
|
||||
# end
|
||||
# ```
|
||||
# Can be shortened to:
|
||||
# ```
|
||||
# it is_not(42)
|
||||
# ```
|
||||
#
|
||||
# These three are functionally equivalent:
|
||||
# ```
|
||||
# expect(subject).to_not eq("foo")
|
||||
# is_expected.to_not eq("foo")
|
||||
# is_not("foo")
|
||||
# ```
|
||||
#
|
||||
# See also: `#is`
|
||||
macro is_not(expected)
|
||||
is_expected.to_not eq({{expected}})
|
||||
end
|
||||
|
||||
macro should(matcher)
|
||||
is_expected.to({{matcher}})
|
||||
end
|
||||
|
||||
macro should_not(matcher)
|
||||
is_expected.to_not({{matcher}})
|
||||
end
|
||||
|
||||
macro should_eventually(matcher)
|
||||
is_expected.to_eventually({{matcher}})
|
||||
end
|
||||
|
||||
macro should_never(matcher)
|
||||
is_expected.to_never({{matcher}})
|
||||
end
|
||||
|
||||
# Immediately fail the current test.
|
||||
# A reason can be passed,
|
||||
# which is reported in the output.
|
||||
def fail(reason : String)
|
||||
raise ExampleFailed.new(reason)
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
@[AlwaysInline]
|
||||
def fail
|
||||
fail("Example failed")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
module Spectator
|
||||
# Exception that indicates an example failed and should abort.
|
||||
class ExampleFailed < Exception
|
||||
end
|
||||
end
|
|
@ -1,15 +0,0 @@
|
|||
require "./example_failed"
|
||||
|
||||
module Spectator
|
||||
# Exception that indicates a required expectation was not met in an example.
|
||||
class ExpectationFailed < ExampleFailed
|
||||
# Expectation that failed.
|
||||
getter expectation : Expectations::Expectation
|
||||
|
||||
# Creates the exception.
|
||||
# The exception string is generated from the expecation message.
|
||||
def initialize(@expectation)
|
||||
super(@expectation.failure_message)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,6 +6,7 @@ require "./dsl"
|
|||
# This type is intentionally outside the `Spectator` module.
|
||||
# The reason for this is to prevent name collision when using the DSL to define a spec.
|
||||
class SpectatorTestContext < SpectatorContext
|
||||
include ::Spectator::DSL::Assertions
|
||||
include ::Spectator::DSL::Examples
|
||||
include ::Spectator::DSL::Groups
|
||||
include ::Spectator::DSL::Hooks
|
||||
|
|
Loading…
Reference in a new issue