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

Fix define_derived_metadata so that it supports cascades. #2630

Merged
merged 1 commit into from
May 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Bug Fixes:
* When defining `let` methods that overwrite an existing method, prevent
a warning being issued by removing the old definition. (Jon Rowe, #2593)
* Prevent warning on Ruby 2.6.0-rc1 (Keiji Yoshimi, #2582)
* Fix `config.define_derived_metadata` so that it supports cascades.
(Myron Marston, #2630).

### 3.8.0 / 2018-08-04
[Full Changelog](http://github.com/rspec/rspec-core/compare/v3.7.1...v3.8.0)
Expand Down
23 changes: 21 additions & 2 deletions lib/rspec/core/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1855,9 +1855,28 @@ def when_first_matching_example_defined(*filters)

# @private
def apply_derived_metadata_to(metadata)
@derived_metadata_blocks.items_for(metadata).each do |block|
block.call(metadata)
already_run_blocks = Set.new

# We loop and attempt to re-apply metadata blocks to support cascades
# (e.g. where a derived bit of metadata triggers the application of
# another piece of derived metadata, etc)
#
# We limit our looping to 200 times as a way to detect infinitely recursing derived metadata blocks.
# It's hard to imagine a valid use case for a derived metadata cascade greater than 200 iterations.
200.times do
return if @derived_metadata_blocks.items_for(metadata).all? do |block|
already_run_blocks.include?(block).tap do |skip_block|
block.call(metadata) unless skip_block
already_run_blocks << block
end
end
end

# If we got here, then `@derived_metadata_blocks.items_for(metadata).all?` never returned
# `true` above and we treat this as an attempt to recurse infinitely. It's better to fail
# with a clear # error than hang indefinitely, which is what would happen if we didn't limit
# the looping above.
raise SystemStackError, "Attempted to recursively derive metadata indefinitely."
end

# Defines a `before` hook. See {Hooks#before} for full docs.
Expand Down
36 changes: 36 additions & 0 deletions spec/rspec/core/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1819,6 +1819,42 @@ def exclude?(line)
expect(group.metadata).to include(:b1_desc => "bar (block 1)", :b2_desc => "bar (block 1) (block 2)")
end

it 'supports cascades of derived metadata, but avoids re-running derived metadata blocks that have already been applied' do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see how this demonstrates the cascade, but not the re-applied logic...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It demonstrates that the blocks are not re-applied by asserting that the values of each of foo* is 1. The blocks each increment the value, so if any of the define_derived_metadata blocks are run multiple times, then the valule would be something greater than 1 and the expectation below would fail.

How can I make this more clear?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't expect in this scenario the blocks to be run more than once though, but this would demonstrate the issue more clearly:

c.define_derived_metadata(:foo1) { |m| m[:foo2] = (m[:foo2] || 0) + 1 }
c.define_derived_metadata(:foo2) { |m| m[:foo3] = (m[:foo3] || 0) + 1 }
c.define_derived_metadata(:foo3) { |m| m[:foo1] = (m[:foo1] || 0) + 1 }

group = RSpec.describe("bar", :foo1 => 1)
expect(group.metadata).to include(:foo1 => 2, :foo2 => 1, :foo3 => 1)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't expect in this scenario the blocks to be run more than once though

They would run more than once if it wasn't for the unless skip block bit on the block.call(metadata) unless skip_block line.

Anyhow, I've changed to:

        RSpec.configure do |c|
          c.define_derived_metadata(:foo1) { |m| m[:foo2] = (m[:foo2] || 0) + 1 }
          c.define_derived_metadata(:foo2) { |m| m[:foo3] = (m[:foo3] || 0) + 1 }
          c.define_derived_metadata(:foo3) { |m| m[:foo1] += 1 }
        end

        group = RSpec.describe("bar", :foo1 => 0)
        expect(group.metadata).to include(:foo1 => 1, :foo2 => 1, :foo3 => 1)

I think it's a bit easier to see that each block runs only once if the values are all 1 (if one value is 2 it makes it seem like a block ran twice I think), so we can start foo1 at 0 to achieve this.

RSpec.configure do |c|
c.define_derived_metadata(:foo1) { |m| m[:foo2] = (m[:foo2] || 0) + 1 }
c.define_derived_metadata(:foo2) { |m| m[:foo3] = (m[:foo3] || 0) + 1 }
c.define_derived_metadata(:foo3) { |m| m[:foo1] += 1 }
end

group = RSpec.describe("bar", :foo1 => 0)
expect(group.metadata).to include(:foo1 => 1, :foo2 => 1, :foo3 => 1)

ex = RSpec.describe("My group").example("foo", :foo1 => 0)
expect(ex.metadata).to include(:foo1 => 1, :foo2 => 1, :foo3 => 1)
end

it 'does not allow a derived metadata cascade to recurse infinitely' do
RSpec.configure do |c|
counter = 1
derive_next_metadata = lambda do |outer_meta|
tag = :"foo#{counter += 1}"
outer_meta[tag] = true

c.define_derived_metadata(tag) do |inner_meta|
derive_next_metadata.call(inner_meta)
end
end

c.define_derived_metadata(:foo1) do |meta|
derive_next_metadata.call(meta)
end
end

expect {
RSpec.describe("group", :foo1)
}.to raise_error(SystemStackError)
end

it "derives metadata before the group or example blocks are eval'd so their logic can depend on the derived metadata" do
RSpec.configure do |c|
c.define_derived_metadata(:foo) do |metadata|
Expand Down