Implement an example filter that supports matching groups

Addresses https://gitlab.com/arctic-fox/spectator/-/issues/25 and 
https://github.com/icy-arctic-fox/spectator/issues/24
This commit is contained in:
Michael Miller 2021-08-17 14:09:37 -06:00
parent 65799fdd3b
commit 94d5c96e7d
No known key found for this signature in database
GPG key ID: FB9F12F7C646A4AD
2 changed files with 87 additions and 2 deletions

View file

@ -1,7 +1,7 @@
require "./config/*" require "./config/*"
require "./node_filter" require "./node_filter"
require "./example_group" require "./example_group"
require "./example_iterator" require "./filtered_example_iterator"
require "./formatting/formatter" require "./formatting/formatter"
require "./run_flags" require "./run_flags"
@ -86,7 +86,7 @@ module Spectator
# Creates an iterator configured to select the filtered examples. # Creates an iterator configured to select the filtered examples.
def iterator(group : ExampleGroup) def iterator(group : ExampleGroup)
ExampleIterator.new(group).select(@node_filter) FilteredExampleIterator.new(group, @node_filter)
end end
# Retrieves the configured random number generator. # Retrieves the configured random number generator.

View file

@ -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