Skip to content

Commit 10cda8b

Browse files
authored
Merge pull request rspec#2284 from rspec/myron/pr-2280-followups
PR 2280 followups
2 parents 0428d9a + d97bb7e commit 10cda8b

File tree

5 files changed

+89
-42
lines changed

5 files changed

+89
-42
lines changed

Changelog.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
### Development
2+
[Full Changelog](http://github.com/rspec/rspec-core/compare/v3.5.0...master)
3+
4+
Bug Fixes:
5+
6+
* Ensure that config hooks that are added to existing example groups are
7+
added only once. (Eugene Kenny, #2280)
8+
19
### 3.5.0 / 2016-07-01
210
[Full Changelog](http://github.com/rspec/rspec-core/compare/v3.5.0.beta4...v3.5.0)
311

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
[]

spec/rspec/core/hooks_filtering_spec.rb

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -83,33 +83,36 @@ module RSpec::Core
8383
]
8484
end
8585

86-
it "applies only to examples with matching metadata" do
87-
sequence = []
88-
89-
group = RSpec.describe do
90-
example("") { sequence << :ex_1 }
91-
example("", :run_hooks) { sequence << :ex_2 }
92-
end
86+
{ ":example" => [:example], ":each" => [:each] }.each do |label, args|
87+
args << :run_hooks
88+
it "applies only to examples with matching metadata (for hooks declared with #{label})" do
89+
sequence = []
90+
91+
group = RSpec.describe do
92+
example("") { sequence << :ex_1 }
93+
example("", :run_hooks) { sequence << :ex_2 }
94+
end
9395

94-
RSpec.configure do |c|
95-
c.before(:example, :run_hooks) { sequence << :before_ex_2 }
96-
c.prepend_before(:example, :run_hooks) { sequence << :before_ex_1 }
96+
RSpec.configure do |c|
97+
c.before(*args) { sequence << :before_ex_2 }
98+
c.prepend_before(*args) { sequence << :before_ex_1 }
9799

98-
c.after(:example, :run_hooks) { sequence << :after_ex_1 }
99-
c.append_after(:example, :run_hooks) { sequence << :after_ex_2 }
100+
c.after(*args) { sequence << :after_ex_1 }
101+
c.append_after(*args) { sequence << :after_ex_2 }
100102

101-
c.around(:example, :run_hooks) do |ex|
102-
sequence << :around_before_ex
103-
ex.run
104-
sequence << :around_after_ex
103+
c.around(*args) do |ex|
104+
sequence << :around_before_ex
105+
ex.run
106+
sequence << :around_after_ex
107+
end
105108
end
106-
end
107109

108-
group.run
109-
expect(sequence).to eq [
110-
:ex_1,
111-
:around_before_ex, :before_ex_1, :before_ex_2, :ex_2, :after_ex_1, :after_ex_2, :around_after_ex,
112-
]
110+
group.run
111+
expect(sequence).to eq [
112+
:ex_1,
113+
:around_before_ex, :before_ex_1, :before_ex_2, :ex_2, :after_ex_1, :after_ex_2, :around_after_ex,
114+
]
115+
end
113116
end
114117

115118
it "does not apply `suite` hooks to groups (or print warnings about suite hooks applied to example groups)" do

0 commit comments

Comments
 (0)