diff --git a/src/spectator/config.cr b/src/spectator/config.cr index cd7175a..ca691b0 100644 --- a/src/spectator/config.cr +++ b/src/spectator/config.cr @@ -1,7 +1,7 @@ require "./config/*" require "./node_filter" require "./example_group" -require "./example_iterator" +require "./filtered_example_iterator" require "./formatting/formatter" require "./run_flags" @@ -86,7 +86,7 @@ module Spectator # Creates an iterator configured to select the filtered examples. def iterator(group : ExampleGroup) - ExampleIterator.new(group).select(@node_filter) + FilteredExampleIterator.new(group, @node_filter) end # Retrieves the configured random number generator. diff --git a/src/spectator/filtered_example_iterator.cr b/src/spectator/filtered_example_iterator.cr new file mode 100644 index 0000000..3286f3b --- /dev/null +++ b/src/spectator/filtered_example_iterator.cr @@ -0,0 +1,85 @@ +require "./example" +require "./node" +require "./node_filter" +require "./node_iterator" + +module Spectator + # Iterates through selected nodes in a group and its nested groups. + # Nodes are iterated in pre-order. + class FilteredExampleIterator + include Iterator(Example) + + # A stack is used to track where in the tree this iterator is. + @stack = Deque(Node).new(1) + + # A queue stores forced examples that have been matched by the a parent group. + @queue = Deque(Example).new + + # Creates a new iterator. + # The *group* is the example group to iterate through. + # The *filter* selects which examples (and groups) to iterate through. + def initialize(@group : Node, @filter : NodeFilter) + @stack.push(@group) + end + + # Retrieves the next selected `Example`. + # If there are none left, then `Iterator::Stop` is returned. + def next + # Return items from the queue first before continuing to the stack. + return @queue.shift unless @queue.empty? + + # Keep going until either: + # a. a suitable example is found. + # b. the stack is empty. + until @stack.empty? + # Retrieve the next node. + node = @stack.pop + + # If the node is a group, conditionally traverse it. + if node.is_a?(Indexable(Node)) + # To traverse, a child node or the group itself must match the filter. + return node if node = next_group_match(node) + elsif node.is_a?(Example) && @filter.includes?(node) + return node + end + end + + # Nothing left to iterate. + stop + end + + # Restart the iterator at the beginning. + def rewind + @stack.clear + @stack.push(@group) + @queue.clear + self + end + + # Attempts to find the next matching example in a group. + # If any child in the group matches, then traversal on the stack (down the tree) continues. + # However, if no children match, but the group itself does, then all examples in the group match. + # In the latter scenario, the examples are added to the queue, and the next item from the queue returned. + # Stack iteration should continue if nil is returned. + private def next_group_match(group : Indexable(Node)) : Example? + # Look for any children that match. + iterator = NodeIterator.new(group) + + # Check if any children match. + # Skip first node because its the group being checked. + if iterator.skip(1).any?(@filter) + # Add the group's direct children to the queue + # in reverse order so that the tree is traversed in pre-order. + group.reverse_each { |node| @stack.push(node) } + + # Check if the group matches, but no children match. + elsif @filter.includes?(group) + # Add examples from the group to the queue. + # Return the next example from the queue. + iterator.rewind.select(Example).each { |node| @queue.push(node) } + @queue.shift unless @queue.empty? + # If the queue is empty (group has no examples), go to next loop iteration of the stack. + end + end + end +end