diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 5d8b88b..9cc9c85 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -65,7 +65,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 `Spec::Node#name`. + # This is intended to be used to convert a description from the spec DSL to `Node#name`. private macro _spectator_example_name(what) {% if what.is_a?(StringLiteral) || what.is_a?(StringInterpolation) || diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 0fd2028..fa78ade 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -56,7 +56,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 `Spec::Node#name`. + # This is intended to be used to convert a description from the spec DSL to `Node#name`. private macro _spectator_group_name(what) {% if (what.is_a?(Generic) || what.is_a?(Path) || diff --git a/src/spectator/example.cr b/src/spectator/example.cr index b048469..08e7858 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -2,17 +2,25 @@ require "./example_context_delegate" require "./example_group" require "./harness" require "./location" +require "./node" require "./pending_result" require "./result" -require "./spec/node" require "./tags" module Spectator # Standard example that runs a test case. - class Example < Spec::Node + class Example < Node # Currently running example. class_getter! current : Example + # Group the node belongs to. + getter! group : ExampleGroup + + # Assigns the node to the specified *group*. + # This is an internal method and should only be called from `ExampleGroup`. + # `ExampleGroup` manages the association of nodes to groups. + protected setter group : ExampleGroup? + # Indicates whether the example already ran. getter? finished : Bool = false @@ -31,8 +39,11 @@ module Spectator # Note: The tags will not be merged with the parent tags. def initialize(@context : Context, @entrypoint : self ->, name : String? = nil, location : Location? = nil, - group : ExampleGroup? = nil, tags = Tags.new) - super(name, location, group, tags) + @group : ExampleGroup? = nil, tags = Tags.new) + super(name, location, tags) + + # Ensure group is linked. + group << self if group end # Creates a dynamic example. @@ -44,11 +55,15 @@ module Spectator # The example will be assigned to *group* if it is provided. # A set of *tags* can be used for filtering and modifying example behavior. # Note: The tags will not be merged with the parent tags. - def initialize(name : String? = nil, location : Location? = nil, group : ExampleGroup? = nil, - tags = Tags.new, &block : self ->) - super(name, location, group, tags) + def initialize(name : String? = nil, location : Location? = nil, + @group : ExampleGroup? = nil, tags = Tags.new, &block : self ->) + super(name, location, tags) + @context = NullContext.new @entrypoint = block + + # Ensure group is linked. + group << self if group end # Executes the test case. @@ -68,13 +83,13 @@ module Spectator begin @result = Harness.run do - group?.try(&.call_once_before_all) - if (parent = group?) + @group.try(&.call_once_before_all) + if (parent = @group) parent.call_around_each(self) { run_internal } else run_internal end - if (parent = group?) + if (parent = @group) parent.call_once_after_all if parent.finished? end end @@ -85,10 +100,10 @@ module Spectator end private def run_internal - group?.try(&.call_before_each(self)) + @group.try(&.call_before_each(self)) @entrypoint.call(self) @finished = true - group?.try(&.call_after_each(self)) + @group.try(&.call_after_each(self)) end # Executes code within the example's test context. @@ -124,26 +139,27 @@ module Spectator # Constructs the full name or description of the example. # This prepends names of groups this example is part of. def to_s(io) - if name? - super - else - io << "" + name = @name + + # Prefix with group's full name if the node belongs to a group. + if (parent = @group) + parent.to_s(io) + + # Add padding between the node names + # only if the names don't appear to be symbolic. + # Skip blank group names (like the root group). + io << ' ' unless !parent.name? || # ameba:disable Style/NegatedConditionsInUnless + (parent.name?.is_a?(Symbol) && name.is_a?(String) && + (name.starts_with?('#') || name.starts_with?('.'))) end + + super end # Exposes information about the example useful for debugging. def inspect(io) - # Full example name. - io << '"' - to_s(io) - io << '"' - - # Add location if it's available. - if (location = self.location) - io << " @ " - io << location - end - + super + io << ' ' io << result end @@ -153,6 +169,11 @@ module Spectator json.string(to_s) end + # Creates a procsy from this example and the provided block. + def procsy(&block : ->) + Procsy.new(self, &block) + end + # Wraps an example to behave like a `Proc`. # This is typically used for an *around_each* hook. # Invoking `#call` or `#run` will run the example. diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index c5f00c6..8031e20 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -1,71 +1,101 @@ require "./events" require "./example_procsy_hook" -require "./spec/node" +require "./node" module Spectator # Collection of examples and sub-groups. - class ExampleGroup < Spec::Node - include Enumerable(Spec::Node) + class ExampleGroup < Node + include Enumerable(Node) include Events - include Iterable(Spec::Node) + include Iterable(Node) - @nodes = [] of Spec::Node + @nodes = [] of Node - group_event before_all do |hooks| - Log.trace { "Processing before_all hooks for #{self}" } + # Parent group this group belongs to. + getter! group : ExampleGroup - if (parent = group?) - parent.call_once_before_all + # Assigns this group to the specified *group*. + # This is an internal method and should only be called from `ExampleGroup`. + # `ExampleGroup` manages the association of nodes to groups. + protected setter group : ExampleGroup? + + # Calls all hooks from the parent group if there is a parent. + # The *hook* is the method name of the group hook to invoke. + private macro call_parent_hooks(hook) + if (parent = @group) + parent.{{hook.id}} end + end + # Calls all hooks from the parent group if there is a parent. + # The *hook* is the method name of the example hook to invoke. + # The current *example* must be provided. + private macro call_parent_hooks(hook, example) + if (parent = @group) + parent.{{hook.id}}({{example}}) + end + end + + # Calls group hooks of the current group. + private def call_hooks(hooks) hooks.each do |hook| Log.trace { "Invoking hook #{hook}" } hook.call end end + # Calls example hooks of the current group. + # Requires the current *example*. + private def call_hooks(hooks, example) + hooks.each do |hook| + Log.trace { "Invoking hook #{hook}" } + hook.call(example) + end + end + + group_event before_all do |hooks| + Log.trace { "Processing before_all hooks for #{self}" } + + call_parent_hooks(:call_once_before_all) + call_hooks(hooks) + end + group_event after_all do |hooks| Log.trace { "Processing after_all hooks for #{self}" } - hooks.each do |hook| - Log.trace { "Invoking hook #{hook}" } - hook.call - end - - if (parent = group?) - parent.call_once_after_all - end + call_hooks(hooks) + call_parent_hooks(:call_once_after_all) end example_event before_each do |hooks, example| Log.trace { "Processing before_each hooks for #{self}" } - if (parent = group?) - parent.call_before_each(example) - end - - hooks.each do |hook| - Log.trace { "Invoking hook #{hook}" } - hook.call(example) - end + call_parent_hooks(:call_before_each, example) + call_hooks(hooks, example) end example_event after_each do |hooks, example| Log.trace { "Processing after_each hooks for #{self}" } - hooks.each do |hook| - Log.trace { "Invoking hook #{hook}" } - hook.call(example) - end + call_hooks(hooks, example) + call_parent_hooks(:call_after_each, example) + end - if (parent = group?) - parent.call_after_each(example) - end + # Creates the example group. + # The *name* describes the purpose of the group. + # It can be a `Symbol` to describe a type. + # The *location* tracks where the group exists in source code. + # This group will be assigned to the parent *group* if it is provided. + # A set of *tags* can be used for filtering and modifying example behavior. + def initialize(@name : Label = nil, @location : Location? = nil, + @group : ExampleGroup? = nil, @tags : Tags = Tags.new) + # Ensure group is linked. + group << self if group end # Removes the specified *node* from the group. # The node will be unassigned from this group. - def delete(node : Spec::Node) + def delete(node : Node) # Only remove from the group if it is associated with this group. return unless node.group == self @@ -88,11 +118,31 @@ module Spectator @nodes.all?(&.finished?) end + # Constructs the full name or description of the example group. + # This prepends names of groups this group is part of. + def to_s(io) + name = @name + + # Prefix with group's full name if the node belongs to a group. + if (parent = @group) + parent.to_s(io) + + # Add padding between the node names + # only if the names don't appear to be symbolic. + # Skip blank group names (like the root group). + io << ' ' unless !parent.name? || # ameba:disable Style/NegatedConditionsInUnless + (parent.name?.is_a?(Symbol) && name.is_a?(String) && + (name.starts_with?('#') || name.starts_with?('.'))) + end + + super + end + # Adds the specified *node* to the group. # Assigns the node to this group. # If the node already belongs to a group, # it will be removed from the previous group before adding it to this group. - def <<(node : Spec::Node) + def <<(node : Node) # Remove from existing group if the node is part of one. if (previous = node.group?) previous.delete(node) @@ -113,26 +163,26 @@ module Spectator # Defines a hook for the *around_each* event. # The block of code given to this method is invoked when the event occurs. # The current example is provided as a block argument. - def around_each(&block : Example::Procsy ->) : Nil + def around_each(&block) : Nil hook = ExampleProcsyHook.new(label: "around_each", &block) add_around_each_hook(hook) end # Signals that the *around_each* event has occurred. # All hooks associated with the event will be called. - def call_around_each(example : Example, &block : -> _) : Nil + def call_around_each(example, &block : -> _) : Nil # Avoid overhead if there's no hooks. return yield if @around_hooks.empty? # Start with a procsy that wraps the original code. - procsy = Example::Procsy.new(example, &block) + procsy = example.procsy(&block) procsy = wrap_around_each(procsy) procsy.call end # Wraps a procsy with the *around_each* hooks from this example group. # The returned procsy will call each hook then *procsy*. - protected def wrap_around_each(procsy : Example::Procsy) : Example::Procsy + protected def wrap_around_each(procsy) # Avoid overhead if there's no hooks. return procsy if @around_hooks.empty? diff --git a/src/spectator/example_iterator.cr b/src/spectator/example_iterator.cr index abcfd33..f28a8bf 100644 --- a/src/spectator/example_iterator.cr +++ b/src/spectator/example_iterator.cr @@ -1,6 +1,6 @@ require "./example" require "./example_group" -require "./spec/node" +require "./node" module Spectator # Iterates through all examples in a group and its nested groups. @@ -9,12 +9,12 @@ module Spectator # Stack that contains the iterators for each group. # A stack is used to track where in the tree this iterator is. - @stack : Array(Iterator(Spec::Node)) + @stack : Array(Iterator(Node)) # Creates a new iterator. # The *group* is the example group to iterate through. def initialize(@group : ExampleGroup) - iter = @group.each.as(Iterator(Spec::Node)) + iter = @group.each.as(Iterator(Node)) @stack = [iter] end @@ -39,7 +39,7 @@ module Spectator # Restart the iterator at the beginning. def rewind # Same code as `#initialize`, but return self. - iter = @group.each.as(Iterator(Spec::Node)) + iter = @group.each.as(Iterator(Node)) @stack = [iter] self end diff --git a/src/spectator/node.cr b/src/spectator/node.cr new file mode 100644 index 0000000..7a562e1 --- /dev/null +++ b/src/spectator/node.cr @@ -0,0 +1,69 @@ +require "./label" +require "./location" +require "./tags" + +module Spectator + # A single item in a test spec. + # This is commonly an `Example` or `ExampleGroup`, + # but can be anything that should be iterated over when running the spec. + abstract class Node + # Location of the node in source code. + getter! location : Location + + # User-provided name or description of the node. + # This does not include the group name or descriptions. + # Use `#to_s` to get the full name. + # + # This value will be nil if no name was provided. + # In this case, and the node is a runnable example, + # the name should be set to the description + # of the first matcher that runs in the test case. + # + # If this value is a `Symbol`, the user specified a type for the name. + getter! name : Label + + # Updates the name of the node. + protected def name=(@name : String) + end + + # User-defined keywords used for filtering and behavior modification. + getter tags : Tags + + # Creates the node. + # The *name* describes the purpose of the node. + # It can be a `Symbol` to describe a type. + # The *location* tracks where the node exists in source code. + # A set of *tags* can be used for filtering and modifying example behavior. + def initialize(@name : Label = nil, @location : Location? = nil, @tags : Tags = Tags.new) + end + + # Indicates whether the node has completed. + abstract def finished? : Bool + + # Checks if the node has been marked as pending. + # Pending items should be skipped during execution. + def pending? + tags.includes?(:pending) + end + + # Constructs the full name or description of the node. + # This prepends names of groups this node is part of. + def to_s(io) + (@name || "").to_s(io) + end + + # Exposes information about the node useful for debugging. + def inspect(io) + # Full node name. + io << '"' + to_s(io) + io << '"' + + # Add location if it's available. + if (location = self.location) + io << " @ " + io << location + end + end + end +end diff --git a/src/spectator/spec/node.cr b/src/spectator/spec/node.cr deleted file mode 100644 index 4bbde0c..0000000 --- a/src/spectator/spec/node.cr +++ /dev/null @@ -1,83 +0,0 @@ -require "../label" -require "../location" -require "../tags" - -module Spectator - class Spec - # A single item in a test spec. - # This is commonly an `Example` or `ExampleGroup`, - # but can be anything that should be iterated over when running the spec. - abstract class Node - # Location of the node in source code. - getter! location : Location - - # User-provided name or description of the node. - # This does not include the group name or descriptions. - # Use `#to_s` to get the full name. - # - # This value will be nil if no name was provided. - # In this case, and the node is a runnable example, - # the name should be set to the description - # of the first matcher that runs in the test case. - # - # If this value is a `Symbol`, the user specified a type for the name. - getter! name : Label - - # Updates the name of the node. - protected def name=(@name : String) - end - - # Group the node belongs to. - getter! group : ExampleGroup - - # User-defined keywords used for filtering and behavior modification. - getter tags : Tags - - # Assigns the node to the specified *group*. - # This is an internal method and should only be called from `ExampleGroup`. - # `ExampleGroup` manages the association of nodes to groups. - protected setter group : ExampleGroup? - - # Creates the node. - # The *name* describes the purpose of the node. - # It can be a `Symbol` to describe a type. - # The *location* tracks where the node exists in source code. - # The node will be assigned to *group* if it is provided. - # A set of *tags* can be used for filtering and modifying example behavior. - def initialize(@name : Label = nil, @location : Location? = nil, - group : ExampleGroup? = nil, @tags : Tags = Tags.new) - # Ensure group is linked. - group << self if group - end - - # Indicates whether the node has completed. - abstract def finished? : Bool - - # Checks if the node has been marked as pending. - # Pending items should be skipped during execution. - def pending? - tags.includes?(:pending) - end - - # Constructs the full name or description of the node. - # This prepends names of groups this node is part of. - def to_s(io) - name = @name - - # Prefix with group's full name if the node belongs to a group. - if (group = @group) - group.to_s(io) - - # Add padding between the node names - # only if the names don't appear to be symbolic. - # Skip blank group names (like the root group). - io << ' ' unless !group.name? || # ameba:disable Style/NegatedConditionsInUnless - (group.name?.is_a?(Symbol) && name.is_a?(String) && - (name.starts_with?('#') || name.starts_with?('.'))) - end - - name.to_s(io) - end - end - end -end