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

Commit 0217ed4

Browse files
committed
Ensure idempotent hook registration if groups/examples are configured multiple times.
1 parent 86362bf commit 0217ed4

File tree

4 files changed

+47
-18
lines changed

4 files changed

+47
-18
lines changed

lib/rspec/core/configuration.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1635,7 +1635,7 @@ def disable_monkey_patching!
16351635
# end
16361636
def define_derived_metadata(*filters, &block)
16371637
meta = Metadata.build_hash_from(filters, :warn_about_example_group_filtering)
1638-
@derived_metadata_blocks.append(block, meta)
1638+
@derived_metadata_blocks.idempotently_append(block, meta)
16391639
end
16401640

16411641
# Defines a callback that runs after the first example with matching
@@ -1670,7 +1670,7 @@ def when_first_matching_example_defined(*filters, &block)
16701670
block.call
16711671
end
16721672

1673-
@derived_metadata_blocks.append(callback, specified_meta)
1673+
@derived_metadata_blocks.idempotently_append(callback, specified_meta)
16741674
end
16751675

16761676
# @private
@@ -2009,7 +2009,7 @@ def define_mixed_in_module(mod, filters, mod_list, config_method, &block)
20092009
end
20102010

20112011
meta = Metadata.build_hash_from(filters, :warn_about_example_group_filtering)
2012-
mod_list.append(mod, meta)
2012+
mod_list.idempotently_append(mod, meta)
20132013
on_existing_matching_groups(meta, &block)
20142014
end
20152015
end

lib/rspec/core/hooks.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ module Hooks
195195
# `RSpec.configuration` since they exist independently of any
196196
# example or example group.
197197
def before(*args, &block)
198-
hooks.register :append, :before, *args, &block
198+
hooks.register :idempotently_append, :before, *args, &block
199199
end
200200

201201
alias_method :append_before, :before
@@ -205,7 +205,7 @@ def before(*args, &block)
205205
#
206206
# See {#before} for scoping semantics.
207207
def prepend_before(*args, &block)
208-
hooks.register :prepend, :before, *args, &block
208+
hooks.register :idempotently_prepend, :before, *args, &block
209209
end
210210

211211
# @api public
@@ -269,7 +269,7 @@ def prepend_before(*args, &block)
269269
# `RSpec.configuration` since they exist independently of any
270270
# example or example group.
271271
def after(*args, &block)
272-
hooks.register :prepend, :after, *args, &block
272+
hooks.register :idempotently_prepend, :after, *args, &block
273273
end
274274

275275
alias_method :prepend_after, :after
@@ -279,7 +279,7 @@ def after(*args, &block)
279279
#
280280
# See {#after} for scoping semantics.
281281
def append_after(*args, &block)
282-
hooks.register :append, :after, *args, &block
282+
hooks.register :idempotently_append, :after, *args, &block
283283
end
284284

285285
# @api public
@@ -330,7 +330,7 @@ def append_after(*args, &block)
330330
# around(:example) {|ex| FakeFS(&ex)}
331331
#
332332
def around(*args, &block)
333-
hooks.register :prepend, :around, *args, &block
333+
hooks.register :idempotently_prepend, :around, *args, &block
334334
end
335335

336336
# @private
@@ -563,7 +563,7 @@ def process(host, parent_groups, globals, position, scope)
563563
return if hooks_to_process.empty?
564564

565565
repository = ensure_hooks_initialized_for(position, scope)
566-
hooks_to_process.each { |hook| repository.append hook, (yield hook) }
566+
hooks_to_process.each { |hook| repository.idempotently_append hook, (yield hook) }
567567
end
568568

569569
def scope_and_options_from(*args)

lib/rspec/core/metadata_filter.rb

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def filters_apply?(key, value, metadata)
9191
# @private
9292
module FilterableItemRepository
9393
# This implementation is simple, and is optimized for frequent
94-
# updates but rare queries. `append` and `prepend` do no extra
94+
# updates but rare queries. `idempotently_append` and `idempotently_prepend` do no extra
9595
# processing, and no internal memoization is done, since this
9696
# is not optimized for queries.
9797
#
@@ -106,14 +106,15 @@ class UpdateOptimized
106106
def initialize(applies_predicate)
107107
@applies_predicate = applies_predicate
108108
@items_and_filters = []
109+
@idempotence_set = Set.new
109110
end
110111

111-
def append(item, metadata)
112-
@items_and_filters << [item, metadata]
112+
def idempotently_append(item, metadata)
113+
idempotently_update(item, metadata, :<<)
113114
end
114115

115-
def prepend(item, metadata)
116-
@items_and_filters.unshift [item, metadata]
116+
def idempotently_prepend(item, metadata)
117+
idempotently_update(item, metadata, :unshift)
117118
end
118119

119120
def items_for(request_meta)
@@ -135,6 +136,16 @@ def items_for(request_meta)
135136
end
136137
# :nocov:
137138
end
139+
140+
private
141+
142+
def idempotently_update(item, metadata, update_method)
143+
idempotence_id = [item.__id__, metadata]
144+
return if @idempotence_set.include?(idempotence_id)
145+
146+
@items_and_filters.__send__(update_method, [item, metadata])
147+
@idempotence_set << idempotence_id
148+
end
138149
end
139150

140151
# This implementation is much more complex, and is optimized for
@@ -162,12 +173,12 @@ def initialize(applies_predicate)
162173
end
163174
end
164175

165-
def append(item, metadata)
176+
def idempotently_append(item, metadata)
166177
super
167178
handle_mutation(metadata)
168179
end
169180

170-
def prepend(item, metadata)
181+
def idempotently_prepend(item, metadata)
171182
super
172183
handle_mutation(metadata)
173184
end

spec/rspec/core/filterable_item_repository_spec.rb

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,24 @@ def self.it_behaves_like_a_filterable_item_repo(&when_the_repo_has_items_with_me
2222
repo.__send__ add_method, *args
2323
end
2424

25+
it "is idempotent" do
26+
add_item item_1, {}
27+
28+
expect {
29+
add_item item_1, {}
30+
}.not_to change { repo.items_for({}) }.from([item_1])
31+
end
32+
33+
it 'allows the same item to be added with different metadata' do
34+
add_item item_1, {:foo => 1}
35+
add_item item_1, {:bar => 2}
36+
37+
expect(repo.items_for({})).to eq []
38+
expect(repo.items_for(:foo => 1)).to eq [item_1]
39+
expect(repo.items_for(:bar => 2)).to eq [item_1]
40+
expect(repo.items_for(:foo => 1, :bar => 2)).to eq [item_1, item_1]
41+
end
42+
2543
context "when the repository has items that have no metadata" do
2644
before do
2745
add_item item_1, {}
@@ -125,11 +143,11 @@ def self.it_behaves_like_a_filterable_item_repo(&when_the_repo_has_items_with_me
125143
end
126144
end
127145

128-
it_behaves_like "adding items to the repository", :append do
146+
it_behaves_like "adding items to the repository", :idempotently_append do
129147
let(:items_in_expected_order) { [item_1, item_2, item_3] }
130148
end
131149

132-
it_behaves_like "adding items to the repository", :prepend do
150+
it_behaves_like "adding items to the repository", :idempotently_prepend do
133151
let(:items_in_expected_order) { [item_3, item_2, item_1] }
134152
end
135153
end

0 commit comments

Comments
 (0)