From f25eb9164f479edf3e55c26fce442e0e19695f54 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Sep 2019 09:58:15 -0600 Subject: [PATCH 1/4] Some test creation rewrite --- src/spectator/dsl/structure_dsl.cr | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/spectator/dsl/structure_dsl.cr b/src/spectator/dsl/structure_dsl.cr index dce58d4..05113b6 100644 --- a/src/spectator/dsl/structure_dsl.cr +++ b/src/spectator/dsl/structure_dsl.cr @@ -1416,17 +1416,20 @@ module Spectator::DSL # Create the wrapper class for the test code. {% if block.is_a?(Nop) %} {% if what.is_a?(Call) %} - _spectator_test(Test%example, %run) do + def %run {{what}} end {% else %} {% raise "Unrecognized syntax: `it #{what}`" %} {% end %} {% else %} - _spectator_test(Test%example, %run) {{block}} + def %run + {{block}} + end {% end %} - # TODO + %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) + ::Spectator::DSL::Builder.add_example({{what.stringify}}, %source, {{@type.name}}) { |test| test.as({{@type.name}}).%run } end # Creates an example, or a test case. From e304224bd6ba563e10c3eede16892f6cc128609f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Sep 2019 09:59:25 -0600 Subject: [PATCH 2/4] Move stack handling to its own type --- src/spectator/builders/example_group_stack.cr | 22 ++++++++ src/spectator/dsl/builder.cr | 52 +++++++------------ 2 files changed, 40 insertions(+), 34 deletions(-) create mode 100644 src/spectator/builders/example_group_stack.cr diff --git a/src/spectator/builders/example_group_stack.cr b/src/spectator/builders/example_group_stack.cr new file mode 100644 index 0000000..cf91a45 --- /dev/null +++ b/src/spectator/builders/example_group_stack.cr @@ -0,0 +1,22 @@ +module Spectator::Builders + struct ExampleGroupStack + getter root = RootExampleGroupBuilder.new + + @stack = Deque(ExampleGroupBuilder).new(1, @root) + + def current + @stack.last + end + + def push(group : NestedExampleGroupBuilder) + current.add_child(group) + @stack.push(group) + end + + def pop + raise "Attempted to pop root example group from stack" if current == root + + @stack.pop + end + end +end diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr index a836559..b815dce 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -6,26 +6,7 @@ module Spectator::DSL module Builder extend self - # Root group that contains all examples and groups in the spec. - private class_getter root_group = RootExampleGroupBuilder.new - - # Stack for tracking the current group the spec is working in. - # The last item (top of the stack) is the current group. - # The first item (bottom of the stack) is the root group (`#root_group`). - # The root group should never be popped. - @@group_stack = Array(ExampleGroupBuilder).new(1, root_group) - - # Retrieves the current group the spec is working in. - private def current_group - @@group_stack.last - end - - # Adds a new group to the stack. - # Calling this method indicates the spec has entered a nested group. - private def push_group(group : NestedExampleGroupBuilder) - current_group.add_child(group) - @@group_stack.push(group) - end + @@stack = Builders::ExampleGroupStack.new # Begins a new nested group in the spec. # A corresponding `#end_group` call must be made @@ -34,7 +15,7 @@ module Spectator::DSL # as arguments to this method are passed directly to it. def start_group(*args) : Nil group = NestedExampleGroupBuilder.new(*args) - push_group(group) + @@stack.push(group) end # Begins a new sample group in the spec - @@ -45,7 +26,7 @@ module Spectator::DSL # as arguments to this method are passed directly to it. def start_sample_group(*args) : Nil group = SampleExampleGroupBuilder.new(*args) - push_group(group) + @@stack.push(group) end # Marks the end of a group in the spec. @@ -53,58 +34,61 @@ module Spectator::DSL # It is also important to line up the start and end calls. # Otherwise examples might get placed into wrong groups. def end_group : Nil - @@group_stack.pop + @@stack.pop end # Adds an example type to the current group. # The class name of the example should be passed as an argument. # The example will be instantiated later. - def add_example(example_type : Example.class) : Nil - factory = ExampleFactory.new(example_type) - current_group.add_child(factory) + def add_example(description : String, source : Source, + example_type : ::SpectatorTest.class, &runner : ::SpectatorTest ->) : Nil + builder = ->{ example_type.new.as(::SpectatorTest) } + wrapper = TestWrapper.new(description, source, builder, runner) + example = Example.new(current_group, wrapper) + # TODO: Add to stack. end # Adds a block of code to run before all examples in the current group. def add_before_all_hook(&block : ->) : Nil - current_group.add_before_all_hook(block) + @@stack.current.add_before_all_hook(block) end # Adds a block of code to run before each example in the current group. def add_before_each_hook(&block : ->) : Nil - current_group.add_before_each_hook(block) + @@stack.current.add_before_each_hook(block) end # Adds a block of code to run after all examples in the current group. def add_after_all_hook(&block : ->) : Nil - current_group.add_after_all_hook(block) + @@stack.current.add_after_all_hook(block) end # Adds a block of code to run after each example in the current group. def add_after_each_hook(&block : ->) : Nil - current_group.add_after_each_hook(block) + @@stack.current.add_after_each_hook(block) end # Adds a block of code to run before and after each example in the current group. # The block of code will be given another proc as an argument. # It is expected that the block will call the proc. def add_around_each_hook(&block : Proc(Nil) ->) : Nil - current_group.add_around_each_hook(block) + @@stack.current.add_around_each_hook(block) end # Adds a pre-condition to run at the start of every example in the current group. def add_pre_condition(&block : ->) : Nil - current_group.add_pre_condition(block) + @@stack.current.add_pre_condition(block) end # Adds a post-condition to run at the end of every example in the current group. def add_post_condition(&block : ->) : Nil - current_group.add_post_condition(block) + @@stack.current.add_post_condition(block) end # Builds the entire spec and returns it as a test suite. # This should be called only once after the entire spec has been defined. protected def build(filter : ExampleFilter) : TestSuite - group = root_group.build(Internals::SampleValues.empty) + group = @@stack.root.build(Internals::SampleValues.empty) TestSuite.new(group, filter) end end From de8f298676b65390eb26c423850363b15d4dd358 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Sep 2019 10:28:02 -0600 Subject: [PATCH 3/4] Remove hooks, conditions, and sample vlues (for now) Removed only from examples and example groups. --- src/spectator/example_group.cr | 74 +-------------------------- src/spectator/nested_example_group.cr | 66 +----------------------- src/spectator/runnable_example.cr | 20 -------- 3 files changed, 2 insertions(+), 158 deletions(-) diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index c4708b2..2d6b28e 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -15,13 +15,8 @@ module Spectator include Enumerable(ExampleComponent) include Iterable(ExampleComponent) - getter sample_values : Internals::SampleValues - # Creates the example group. - # The hooks are stored to be triggered later. - def initialize(@hooks : ExampleHooks, @conditions : ExampleConditions, @sample_values) - @before_all_hooks_run = false - @after_all_hooks_run = false + def initialize end # Retrieves the children in the group. @@ -123,72 +118,5 @@ module Spectator def finished? children.all?(&.finished?) end - - # Runs all of the "before-each" and "before-all" hooks. - # This should run prior to every example in the group. - def run_before_hooks - run_before_all_hooks - run_before_each_hooks - end - - # Runs all of the "before-all" hooks. - # This should run prior to any examples in the group. - # The hooks will be run only once. - # Subsequent calls to this method will do nothing. - protected def run_before_all_hooks : Nil - return if @before_all_hooks_run - @hooks.run_before_all - @before_all_hooks_run = true - end - - # Runs all of the "before-each" hooks. - # This method should run prior to every example in the group. - protected def run_before_each_hooks : Nil - @hooks.run_before_each - end - - # Runs all of the "after-all" and "after-each" hooks. - # This should run following every example in the group. - def run_after_hooks - run_after_each_hooks - run_after_all_hooks - end - - # Runs all of the "after-all" hooks. - # This should run following all examples in the group. - # The hooks will be run only once, - # and only after all examples in the group have finished. - # Subsequent calls after the hooks have been run will do nothing. - protected def run_after_all_hooks(ignore_unfinished = false) : Nil - return if @after_all_hooks_run - return unless ignore_unfinished || finished? - - @hooks.run_after_all - @after_all_hooks_run = true - end - - # Runs all of the "after-each" hooks. - # This method should run following every example in the group. - protected def run_after_each_hooks : Nil - @hooks.run_after_each - end - - # Creates a proc that runs the "around-each" hooks - # in addition to a block passed to this method. - # To call the block and all "around-each" hooks, - # just invoke `Proc#call` on the returned proc. - def wrap_around_each_hooks(&block : ->) : -> - @hooks.wrap_around_each(&block) - end - - # Runs all of the pre-conditions for an example. - def run_pre_conditions - @conditions.run_pre_conditions - end - - # Runs all of the post-conditions for an example. - def run_post_conditions - @conditions.run_post_conditions - end end end diff --git a/src/spectator/nested_example_group.cr b/src/spectator/nested_example_group.cr index 2bd1e09..00f16e9 100644 --- a/src/spectator/nested_example_group.cr +++ b/src/spectator/nested_example_group.cr @@ -18,8 +18,7 @@ module Spectator # The parent's children must contain this group, # otherwise there may be unexpected behavior. # The *hooks* are stored to be triggered later. - def initialize(@what, @parent, hooks : ExampleHooks, conditions : ExampleConditions, sample_values) - super(hooks, conditions, sample_values) + def initialize(@what, @parent) end # Indicates wheter the group references a type. @@ -27,69 +26,6 @@ module Spectator @what.is_a?(Symbol) end - # Runs all of the "before-all" hooks. - # This should run prior to any examples in the group. - # The hooks will be run only once. - # Subsequent calls to this method will do nothing. - # Parent "before-all" hooks will be run first. - protected def run_before_all_hooks : Nil - parent.run_before_all_hooks - super - end - - # Runs all of the "before-each" hooks. - # This method should run prior to every example in the group. - # Parent "before-each" hooks will be run first. - protected def run_before_each_hooks : Nil - parent.run_before_each_hooks - super - end - - # Runs all of the "after-all" hooks. - # This should run following all examples in the group. - # The hooks will be run only once, - # and only after all examples in the group have finished. - # Subsequent calls after the hooks have been run will do nothing. - # Parent "after-all" hooks will be run last. - protected def run_after_all_hooks(ignore_unfinished = false) : Nil - super - parent.run_after_all_hooks(ignore_unfinished) - end - - # Runs all of the "after-each" hooks. - # This method should run following every example in the group. - # Parent "after-each" hooks will be run last. - protected def run_after_each_hooks : Nil - super - parent.run_after_each_hooks - end - - # Creates a proc that runs the "around-each" hooks - # in addition to a block passed to this method. - # To call the block and all `around_each` hooks, - # just invoke `Proc#call` on the returned proc. - # Parent "around-each" hooks will be in the outermost wrappings. - def wrap_around_each_hooks(&block : ->) : -> - wrapper = super(&block) - parent.wrap_around_each_hooks(&wrapper) - end - - # Runs all of the pre-condition checks. - # This method should run prior to every example in the group. - # Parent pre-conditions will be checked first. - def run_pre_conditions : Nil - parent.run_pre_conditions - super - end - - # Runs all of the post-condition checks. - # This method should run following every example in the group. - # Parent post-conditions will be checked last. - def run_post_conditions : Nil - super - parent.run_post_conditions - end - # Creates a string representation of the group. # The string consists of `#what` appended to the parent. # This results in a string like: diff --git a/src/spectator/runnable_example.cr b/src/spectator/runnable_example.cr index 2879cb1..0809291 100644 --- a/src/spectator/runnable_example.cr +++ b/src/spectator/runnable_example.cr @@ -17,22 +17,6 @@ module Spectator # Runs the actual test code. private abstract def run_instance - # Runs the hooks that should be performed before starting the test code. - private def run_before_hooks - group.run_before_hooks - rescue ex - # If an error occurs in the before hooks, skip running the example. - raise Exception.new("Error encountered while running before hooks", ex) - end - - # Runs the hooks that should be performed after the test code finishes. - private def run_after_hooks - group.run_after_hooks - rescue ex - # If an error occurs in the after hooks, elevate it to abort testing. - raise Exception.new("Error encountered while running after hooks", ex) - end - # Runs all hooks and the example code. # A captured result is returned. private def capture_result @@ -40,9 +24,7 @@ module Spectator # Get the proc that will call around-each hooks and the example. wrapper = wrap_run_example(result) - run_before_hooks run_wrapper(wrapper) - run_after_hooks end end @@ -70,9 +52,7 @@ module Spectator # Capture how long it takes to run the test code. result.elapsed = Time.measure do begin - group.run_pre_conditions run_instance # Actually run the example code. - group.run_post_conditions rescue ex # Catch all errors and handle them later. result.error = ex end From a178db05ac42e11fd5806ade67c8e6f4af7c4b31 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Sep 2019 10:38:26 -0600 Subject: [PATCH 4/4] Use TestWrapper in Example classes --- src/spectator/example.cr | 14 ++++++++++---- src/spectator/example_component.cr | 1 + src/spectator/pending_example.cr | 2 +- src/spectator/runnable_example.cr | 11 +++-------- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index d8185ef..d7c2d18 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -2,7 +2,7 @@ require "./example_component" module Spectator # Base class for all types of examples. - # Concrete types must implement the `#run_impl, `#what`, `#instance`, and `#source` methods. + # Concrete types must implement the `#run_impl` method. abstract class Example < ExampleComponent # Indicates whether the example has already been run. getter? finished = false @@ -11,10 +11,16 @@ module Spectator getter group : ExampleGroup # Retrieves the internal wrapped instance. - abstract def instance + private getter @test_wrapper : TestWrapper # Source where the example originated from. - abstract def source : Source + def source + @test_wrapper.source + end + + def what + @test_wrapper.description + end # Runs the example code. # A result is returned, which represents the outcome of the test. @@ -31,7 +37,7 @@ module Spectator # Creates the base of the example. # The group should be the example group the example belongs to. - def initialize(@group) + def initialize(@group, @test_wrapper) end # Indicates there is only one example to run. diff --git a/src/spectator/example_component.cr b/src/spectator/example_component.cr index 9c9fa95..a6aa9d9 100644 --- a/src/spectator/example_component.cr +++ b/src/spectator/example_component.cr @@ -3,6 +3,7 @@ module Spectator # This is used as the base node type for the composite design pattern. abstract class ExampleComponent # Text that describes the context or test. + # TODO: Rename to description. abstract def what : String # Indicates whether the example (or group) has been completely run. diff --git a/src/spectator/pending_example.cr b/src/spectator/pending_example.cr index f485ec4..30dc99d 100644 --- a/src/spectator/pending_example.cr +++ b/src/spectator/pending_example.cr @@ -3,7 +3,7 @@ require "./example" module Spectator # Common class for all examples marked as pending. # This class will not run example code. - abstract class PendingExample < Example + class PendingExample < Example # Returns a pending result. private def run_impl PendingResult.new(self) diff --git a/src/spectator/runnable_example.cr b/src/spectator/runnable_example.cr index 0809291..7964ff1 100644 --- a/src/spectator/runnable_example.cr +++ b/src/spectator/runnable_example.cr @@ -1,11 +1,9 @@ require "./example" module Spectator - # Common base for all examples that can be run. - # This class includes all the logic for running example hooks, + # Includes all the logic for running example hooks, # the example code, and capturing a result. - # Sub-classes need to implement the `#what` and `#run_instance` methods. - abstract class RunnableExample < Example + class RunnableExample < Example # Runs the example, hooks, and captures the result # and translates to a usable result. def run_impl @@ -14,9 +12,6 @@ module Spectator translate_result(result, expectations) end - # Runs the actual test code. - private abstract def run_instance - # Runs all hooks and the example code. # A captured result is returned. private def capture_result @@ -52,7 +47,7 @@ module Spectator # Capture how long it takes to run the test code. result.elapsed = Time.measure do begin - run_instance # Actually run the example code. + test_wrapper.run {} # Actually run the example code. rescue ex # Catch all errors and handle them later. result.error = ex end