diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 628f0fc..91d196a 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -1,3 +1,4 @@ +require "../context" require "../source" require "./builder" @@ -8,11 +9,23 @@ module Spectator::DSL # The *name* is the name given to the macro. # TODO: Mark example as pending if block is omitted. macro define_example(name) + # Defines an example. + # + # If a block is given, it is treated as the code to test. + # The block is provided the current example instance as an argument. + # + # The first argument names the example (test). + # Typically, this specifies what is being tested. + # It has no effect on the test and is purely used for output. + # If omitted, a name is generated from the first assertion in the test. + # + # The example will be marked as pending if the block is omitted. + # A block or name must be provided. macro {{name.id}}(what = nil, &block) \{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %} \{% raise "A description or block must be provided. Cannot use '{{name.id}}' alone." unless what || block %} - def \%test # TODO: Pass example instance. + def \%test : Nil # TODO: Pass example instance. \{{block.body}} end @@ -27,7 +40,7 @@ module Spectator::DSL # Inserts the correct representation of a example's name. # If *what* is a string, then it is dropped in as-is. # For anything else, it is stringified. - # This is intended to be used to convert a description from the spec DSL to `ExampleNode#name`. + # This is intended to be used to convert a description from the spec DSL to `SpecNode#name`. private macro _spectator_example_name(what) {% if what.is_a?(StringLiteral) || what.is_a?(StringInterpolation) || @@ -38,77 +51,12 @@ module Spectator::DSL {% end %} end - # Defines an example. - # - # If a block is given, it is treated as the code to test. - # The block is provided the current example instance as an argument. - # - # The first argument names the example (test). - # Typically, this specifies what is being tested. - # It has no effect on the test and is purely used for output. - # If omitted, a name is generated from the first assertion in the test. - # - # The example will be marked as pending if the block is omitted. - # A block or name must be provided. define_example :example - # Defines an example. - # - # If a block is given, it is treated as the code to test. - # The block is provided the current example instance as an argument. - # - # The first argument names the example (test). - # Typically, this specifies what is being tested. - # It has no effect on the test and is purely used for output. - # If omitted, a name is generated from the first assertion in the test. - # - # The example will be marked as pending if the block is omitted. - # A block or name must be provided. define_example :it - # Defines an example. - # - # If a block is given, it is treated as the code to test. - # The block is provided the current example instance as an argument. - # - # The first argument names the example (test). - # Typically, this specifies what is being tested. - # It has no effect on the test and is purely used for output. - # If omitted, a name is generated from the first assertion in the test. - # - # The example will be marked as pending if the block is omitted. - # A block or name must be provided. define_example :specify + + # TODO: pending, skip, and xit end - - macro pending(description = nil, _source_file = __FILE__, _source_line = __LINE__, &block) - {% if block.is_a?(Nop) %} - {% if description.is_a?(Call) %} - def %run - {{description}} - end - {% else %} - {% raise "Unrecognized syntax: `pending #{description}` at #{_source_file}:#{_source_line}" %} - {% end %} - {% else %} - def %run - {{block.body}} - end - {% end %} - - %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - ::Spectator::SpecBuilder.add_pending_example( - {{description.is_a?(StringLiteral) || description.is_a?(StringInterpolation) || description.is_a?(NilLiteral) ? description : description.stringify}}, - %source, - {{@type.name}} - ) { |test| test.as({{@type.name}}).%run } - end - - macro skip(description = nil, &block) - pending({{description}}) {{block}} - end - - 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 4a4c0f7..6bc8b47 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -32,7 +32,7 @@ module Spectator::DSL # If *what* appears to be a type name, it will be symbolized. # If it's a string, then it is dropped in as-is. # For anything else, it is stringified. - # This is intended to be used to convert a description from the spec DSL to `ExampleNode#name`. + # This is intended to be used to convert a description from the spec DSL to `SpecNode#name`. private macro _spectator_group_name(what) {% if (what.is_a?(Generic) || what.is_a?(Path) || @@ -85,107 +85,7 @@ module Spectator::DSL define_example_group :describe define_example_group :context + + # TODO: sample, random_sample, and given end - - macro sample(collection, count = nil, _source_file = __FILE__, _source_line = __LINE__, &block) - {% name = block.args.empty? ? :value.id : block.args.first.id %} - - def %collection - {{collection}} - end - - def %to_a - {% if count %} - %collection.first({{count}}) - {% else %} - %collection.to_a - {% end %} - end - - class Context%sample < {{@type.id}} - %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - ::Spectator::SpecBuilder.start_sample_group({{collection.stringify}}, %source, :%sample, {{name.stringify}}) do |values| - sample = {{@type.id}}.new(values) - sample.%to_a - end - - def {{name}} - @spectator_test_values.get_value(:%sample, typeof(%to_a.first)) - end - - {{block.body}} - - ::Spectator::SpecBuilder.end_group - end - end - - macro random_sample(collection, count = nil, _source_file = __FILE__, _source_line = __LINE__, &block) - {% name = block.args.empty? ? :value.id : block.args.first.id %} - - def %collection - {{collection}} - end - - def %to_a - {% if count %} - %collection.first({{count}}) - {% else %} - %collection.to_a - {% end %} - end - - class Context%sample < {{@type.id}} - %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) - ::Spectator::SpecBuilder.start_sample_group({{collection.stringify}}, %source, :%sample, {{name.stringify}}) do |values| - sample = {{@type.id}}.new(values) - collection = sample.%to_a - {% if count %} - collection.sample({{count}}, ::Spectator.random) - {% else %} - collection.shuffle(::Spectator.random) - {% end %} - end - - def {{name}} - @spectator_test_values.get_value(:%sample, typeof(%to_a.first)) - end - - {{block.body}} - - ::Spectator::SpecBuilder.end_group - end - end - - macro given(*assignments, &block) - context({{assignments.splat.stringify}}) do - {% for assignment in assignments %} - let({{assignment.target}}) { {{assignment.value}} } - {% end %} - - {% # Trick to get the contents of the block as an array of nodes. -# If there are multiple expressions/statements in the block, -# then the body will be a `Expressions` type. -# If there's only one expression, then the body is just that. - body = if block.is_a?(Nop) - raise "Missing block for 'given'" - elsif block.body.is_a?(Expressions) - # Get the expressions, which is already an array. - block.body.expressions - else - # Wrap the expression in an array. - [block.body] - end %} - - {% for item in body %} - # If the item starts with "it", then leave it as-is. - # Otherwise, prefix it with "it" - # and treat it as the one-liner "it" syntax. - {% if item.is_a?(Call) && item.name == :it.id %} - {{item}} - {% else %} - it {{item}} - {% end %} - {% end %} - end - end end diff --git a/src/spectator/dsl/hooks.cr b/src/spectator/dsl/hooks.cr index 8cb4d06..e84df19 100644 --- a/src/spectator/dsl/hooks.cr +++ b/src/spectator/dsl/hooks.cr @@ -1,55 +1,61 @@ +require "../source" require "./builder" module Spectator::DSL # DSL methods for adding custom logic to key times of the spec execution. module Hooks - # Defines code to run before any and all examples in an example group. - macro before_all(&block) - {% raise "Cannot use 'before_all' inside of a test block" if @def %} + # Defines a macro to create an example group hook. + # The *type* indicates when the hook runs and must be a method on `Spectator::DSL::Builder`. + macro define_example_group_hook(type) + macro {{type.id}}(&block) + \{% raise "Missing block for '{{type.id}}' hook" unless block %} + \{% raise "Cannot use '{{type.id}}' inside of a test block" if @def %} - def self.%hook : Nil - {{block.body}} + def self.\%hook : Nil + \{{block.body}} + end + + ::Spectator::DSL::Builder.{{type.id}}( + ::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}}) + ) { \{{@type.name}}.\%hook } end - - ::Spectator::DSL::Builder.before_all( - ::Spectator::Source.new({{block.filename}}, {{block.line_number}}) - ) { {{@type.name}}.%hook } end - macro before_each(&block) - {% raise "Cannot use 'before_each' inside of a test block" if @def %} + # Defines a macro to create an example hook. + # The *type* indicates when the hook runs and must be a method on `Spectator::DSL::Builder`. + macro define_example_hook(type) + macro {{type.id}}(&block) + \{% raise "Missing block for '{{type.id}}' hook" unless block %} + \{% raise "Cannot use '{{type.id}}' inside of a test block" if @def %} - def %hook : Nil - {{block.body}} + def \%hook : Nil # TODO: Pass example instance. + \{{block.body}} + end + + ::Spectator::DSL::Builder.{{type.id}}( + ::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}}) + ) { |example| example.with_context(\{{@type.name}}) { \%hook } } end - - ::Spectator::DSL::Builder.before_each( - ::Spectator::Source.new({{block.filename}}, {{block.line_number}}) - ) { |example| example.with_context({{@type.name}}) { %hook } } end - macro after_all(&block) - {% raise "Cannot use 'after_all' inside of a test block" if @def %} + # Defines a block of code that will be invoked once before any examples in the group. + # The block will not run in the context of the current running example. + # This means that values defined by `let` and `subject` are not available. + define_example_group_hook :before_all - def self.%hook : Nil - {{block.body}} - end + # Defines a block of code that will be invoked once after all examples in the group. + # The block will not run in the context of the current running example. + # This means that values defined by `let` and `subject` are not available. + define_example_group_hook :after_all - ::Spectator::DSL::Builder.after_all( - ::Spectator::Source.new({{block.filename}}, {{block.line_number}}) - ) { {{@type.name}}.%hook } - end + # Defines a block of code that will be invoked before every example in the group. + # The block will be run in the context of the current running example. + # This means that values defined by `let` and `subject` are available. + define_example_hook :before_each - macro after_each(&block) - {% raise "Cannot use 'after_each' inside of a test block" if @def %} - - def %hook : Nil - {{block.body}} - end - - ::Spectator::DSL::Builder.after_each( - ::Spectator::Source.new({{block.filename}}, {{block.line_number}}) - ) { |example| example.with_context({{@type.name}}) { %hook } } - end + # Defines a block of code that will be invoked after every example in the group. + # The block will be run in the context of the current running example. + # This means that values defined by `let` and `subject` are available. + define_example_hook :after_each end end diff --git a/src/spectator/dsl/top.cr b/src/spectator/dsl/top.cr index d6682b6..467b415 100644 --- a/src/spectator/dsl/top.cr +++ b/src/spectator/dsl/top.cr @@ -16,9 +16,7 @@ module Spectator::DSL # # Your examples for `Foo` go here. # end # ``` - # NOTE: Inside the block, the `Spectator` prefix is no longer needed. - # Actually, prefixing methods and macros with `Spectator` - # most likely won't work and can cause compiler errors. + # NOTE: Inside the block, the `Spectator` prefix _should not_ be used. macro {{method.id}}(description, &block) class ::SpectatorTestContext {{method.id}}(\{{description}}) \{{block}}