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

Commit 6dfaa30

Browse files
committed
Fix adding config hooks to existing example groups
When an example group is created, the existing config hooks are added to it if their metadata filters match and none of the new example group's parent groups already have the hook added. The current implementation adds new config hooks to all existing example groups, which means they can be run multiple times if several nested example groups all match their metadata filters, which doesn't happen if the config hook is defined first. To get the same behaviour regardless of the order in which hooks and groups are defined, we can walk the inheritence heirarchy starting from the top level example groups, and only add the new hook to the first matching group on each branch.
1 parent 9b84b78 commit 6dfaa30

File tree

3 files changed

+85
-8
lines changed

3 files changed

+85
-8
lines changed

lib/rspec/core/configuration.rb

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1749,7 +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-
on_existing_matching_groups({}) { |g| g.before(scope, *meta, &block) }
1752+
add_hook_to_existing_groups(:append, :before, scope, *meta, &block)
17531753
super(scope, *meta, &block)
17541754
end
17551755
end
@@ -1772,7 +1772,7 @@ def prepend_before(scope=nil, *meta, &block)
17721772
handle_suite_hook(scope, meta) do
17731773
@before_suite_hooks.unshift Hooks::BeforeHook.new(block, {})
17741774
end || begin
1775-
on_existing_matching_groups({}) { |g| g.prepend_before(scope, *meta, &block) }
1775+
add_hook_to_existing_groups(:prepend, :before, scope, *meta, &block)
17761776
super(scope, *meta, &block)
17771777
end
17781778
end
@@ -1790,7 +1790,7 @@ def after(scope=nil, *meta, &block)
17901790
handle_suite_hook(scope, meta) do
17911791
@after_suite_hooks.unshift Hooks::AfterHook.new(block, {})
17921792
end || begin
1793-
on_existing_matching_groups({}) { |g| g.after(scope, *meta, &block) }
1793+
add_hook_to_existing_groups(:prepend, :after, scope, *meta, &block)
17941794
super(scope, *meta, &block)
17951795
end
17961796
end
@@ -1813,7 +1813,7 @@ def append_after(scope=nil, *meta, &block)
18131813
handle_suite_hook(scope, meta) do
18141814
@after_suite_hooks << Hooks::AfterHook.new(block, {})
18151815
end || begin
1816-
on_existing_matching_groups({}) { |g| g.append_after(scope, *meta, &block) }
1816+
add_hook_to_existing_groups(:append, :after, scope, *meta, &block)
18171817
super(scope, *meta, &block)
18181818
end
18191819
end
@@ -1822,8 +1822,7 @@ def append_after(scope=nil, *meta, &block)
18221822
#
18231823
# See {Hooks#around} for full `around` hook docs.
18241824
def around(scope=nil, *meta, &block)
1825-
on_existing_matching_groups({}) { |g| g.around(scope, *meta, &block) }
1826-
1825+
add_hook_to_existing_groups(:prepend, :around, scope, *meta, &block)
18271826
super(scope, *meta, &block)
18281827
end
18291828

@@ -2033,6 +2032,12 @@ def on_existing_matching_groups(meta)
20332032
end
20342033
end
20352034

2035+
def add_hook_to_existing_groups(prepend_or_append, position, *meta, &block)
2036+
world.example_groups.each do |group|
2037+
group.hooks.register_global_hook(prepend_or_append, position, *meta, &block)
2038+
end
2039+
end
2040+
20362041
if RSpec::Support::RubyFeatures.module_prepends_supported?
20372042
def safe_prepend(mod, host)
20382043
host.__send__(:prepend, mod) unless host < mod

lib/rspec/core/hooks.rb

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,18 @@ def register_global_singleton_context_hooks(example, globals)
438438
process(example, parent_groups, globals, :after, :context) { {} }
439439
end
440440

441+
def register_global_hook(prepend_or_append, position, *args, &block)
442+
scope, options = scope_and_options_from(*args)
443+
444+
if scope == :example || options.empty? || MetadataFilter.apply?(:all?, options, @owner.metadata)
445+
register_hook(prepend_or_append, position, scope, options, &block)
446+
else
447+
@owner.children.each do |group|
448+
group.hooks.register_global_hook(prepend_or_append, position, *args, &block)
449+
end
450+
end
451+
end
452+
441453
def register(prepend_or_append, position, *args, &block)
442454
scope, options = scope_and_options_from(*args)
443455

@@ -451,8 +463,7 @@ def register(prepend_or_append, position, *args, &block)
451463
return
452464
end
453465

454-
hook = HOOK_TYPES[position][scope].new(block, options)
455-
ensure_hooks_initialized_for(position, scope).__send__(prepend_or_append, hook, options)
466+
register_hook(prepend_or_append, position, scope, options, &block)
456467
end
457468

458469
# @private
@@ -553,6 +564,11 @@ def ensure_hooks_initialized_for(position, scope)
553564
end
554565
end
555566

567+
def register_hook(prepend_or_append, position, scope, options, &block)
568+
hook = HOOK_TYPES[position][scope].new(block, options)
569+
ensure_hooks_initialized_for(position, scope).__send__(prepend_or_append, hook, options)
570+
end
571+
556572
def process(host, parent_groups, globals, position, scope)
557573
hooks_to_process = globals.processable_hooks_for(position, scope, host)
558574
return if hooks_to_process.empty?

spec/rspec/core/hooks_filtering_spec.rb

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,62 @@ module RSpec::Core
129129
group.run
130130
expect(sequence).to eq [:example]
131131
end
132+
133+
it "only runs example hooks once when there are multiple nested example groups" do
134+
sequence = []
135+
136+
group = RSpec.describe do
137+
context do
138+
example { sequence << :ex_1 }
139+
example { sequence << :ex_2 }
140+
end
141+
end
142+
143+
RSpec.configure do |c|
144+
c.before(:example) { sequence << :before_ex_2 }
145+
c.prepend_before(:example) { sequence << :before_ex_1 }
146+
147+
c.after(:example) { sequence << :after_ex_1 }
148+
c.append_after(:example) { sequence << :after_ex_2 }
149+
150+
c.around(:example) do |ex|
151+
sequence << :around_before_ex
152+
ex.run
153+
sequence << :around_after_ex
154+
end
155+
end
156+
157+
group.run
158+
159+
expect(sequence).to eq [
160+
:around_before_ex, :before_ex_1, :before_ex_2, :ex_1, :after_ex_1, :after_ex_2, :around_after_ex,
161+
:around_before_ex, :before_ex_1, :before_ex_2, :ex_2, :after_ex_1, :after_ex_2, :around_after_ex
162+
]
163+
end
164+
165+
it "only runs context hooks around the highest level group with matching filters" do
166+
sequence = []
167+
168+
group = RSpec.describe do
169+
before(:context) { sequence << :before_context }
170+
after(:context) { sequence << :after_context }
171+
172+
context "", :match do
173+
context "", :match do
174+
example { sequence << :example }
175+
end
176+
end
177+
end
178+
179+
RSpec.configure do |config|
180+
config.before(:context, :match) { sequence << :before_hook }
181+
config.after(:context, :match) { sequence << :after_hook }
182+
end
183+
184+
group.run
185+
186+
expect(sequence).to eq [:before_context, :before_hook, :example, :after_hook, :after_context]
187+
end
132188
end
133189

134190
describe "unfiltered hooks" do

0 commit comments

Comments
 (0)