Skip to content

Commit

Permalink
Provide config option for shared context metadata behavior.
Browse files Browse the repository at this point in the history
Previously, it always triggered auto-inclusion based on
matching metadata. The option allows you to opt-in to having
it add the metadata to included groups and examples instead.

- Closes #1790 (this is the last thing necessary for it).
- Addresses #1762.
- Addresses user confusion reported in:
  - rspec/rspec-rails#1241
  - rspec/rspec-rails#1579
  • Loading branch information
myronmarston committed Jun 5, 2016
1 parent 6f856b7 commit b73d081
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 34 deletions.
30 changes: 29 additions & 1 deletion features/example_groups/shared_context.feature
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,17 @@ Feature: shared context
Background:
Given a file named "shared_stuff.rb" with:
"""ruby
RSpec.shared_context "shared stuff" do
RSpec.configure do |rspec|
# This config option will be enabled by default on RSpec 4,
# but for reasons of backwards compatibility, you have to
# set it on RSpec 3.
#
# It causes the host group and examples to inherit metadata
# from the shared context.
rspec.shared_context_metadata_behavior = :apply_to_host_group
end
RSpec.shared_context "shared stuff", :shared_context => :metadata do
before { @some_var = :some_value }
def shared_method
"it works"
Expand Down Expand Up @@ -46,6 +56,13 @@ Feature: shared context
it "accesses the subject defined in the shared context" do
expect(subject).to eq('this is the subject')
end
group = self
it "inherits metadata from the included context" do |ex|
expect(group.metadata).to include(:shared_context => :metadata)
expect(ex.metadata).to include(:shared_context => :metadata)
end
end
"""
When I run `rspec shared_context_example.rb`
Expand Down Expand Up @@ -90,6 +107,13 @@ Feature: shared context
it "accesses the subject defined in the shared context" do
expect(subject).to eq('this is the subject')
end
group = self
it "inherits metadata from the included context" do |ex|
expect(group.metadata).to include(:shared_context => :metadata)
expect(ex.metadata).to include(:shared_context => :metadata)
end
end
"""
When I run `rspec shared_context_example.rb`
Expand All @@ -108,6 +132,10 @@ Feature: shared context
it "has access to shared methods from examples with matching metadata", :include_shared => true do
expect(shared_method).to eq("it works")
end
it "inherits metadata form the included context due to the matching metadata", :include_shared => true do |ex|
expect(ex.metadata).to include(:shared_context => :metadata)
end
end
"""
When I run `rspec shared_context_example.rb`
Expand Down
16 changes: 16 additions & 0 deletions lib/rspec/core/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,21 @@ def treat_symbols_as_metadata_keys_with_true_values=(_value)
)
end

# @macro define_reader
# TODO: document this
define_reader :shared_context_metadata_behavior

def shared_context_metadata_behavior=(value)
case value
when :trigger_inclusion, :apply_to_host_group
@shared_context_metadata_behavior = value
else
raise ArgumentError, "Cannot set `RSpec.configuration." \
"shared_context_metadata_behavior` to `#{value.inspect}`. Only " \
"`:trigger_inclusion` and `:apply_to_host_group` are valid values."
end
end

# Record the start time of the spec suite to measure load time.
add_setting :start_time

Expand Down Expand Up @@ -398,6 +413,7 @@ def initialize
@threadsafe = true
@max_displayed_failure_line_count = 10
@world = World::Null
@shared_context_metadata_behavior = :trigger_inclusion

define_built_in_hooks
end
Expand Down
47 changes: 37 additions & 10 deletions lib/rspec/core/shared_example_group.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ class SharedExampleGroupModule < Module
# @private
attr_reader :definition

def initialize(description, definition)
def initialize(description, definition, metadata)
@description = description
@definition = definition
@metadata = metadata
end

# Provides a human-readable representation of this module.
Expand All @@ -29,6 +30,10 @@ def included(klass)

# @private
def include_in(klass, inclusion_line, args, customization_block)
unless @metadata.empty?
klass.update_inherited_metadata(@metadata)
end

SharedExampleGroupInclusionStackFrame.with_frame(@description, inclusion_line) do
klass.class_exec(*args, &@definition)
klass.class_exec(&customization_block) if customization_block
Expand Down Expand Up @@ -150,18 +155,21 @@ def self.remove_globally!
# @private
class Registry
def add(context, name, *metadata_args, &block)
ensure_block_has_source_location(block) { CallerFilter.first_non_rspec_line }
shared_module = SharedExampleGroupModule.new(name, block)
if RSpec.configuration.shared_context_metadata_behavior == :trigger_inclusion
return legacy_add(context, name, *metadata_args, &block)
end

if valid_name?(name)
warn_if_key_taken context, name, block
shared_example_groups[context][name] = shared_module
else
metadata_args.unshift name
unless valid_name?(name)
raise ArgumentError, "Shared example group names can only be a string, " \
"symbol or module but got: #{name.inspect}"
end

return if metadata_args.empty?
RSpec.configuration.include shared_module, *metadata_args
ensure_block_has_source_location(block) { CallerFilter.first_non_rspec_line }
warn_if_key_taken context, name, block

metadata = Metadata.build_hash_from(metadata_args)
shared_module = SharedExampleGroupModule.new(name, block, metadata)
shared_example_groups[context][name] = shared_module
end

def find(lookup_contexts, name)
Expand All @@ -175,6 +183,25 @@ def find(lookup_contexts, name)

private

# TODO: remove this in RSpec 4. This exists only to support
# `config.shared_context_metadata_behavior == :trigger_inclusion`,
# the legacy behavior of shared context metadata, which we do
# not want to support in RSpec 4.
def legacy_add(context, name, *metadata_args, &block)
ensure_block_has_source_location(block) { CallerFilter.first_non_rspec_line }
shared_module = SharedExampleGroupModule.new(name, block, {})

if valid_name?(name)
warn_if_key_taken context, name, block
shared_example_groups[context][name] = shared_module
else
metadata_args.unshift name
end

return if metadata_args.empty?
RSpec.configuration.include shared_module, *metadata_args
end

def shared_example_groups
@shared_example_groups ||= Hash.new { |hash, context| hash[context] = {} }
end
Expand Down
25 changes: 25 additions & 0 deletions spec/rspec/core/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2414,6 +2414,31 @@ def emulate_not_configured_expectation_framework
end
end

describe "#shared_context_metadata_behavior" do
it "defaults to :trigger_inclusion for backwards compatibility" do
expect(config.shared_context_metadata_behavior).to eq :trigger_inclusion
end

it "can be set to :apply_to_host_group" do
config.shared_context_metadata_behavior = :apply_to_host_group
expect(config.shared_context_metadata_behavior).to eq :apply_to_host_group
end

it "can be set to :trigger_inclusion explicitly" do
config.shared_context_metadata_behavior = :trigger_inclusion
expect(config.shared_context_metadata_behavior).to eq :trigger_inclusion
end

it "cannot be set to any other values" do
expect {
config.shared_context_metadata_behavior = :another_value
}.to raise_error(ArgumentError, a_string_including(
"shared_context_metadata_behavior",
":another_value", ":trigger_inclusion", ":apply_to_host_group"
))
end
end

# assigns files_or_directories_to_run and triggers post-processing
# via `files_to_run`.
def assign_files_or_directories_to_run(*value)
Expand Down
126 changes: 103 additions & 23 deletions spec/rspec/core/shared_example_group_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,18 @@ module Core
describe shared_method_name do
let(:group) { RSpec.describe('example group') }

before do
RSpec.configuration.shared_context_metadata_behavior = :apply_to_host_group
end

define_method :define_shared_group do |*args, &block|
group.send(shared_method_name, *args, &block)
end

define_method :define_top_level_shared_group do |*args, &block|
RSpec.send(shared_method_name, *args, &block)
end

def find_implementation_block(registry, scope, name)
registry.find([scope], name).definition
end
Expand Down Expand Up @@ -80,7 +88,8 @@ def find_implementation_block(registry, scope, name)
end

it 'generates a named (rather than anonymous) module' do
define_shared_group("shared behaviors", :include_it) { }
define_top_level_shared_group("shared behaviors") { }
RSpec.configuration.include_context "shared behaviors", :include_it
example_group = RSpec.describe("Group", :include_it) { }

anonymous_module_regex = /#<Module:0x[0-9a-f]+>/
Expand All @@ -107,42 +116,109 @@ def find_implementation_block(registry, scope, name)
end
end

context "given a hash" do
it "includes itself in matching example groups" do
implementation = Proc.new { def self.bar; 'bar'; end }
define_shared_group(:foo => :bar, &implementation)
context "when `config.shared_context_metadata_behavior == :trigger_inclusion`" do
before do
RSpec.configuration.shared_context_metadata_behavior = :trigger_inclusion
end

context "given a hash" do
it "includes itself in matching example groups" do
implementation = Proc.new { def self.bar; 'bar'; end }
define_shared_group(:foo => :bar, &implementation)

matching_group = RSpec.describe "Group", :foo => :bar
non_matching_group = RSpec.describe "Group"
matching_group = RSpec.describe "Group", :foo => :bar
non_matching_group = RSpec.describe "Group"

expect(matching_group.bar).to eq("bar")
expect(non_matching_group).not_to respond_to(:bar)
expect(matching_group.bar).to eq("bar")
expect(non_matching_group).not_to respond_to(:bar)
end
end

context "given a string and a hash" do
it "captures the given string and block in the World's collection of shared example groups" do
implementation = lambda { }
define_shared_group("name", :foo => :bar, &implementation)
expect(find_implementation_block(registry, group, "name")).to eq implementation
end

it "delegates include on configuration" do
implementation = Proc.new { def self.bar; 'bar'; end }
define_shared_group("name", :foo => :bar, &implementation)

matching_group = RSpec.describe "Group", :foo => :bar
non_matching_group = RSpec.describe "Group"

expect(matching_group.bar).to eq("bar")
expect(non_matching_group).not_to respond_to(:bar)
end
end

it "displays a warning when adding a second shared example group with the same name" do
group.send(shared_method_name, 'some shared group') {}
original_declaration = [__FILE__, __LINE__ - 1].join(':')

warning = nil
allow(::Kernel).to receive(:warn) { |msg| warning = msg }

group.send(shared_method_name, 'some shared group') {}
second_declaration = [__FILE__, __LINE__ - 1].join(':')
expect(warning).to include('some shared group', original_declaration, second_declaration)
expect(warning).to_not include 'Called from'
end
end

context "given a string and a hash" do
it "captures the given string and block in the World's collection of shared example groups" do
implementation = lambda { }
define_shared_group("name", :foo => :bar, &implementation)
expect(find_implementation_block(registry, group, "name")).to eq implementation
context "when `config.shared_context_metadata_behavior == :apply_to_host_group`" do
before do
RSpec.configuration.shared_context_metadata_behavior = :apply_to_host_group
end

it "delegates include on configuration" do
implementation = Proc.new { def self.bar; 'bar'; end }
define_shared_group("name", :foo => :bar, &implementation)
it "does not auto-include the shared group based on passed metadata" do
define_top_level_shared_group("name", :foo => :bar) do
def self.bar; 'bar'; end
end

matching_group = RSpec.describe "Group", :foo => :bar
non_matching_group = RSpec.describe "Group"

expect(matching_group.bar).to eq("bar")
expect(non_matching_group).not_to respond_to(:bar)
expect(matching_group).not_to respond_to(:bar)
end

it "adds passed metadata to including groups and examples" do
define_top_level_shared_group("name", :foo => :bar) { }

group = RSpec.describe("outer")
nested = group.describe("inner")
example = group.example("ex")

group.include_context "name"

expect([group, nested, example]).to all have_attributes(
:metadata => a_hash_including(:foo => :bar)
)
end

it "requires a valid name" do
expect {
define_shared_group(:foo => 1)
}.to raise_error(ArgumentError, a_string_including(
"Shared example group names",
{:foo => 1}.inspect
))
end
end

context "when the group is included via `config.include_context` and matching metadata" do
before do
# To ensure we don't accidentally include shared contexts the
# old way in this context, we disable the option here.
RSpec.configuration.shared_context_metadata_behavior = :apply_to_host_group
end

describe "when it has a `let` and applies to an individual example via metadata" do
it 'defines the `let` method correctly' do
define_shared_group("name", :include_it) do
define_top_level_shared_group("name") do
let(:foo) { "bar" }
end
RSpec.configuration.include_context "name", :include_it

ex = value = nil
RSpec.describe "group" do
Expand All @@ -166,7 +242,7 @@ def find_implementation_block(registry, scope, name)
it 'runs them' do
sequence = []

define_shared_group("name", :include_it) do
define_top_level_shared_group("name") do
before(:context) { sequence << :before_context }
after(:context) { sequence << :after_context }

Expand All @@ -180,6 +256,8 @@ def find_implementation_block(registry, scope, name)
end
end

RSpec.configuration.include_context "name", :include_it

RSpec.describe "group" do
example("ex1") { sequence << :unmatched_example_1 }
example("ex2", :include_it) { sequence << :matched_example }
Expand All @@ -202,14 +280,16 @@ def find_implementation_block(registry, scope, name)
it 'runs the `after(:context)` hooks even if the `before(:context)` hook raises an error' do
sequence = []

define_shared_group("name", :include_it) do
define_top_level_shared_group("name") do
before(:context) do
sequence << :before_context
raise "boom"
end
after(:context) { sequence << :after_context }
end

RSpec.configuration.include_context "name", :include_it

RSpec.describe "group" do
example("ex", :include_it) { sequence << :example }
end.run
Expand Down

0 comments on commit b73d081

Please sign in to comment.