From 1b53607f8ecf0d084a5e6df6fcde3317a1246111 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 17 Jul 2021 14:01:27 -0600 Subject: [PATCH] Cleanup and add docs --- src/spectator/example_builder.cr | 15 ++++++- src/spectator/example_group_builder.cr | 34 +++++++++++++--- src/spectator/example_group_iteration.cr | 13 +++++++ .../iterative_example_group_builder.cr | 39 +++++++++++++------ src/spectator/node_builder.cr | 3 +- src/spectator/pending_example_builder.cr | 12 +++++- src/spectator/spec_builder.cr | 2 + 7 files changed, 99 insertions(+), 19 deletions(-) diff --git a/src/spectator/example_builder.cr b/src/spectator/example_builder.cr index d261992..c3cc01e 100644 --- a/src/spectator/example_builder.cr +++ b/src/spectator/example_builder.cr @@ -1,12 +1,25 @@ +require "./context" +require "./example" +require "./location" +require "./metadata" require "./node_builder" module Spectator + # Constructs examples. + # Call `#build` to produce an `Example`. class ExampleBuilder < NodeBuilder + # Creates the builder. + # A proc provided by *context_builder* is used to create a unique `Context` for each example produced by `#build`. + # The *entrypoint* indicates the proc used to invoke the test code in the example. + # The *name*, *location*, and *metadata* will be applied to the `Example` produced by `#build`. def initialize(@context_builder : -> Context, @entrypoint : Example ->, @name : String? = nil, @location : Location? = nil, @metadata : Metadata = Metadata.new) end - def build(parent) + # Constructs an example with previously defined attributes and context. + # The *parent* is an already constructed example group to nest the new example under. + # It can be nil if the new example won't have a parent. + def build(parent = nil) context = @context_builder.call Example.new(context, @entrypoint, @name, @location, parent, @metadata) end diff --git a/src/spectator/example_group_builder.cr b/src/spectator/example_group_builder.cr index e140203..0c3affa 100644 --- a/src/spectator/example_group_builder.cr +++ b/src/spectator/example_group_builder.cr @@ -1,6 +1,16 @@ +require "./example_group" +require "./example_group_hook" +require "./example_hook" +require "./example_procsy_hook" +require "./label" +require "./location" +require "./metadata" require "./node_builder" module Spectator + # Progressively constructs an example group. + # Hooks and builders for child nodes can be added over time to this builder. + # When done, call `#build` to produce an `ExampleGroup`. class ExampleGroupBuilder < NodeBuilder @children = [] of NodeBuilder @before_all_hooks = [] of ExampleGroupHook @@ -9,6 +19,9 @@ module Spectator @after_each_hooks = [] of ExampleHook @around_each_hooks = [] of ExampleProcsyHook + # Creates the builder. + # Initially, the builder will have no children and no hooks. + # The *name*, *location*, and *metadata* will be applied to the `ExampleGroup` produced by `#build`. def initialize(@name : Label = nil, @location : Location? = nil, @metadata : Metadata = Metadata.new) end @@ -68,19 +81,30 @@ module Spectator @around_each_hooks << ExampleProcsyHook.new(label: "around_each", &block) end + # Constructs an example group with previously defined attributes, children, and hooks. + # The *parent* is an already constructed example group to nest the new example group under. + # It can be nil if the new example group won't have a parent. def build(parent = nil) ExampleGroup.new(@name, @location, parent, @metadata).tap do |group| - @before_all_hooks.each { |hook| group.add_before_all_hook(hook) } - @before_each_hooks.each { |hook| group.add_before_each_hook(hook) } - @after_all_hooks.each { |hook| group.add_after_all_hook(hook) } - @after_each_hooks.each { |hook| group.add_after_each_hook(hook) } - @around_each_hooks.each { |hook| group.add_around_each_hook(hook) } + apply_hooks(group) @children.each(&.build(group)) end end + # Adds a child builder to the group. + # The *builder* will have `NodeBuilder#build` called on it from within `#build`. + # The new example group will be passed to it. def <<(builder) @children << builder end + + # Adds all previously configured hooks to an example group. + private def apply_hooks(group) + @before_all_hooks.each { |hook| group.add_before_all_hook(hook) } + @before_each_hooks.each { |hook| group.add_before_each_hook(hook) } + @after_all_hooks.each { |hook| group.add_after_all_hook(hook) } + @after_each_hooks.each { |hook| group.add_after_each_hook(hook) } + @around_each_hooks.each { |hook| group.add_around_each_hook(hook) } + end end end diff --git a/src/spectator/example_group_iteration.cr b/src/spectator/example_group_iteration.cr index d6995b8..d6576d2 100644 --- a/src/spectator/example_group_iteration.cr +++ b/src/spectator/example_group_iteration.cr @@ -1,9 +1,22 @@ require "./example_group" +require "./label" +require "./location" +require "./metadata" module Spectator + # Collection of examples and sub-groups for a single iteration of an iterative example group. class ExampleGroupIteration(T) < ExampleGroup + # Item for this iteration of the example groups. getter item : T + # Creates the example group iteration. + # The element for the current iteration is provided by *item*. + # The *name* describes the purpose of the group. + # It can be a `Symbol` to describe a type. + # This is typically a stringified form of *item*. + # 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 *metadata* can be used for filtering and modifying example behavior. def initialize(@item : T, name : Label = nil, location : Location? = nil, group : ExampleGroup? = nil, metadata : Metadata = Metadata.new) super(name, location, group, metadata) diff --git a/src/spectator/iterative_example_group_builder.cr b/src/spectator/iterative_example_group_builder.cr index 14c81ea..6bc866b 100644 --- a/src/spectator/iterative_example_group_builder.cr +++ b/src/spectator/iterative_example_group_builder.cr @@ -1,31 +1,48 @@ +require "./example_group" require "./example_group_builder" require "./example_group_iteration" +require "./location" +require "./metadata" module Spectator + # Progressively constructs an iterative example group. + # Hooks and builders for child nodes can be added over time to this builder. + # When done, call `#build` to produce an `ExampleGroup` with nested `ExampleGroupIteration` instances. class IterativeExampleGroupBuilder(T) < ExampleGroupBuilder + # Creates the builder. + # Initially, the builder will have no children and no hooks. + # The *name*, *location*, and *metadata* will be applied to the `ExampleGroup` produced by `#build`. + # The *collection* is the set of items to create sub-nodes for. + # The *iterator* is an optional name given to a single item in the collection. def initialize(@collection : Enumerable(T), name : String? = nil, @iterator : String? = nil, location : Location? = nil, metadata : Metadata = Metadata.new) super(name, location, metadata) end + # Constructs an iterative example group with previously defined attributes, children, and hooks. + # The *parent* is an already constructed example group to nest the new example group under. + # It can be nil if the new example group won't have a parent. def build(parent = nil) ExampleGroup.new(@name, @location, parent, @metadata).tap do |group| - @before_all_hooks.each { |hook| group.add_before_all_hook(hook) } - @before_each_hooks.each { |hook| group.add_before_each_hook(hook) } - @after_all_hooks.each { |hook| group.add_after_all_hook(hook) } - @after_each_hooks.each { |hook| group.add_after_each_hook(hook) } - @around_each_hooks.each { |hook| group.add_around_each_hook(hook) } + # Hooks are applied once to the outer group, + # instead of multiple times for each inner group (iteration). + apply_hooks(group) + @collection.each do |item| - name = if iterator = @iterator - "#{iterator}: #{item.inspect}" - else - item.inspect - end - ExampleGroupIteration.new(item, name, @location, group).tap do |iteration| + ExampleGroupIteration.new(item, iteration_name(item), @location, group).tap do |iteration| @children.each(&.build(iteration)) end end end end + + # Constructs the name of an example group iteration. + private def iteration_name(item) + if iterator = @iterator + "#{iterator}: #{item.inspect}" + else + item.inspect + end + end end end diff --git a/src/spectator/node_builder.cr b/src/spectator/node_builder.cr index 457f39f..939374b 100644 --- a/src/spectator/node_builder.cr +++ b/src/spectator/node_builder.cr @@ -1,5 +1,6 @@ module Spectator abstract class NodeBuilder - abstract def build(parent) + # Produces a node for a spec. + abstract def build(parent = nil) end end diff --git a/src/spectator/pending_example_builder.cr b/src/spectator/pending_example_builder.cr index 40880b1..c7a1a46 100644 --- a/src/spectator/pending_example_builder.cr +++ b/src/spectator/pending_example_builder.cr @@ -1,11 +1,21 @@ +require "./example" +require "./location" +require "./metadata" require "./node_builder" module Spectator + # Constructs pending examples. + # Call `#build` to produce an `Example`. class PendingExampleBuilder < NodeBuilder + # Creates the builder. + # The *name*, *location*, and *metadata* will be applied to the `Example` produced by `#build`. def initialize(@name : String? = nil, @location : Location? = nil, @metadata : Metadata = Metadata.new) end - def build(parent) + # Constructs an example with previously defined attributes. + # The *parent* is an already constructed example group to nest the new example under. + # It can be nil if the new example won't have a parent. + def build(parent = nil) Example.pending(@name, @location, parent, @metadata) end end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 43e93a5..7b63013 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -71,6 +71,8 @@ module Spectator # # The *collection* is the set of items to iterate over. # Child nodes in this group will be executed once for every item in the collection. + # The *name* should be a string representation of *collection*. + # The *iterator* is an optional name given to a single item in *collection*. # # The *location* optionally defined where the group originates in source code. #