Skip to content
This repository was archived by the owner on Nov 30, 2024. It is now read-only.
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
3 changes: 3 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ Enhancements:
* Add `disable_monkey_patching!` config option that disables all monkey
patching from whatever pieces of RSpec you use. (Alexey Fedorov)
* Add `Pathname` support for setting all output streams. (Aaron Kromer)
* Add `config.define_derived_metadata`, which can be used to apply
additional metadata to all groups or examples that match a given
filter. (Myron Marston)

Bug Fixes:

Expand Down
27 changes: 27 additions & 0 deletions lib/rspec/core/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ def initialize
@profile_examples = false
@requires = []
@libs = []
@derived_metadata_blocks = []
end

# @private
Expand Down Expand Up @@ -1209,6 +1210,32 @@ def disable_monkey_patching!
# @private
attr_accessor :disable_monkey_patching

# Defines a callback that can assign derived metadata values.
#
# @param filters [Array<Symbol>, Hash] metadata filters that determine which example
# or group metadata hashes the callback will be triggered for. If none are given,
# the callback will be run against the metadata hashes of all groups and examples.
# @yieldparam metadata [Hash] original metadata hash from an example or group. Mutate this in
# your block as needed.
#
# @example
# RSpec.configure do |config|
# # Tag all groups and examples in the spec/unit directory with :type => :unit
# config.define_derived_metadata(:file_path => %r{/spec/unit/}) do |metadata|
# metadata[:type] = :unit
# end
# end
def define_derived_metadata(*filters, &block)
@derived_metadata_blocks << [Metadata.build_hash_from(filters), block]
end

# @private
def apply_derived_metadata_to(metadata)
@derived_metadata_blocks.each do |filter, block|
block.call(metadata) if filter.empty? || MetadataFilter.any_apply?(filter, metadata)
Copy link
Member

Choose a reason for hiding this comment

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

Why if filter.empty? here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Because if you don't pass any filters, it should always apply:

RSpec.configure do |config|
  config.define_derived_metadata do |metadata|
    # this block should always run against every metadata hash
  end
end

Copy link
Member

Choose a reason for hiding this comment

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

Oh, to apply the block globally, I see.

Copy link
Member

Choose a reason for hiding this comment

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

Figured it out as you commented there ;)

end
end

private

def get_files_to_run(paths)
Expand Down
1 change: 1 addition & 0 deletions lib/rspec/core/metadata.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ def populate

populate_location_attributes
metadata.update(user_metadata)
RSpec.configuration.apply_derived_metadata_to(metadata)
Copy link
Member

Choose a reason for hiding this comment

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

Minor concern about using the singleton config here, but it's not very common to replace the config and shouldn't happen during run time so I think that's ok.

Copy link
Member Author

Choose a reason for hiding this comment

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

If there's a better way to access it I'm all ears...

end

private
Expand Down
101 changes: 101 additions & 0 deletions spec/rspec/core/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1077,6 +1077,107 @@ def metadata_hash(*args)
end
end

describe "#define_derived_metadata" do
it 'allows the provided block to mutate example group metadata' do
RSpec.configuration.define_derived_metadata do |metadata|
metadata[:reverse_description] = metadata[:description].reverse
end

group = RSpec.describe("My group")
expect(group.metadata).to include(:description => "My group", :reverse_description => "puorg yM")
end

it 'allows the provided block to mutate example metadata' do
RSpec.configuration.define_derived_metadata do |metadata|
metadata[:reverse_description] = metadata[:description].reverse
end

ex = RSpec.describe("My group").example("foo")
expect(ex.metadata).to include(:description => "foo", :reverse_description => "oof")
end

it 'allows multiple configured blocks to be applied, in order of definition' do
Copy link

Choose a reason for hiding this comment

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

although, I like to be able to do this, i find the syntax somewhat confusing... it seems like the 2nd line overrides the first.
how do you see this being used?

it seems to me that it would be easier to have a single derived_metadata hash, which you can just edit...

c.update_metadata { |m| m[:b1_desc] = m[:description] + " (block 1)" }
c.update_metadata { |m| m[:b2_desc] = m[:b1_desc]     + " (block 2)" }

Copy link
Member

Choose a reason for hiding this comment

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

@yelled3 this is mainly being used for rspec-rails magic to prevent mutation of the hash pre runtime.

RSpec.configure do |c|
c.define_derived_metadata { |m| m[:b1_desc] = m[:description] + " (block 1)" }
c.define_derived_metadata { |m| m[:b2_desc] = m[:b1_desc] + " (block 2)" }
end

group = RSpec.describe("bar")
expect(group.metadata).to include(:b1_desc => "bar (block 1)", :b2_desc => "bar (block 1) (block 2)")
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|
metadata[:bar] = "bar"
end
end

group_bar_value = example_bar_value = nil

RSpec.describe "Group", :foo do
group_bar_value = metadata[:bar]
example_bar_value = example("ex", :foo).metadata[:bar]
end

expect(group_bar_value).to eq("bar")
expect(example_bar_value).to eq("bar")
end

context "when passed a metadata filter" do
it 'only applies to the groups and examples that match that filter' do
RSpec.configure do |c|
c.define_derived_metadata(:apply => true) do |metadata|
metadata[:reverse_description] = metadata[:description].reverse
end
end

g1 = RSpec.describe("G1", :apply)
g2 = RSpec.describe("G2")
e1 = g1.example("E1")
e2 = g2.example("E2", :apply)
e3 = g2.example("E3")

expect(g1.metadata).to include(:reverse_description => "1G")
expect(g2.metadata).not_to include(:reverse_description)

expect(e1.metadata).to include(:reverse_description => "1E")
expect(e2.metadata).to include(:reverse_description => "2E")
expect(e3.metadata).not_to include(:reverse_description)
end

it 'applies if any of multiple filters apply (to align with module inclusion semantics)' do
RSpec.configure do |c|
c.define_derived_metadata(:a => 1, :b => 2) do |metadata|
metadata[:reverse_description] = metadata[:description].reverse
end
end

g1 = RSpec.describe("G1", :a => 1)
g2 = RSpec.describe("G2", :b => 2)
g3 = RSpec.describe("G3", :c => 3)

expect(g1.metadata).to include(:reverse_description => "1G")
expect(g2.metadata).to include(:reverse_description => "2G")
expect(g3.metadata).not_to include(:reverse_description)
end

it 'allows a metadata filter to be passed as a raw symbol' do
RSpec.configure do |c|
c.define_derived_metadata(:apply) do |metadata|
metadata[:reverse_description] = metadata[:description].reverse
end
end

g1 = RSpec.describe("G1", :apply)
g2 = RSpec.describe("G2")

expect(g1.metadata).to include(:reverse_description => "1G")
expect(g2.metadata).not_to include(:reverse_description)
end
end
end

describe "#add_setting" do
describe "with no modifiers" do
context "with no additional options" do
Expand Down