Skip to content
This repository was archived by the owner on Nov 30, 2024. It is now read-only.

Commit 6a4d625

Browse files
committed
Refactor on_existing_matching_groups.
- Use a functional style using recursion instead of mutating a `groups` array imperatively. I find a functional style easier to reason about. - Split off the hook-specific logic, but preserve the general traversal logic. - Remove duplication of `Metadata.build_hash_from`. - Only call `Metadata.build_hash_from` if needed. If we are dealing with an :example hook there is no need.
1 parent 9c0bff6 commit 6a4d625

File tree

3 files changed

+56
-20
lines changed

3 files changed

+56
-20
lines changed

lib/rspec/core/configuration.rb

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1749,8 +1749,7 @@ def before(scope=nil, *meta, &block)
17491749
handle_suite_hook(scope, meta) do
17501750
@before_suite_hooks << Hooks::BeforeHook.new(block, {})
17511751
end || begin
1752-
metadata = Metadata.build_hash_from(meta.dup)
1753-
on_existing_matching_groups(metadata, scope) { |g| g.before(scope, *meta, &block) }
1752+
add_hook_to_existing_matching_groups(meta, scope) { |g| g.before(scope, *meta, &block) }
17541753
super(scope, *meta, &block)
17551754
end
17561755
end
@@ -1773,8 +1772,7 @@ def prepend_before(scope=nil, *meta, &block)
17731772
handle_suite_hook(scope, meta) do
17741773
@before_suite_hooks.unshift Hooks::BeforeHook.new(block, {})
17751774
end || begin
1776-
metadata = Metadata.build_hash_from(meta.dup)
1777-
on_existing_matching_groups(metadata, scope) { |g| g.prepend_before(scope, *meta, &block) }
1775+
add_hook_to_existing_matching_groups(meta, scope) { |g| g.prepend_before(scope, *meta, &block) }
17781776
super(scope, *meta, &block)
17791777
end
17801778
end
@@ -1792,8 +1790,7 @@ def after(scope=nil, *meta, &block)
17921790
handle_suite_hook(scope, meta) do
17931791
@after_suite_hooks.unshift Hooks::AfterHook.new(block, {})
17941792
end || begin
1795-
metadata = Metadata.build_hash_from(meta.dup)
1796-
on_existing_matching_groups(metadata, scope) { |g| g.after(scope, *meta, &block) }
1793+
add_hook_to_existing_matching_groups(meta, scope) { |g| g.after(scope, *meta, &block) }
17971794
super(scope, *meta, &block)
17981795
end
17991796
end
@@ -1816,8 +1813,7 @@ def append_after(scope=nil, *meta, &block)
18161813
handle_suite_hook(scope, meta) do
18171814
@after_suite_hooks << Hooks::AfterHook.new(block, {})
18181815
end || begin
1819-
metadata = Metadata.build_hash_from(meta.dup)
1820-
on_existing_matching_groups(metadata, scope) { |g| g.append_after(scope, *meta, &block) }
1816+
add_hook_to_existing_matching_groups(meta, scope) { |g| g.append_after(scope, *meta, &block) }
18211817
super(scope, *meta, &block)
18221818
end
18231819
end
@@ -1826,8 +1822,7 @@ def append_after(scope=nil, *meta, &block)
18261822
#
18271823
# See {Hooks#around} for full `around` hook docs.
18281824
def around(scope=nil, *meta, &block)
1829-
metadata = Metadata.build_hash_from(meta.dup)
1830-
on_existing_matching_groups(metadata, scope) { |g| g.around(scope, *meta, &block) }
1825+
add_hook_to_existing_matching_groups(meta, scope) { |g| g.around(scope, *meta, &block) }
18311826
super(scope, *meta, &block)
18321827
end
18331828

@@ -2031,21 +2026,32 @@ def configure_group_with(group, module_list, application_method)
20312026
end
20322027
end
20332028

2034-
def on_existing_matching_groups(meta, scope=:ignore)
2035-
groups = world.example_groups.dup
2036-
2037-
until groups.empty?
2038-
group = groups.shift
2029+
def add_hook_to_existing_matching_groups(meta, scope, &block)
2030+
# For example hooks, we have to apply it to each of the top level
2031+
# groups, even if the groups do not match. When we apply it, we
2032+
# apply it with the metadata, so it will only apply to examples
2033+
# in the group that match the metadata.
2034+
# #2280 for background and discussion.
2035+
if scope == :example || scope == :each || scope.nil?
2036+
world.example_groups.each(&block)
2037+
else
2038+
meta = Metadata.build_hash_from(meta.dup)
2039+
on_existing_matching_groups(meta, &block)
2040+
end
2041+
end
20392042

2040-
if scope == :example || scope == :each || scope.nil? ||
2041-
meta.empty? || MetadataFilter.apply?(:any?, meta, group.metadata)
2042-
yield group
2043-
else
2044-
groups.concat(group.children)
2043+
def on_existing_matching_groups(meta)
2044+
world.traverse_example_group_trees_until do |group|
2045+
metadata_applies_to_group?(meta, group).tap do |applies|
2046+
yield group if applies
20452047
end
20462048
end
20472049
end
20482050

2051+
def metadata_applies_to_group?(meta, group)
2052+
meta.empty? || MetadataFilter.apply?(:any?, meta, group.metadata)
2053+
end
2054+
20492055
if RSpec::Support::RubyFeatures.module_prepends_supported?
20502056
def safe_prepend(mod, host)
20512057
host.__send__(:prepend, mod) unless host < mod

lib/rspec/core/example_group.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,20 @@ def self.children
450450
@children ||= []
451451
end
452452

453+
# @private
454+
# Traverses the tree of groups, starting with `self`, then the children, recursively.
455+
# Halts the traversal of a branch of the tree as soon as the passed block returns true.
456+
# Note that siblings groups and their sub-trees will continue to be explored.
457+
# This is intended to make it easy to find the top-most group that satisfies some
458+
# condition.
459+
def self.traverse_tree_until(&block)
460+
return if yield self
461+
462+
children.each do |child|
463+
child.traverse_tree_until(&block)
464+
end
465+
end
466+
453467
# @private
454468
def self.next_runnable_index_for(file)
455469
if self == ExampleGroup

lib/rspec/core/world.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,19 @@ def all_examples
9292
FlatMap.flat_map(all_example_groups) { |g| g.examples }
9393
end
9494

95+
# @private
96+
# Traverses the tree of each top level group.
97+
# For each it yields the group, then the children, recursively.
98+
# Halts the traversal of a branch of the tree as soon as the passed block returns true.
99+
# Note that siblings groups and their sub-trees will continue to be explored.
100+
# This is intended to make it easy to find the top-most group that satisfies some
101+
# condition.
102+
def traverse_example_group_trees_until(&block)
103+
example_groups.each do |group|
104+
group.traverse_tree_until(&block)
105+
end
106+
end
107+
95108
# @api private
96109
#
97110
# Find line number of previous declaration.
@@ -215,6 +228,9 @@ def self.registered_example_group_files
215228
[]
216229
end
217230

231+
def self.traverse_example_group_trees_until
232+
end
233+
218234
# :nocov:
219235
def self.example_groups
220236
[]

0 commit comments

Comments
 (0)