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

Commit e0ecbaf

Browse files
myronmarstonJonRowe
authored andcommitted
Fix define_derived_metadata so that it supports cascades. (#2630)
For example: ``` RSpec.configure do |c| c.define_derived_metadata(:elasticsearch) do |meta| meta[:vcr] = true end c.define_derived_metadata(:vcr) do |meta| meta[:retries] = 2 end end ``` With this configuration, an example or group tagged with `:elasticsearch` should get tagged with `:vcr` as well, which in turn should add `retries: 2` metadata to the example or group. Before this change, this did not work properly, because we did look to see if additional metadata blocks should apply after applying them once.
1 parent d04d4de commit e0ecbaf

File tree

3 files changed

+59
-2
lines changed

3 files changed

+59
-2
lines changed

Changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Bug Fixes:
1919
* When defining `let` methods that overwrite an existing method, prevent
2020
a warning being issued by removing the old definition. (Jon Rowe, #2593)
2121
* Prevent warning on Ruby 2.6.0-rc1 (Keiji Yoshimi, #2582)
22+
* Fix `config.define_derived_metadata` so that it supports cascades.
23+
(Myron Marston, #2630).
2224

2325
### 3.8.0 / 2018-08-04
2426
[Full Changelog](http://github.com/rspec/rspec-core/compare/v3.7.1...v3.8.0)

lib/rspec/core/configuration.rb

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1855,9 +1855,28 @@ def when_first_matching_example_defined(*filters)
18551855

18561856
# @private
18571857
def apply_derived_metadata_to(metadata)
1858-
@derived_metadata_blocks.items_for(metadata).each do |block|
1859-
block.call(metadata)
1858+
already_run_blocks = Set.new
1859+
1860+
# We loop and attempt to re-apply metadata blocks to support cascades
1861+
# (e.g. where a derived bit of metadata triggers the application of
1862+
# another piece of derived metadata, etc)
1863+
#
1864+
# We limit our looping to 200 times as a way to detect infinitely recursing derived metadata blocks.
1865+
# It's hard to imagine a valid use case for a derived metadata cascade greater than 200 iterations.
1866+
200.times do
1867+
return if @derived_metadata_blocks.items_for(metadata).all? do |block|
1868+
already_run_blocks.include?(block).tap do |skip_block|
1869+
block.call(metadata) unless skip_block
1870+
already_run_blocks << block
1871+
end
1872+
end
18601873
end
1874+
1875+
# If we got here, then `@derived_metadata_blocks.items_for(metadata).all?` never returned
1876+
# `true` above and we treat this as an attempt to recurse infinitely. It's better to fail
1877+
# with a clear # error than hang indefinitely, which is what would happen if we didn't limit
1878+
# the looping above.
1879+
raise SystemStackError, "Attempted to recursively derive metadata indefinitely."
18611880
end
18621881

18631882
# Defines a `before` hook. See {Hooks#before} for full docs.

spec/rspec/core/configuration_spec.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1819,6 +1819,42 @@ def exclude?(line)
18191819
expect(group.metadata).to include(:b1_desc => "bar (block 1)", :b2_desc => "bar (block 1) (block 2)")
18201820
end
18211821

1822+
it 'supports cascades of derived metadata, but avoids re-running derived metadata blocks that have already been applied' do
1823+
RSpec.configure do |c|
1824+
c.define_derived_metadata(:foo1) { |m| m[:foo2] = (m[:foo2] || 0) + 1 }
1825+
c.define_derived_metadata(:foo2) { |m| m[:foo3] = (m[:foo3] || 0) + 1 }
1826+
c.define_derived_metadata(:foo3) { |m| m[:foo1] += 1 }
1827+
end
1828+
1829+
group = RSpec.describe("bar", :foo1 => 0)
1830+
expect(group.metadata).to include(:foo1 => 1, :foo2 => 1, :foo3 => 1)
1831+
1832+
ex = RSpec.describe("My group").example("foo", :foo1 => 0)
1833+
expect(ex.metadata).to include(:foo1 => 1, :foo2 => 1, :foo3 => 1)
1834+
end
1835+
1836+
it 'does not allow a derived metadata cascade to recurse infinitely' do
1837+
RSpec.configure do |c|
1838+
counter = 1
1839+
derive_next_metadata = lambda do |outer_meta|
1840+
tag = :"foo#{counter += 1}"
1841+
outer_meta[tag] = true
1842+
1843+
c.define_derived_metadata(tag) do |inner_meta|
1844+
derive_next_metadata.call(inner_meta)
1845+
end
1846+
end
1847+
1848+
c.define_derived_metadata(:foo1) do |meta|
1849+
derive_next_metadata.call(meta)
1850+
end
1851+
end
1852+
1853+
expect {
1854+
RSpec.describe("group", :foo1)
1855+
}.to raise_error(SystemStackError)
1856+
end
1857+
18221858
it "derives metadata before the group or example blocks are eval'd so their logic can depend on the derived metadata" do
18231859
RSpec.configure do |c|
18241860
c.define_derived_metadata(:foo) do |metadata|

0 commit comments

Comments
 (0)