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 rspec#1790 (this is the last thing necessary for it).
- Addresses rspec#1762.
- Addresses user confusion reported in:
  - rspec/rspec-rails#1241
  - rspec/rspec-rails#1579
  • Loading branch information
myronmarston committed Jun 5, 2016
1 parent 83d576d commit 6fc26d3
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 35 deletions.
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Metrics/LineLength:

# This should go down over time.
Metrics/MethodLength:
Max: 40
Max: 37

# This should go down over time.
Metrics/CyclomaticComplexity:
Expand Down
4 changes: 4 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ Enhancements:
* Add new `config.include_context` API for configuring global or
filtered inclusion of shared contexts in example groups.
(Myron Marston, #2256)
* Add new `config.shared_context_metadata_behavior = :apply_to_host_groups`
option, which causes shared context metadata to be inherited by the
metadata hash of all host groups and examples instead of configuring
implicit auto-inclusion based on the passed metadata. (Myron Marston, #2256)

Bug Fixes:

Expand Down
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_groups
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
56 changes: 56 additions & 0 deletions lib/rspec/core/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,59 @@ def treat_symbols_as_metadata_keys_with_true_values=(_value)
)
end

# @macro define_reader
# Configures how RSpec treats metadata passed as part of a shared example
# group definition. For example, given this shared example group definition:
#
# RSpec.shared_context "uses DB", :db => true do
# around(:example) do |ex|
# MyORM.transaction(:rollback => true, &ex)
# end
# end
#
# ...there are two ways RSpec can treat the `:db => true` metadata, each
# of which has a corresponding config option:
#
# 1. `:trigger_inclusion`: this shared context will be implicitly included
# in any groups (or examples) that have `:db => true` metadata.
# 2. `:apply_to_host_groups`: the metadata will be inherited by the metadata
# hash of all host groups and examples.
#
# `:trigger_inclusion` is the legacy behavior from before RSpec 3.5 but should
# be considered deprecated. Instead, you can explicitly include a group with
# `include_context`:
#
# RSpec.describe "My model" do
# include_context "uses DB"
# end
#
# ...or you can configure RSpec to include the context based on matching metadata
# using an API that mirrors configured module inclusion:
#
# RSpec.configure do |rspec|
# rspec.include_context "uses DB", :db => true
# end
#
# `:apply_to_host_groups` is a new feature of RSpec 3.5 and will be the only
# supported behavior in RSpec 4.
#
# @overload shared_context_metadata_behavior
# @return [:trigger_inclusion, :apply_to_host_groups] the configured behavior
# @overload shared_context_metadata_behavior=(value)
# @param value [:trigger_inclusion, :apply_to_host_groups] sets the configured behavior
define_reader :shared_context_metadata_behavior
# @see shared_context_metadata_behavior
def shared_context_metadata_behavior=(value)
case value
when :trigger_inclusion, :apply_to_host_groups
@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_groups` are valid values."
end
end

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

Expand All @@ -352,6 +405,7 @@ def treat_symbols_as_metadata_keys_with_true_values=(_value)
attr_reader :backtrace_formatter, :ordering_manager, :loaded_spec_files

# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/MethodLength
def initialize
# rubocop:disable Style/GlobalVars
@start_time = $_rspec_core_load_started_at || ::RSpec::Core::Time.now
Expand Down Expand Up @@ -398,9 +452,11 @@ def initialize
@threadsafe = true
@max_displayed_failure_line_count = 10
@world = World::Null
@shared_context_metadata_behavior = :trigger_inclusion

define_built_in_hooks
end
# rubocop:enable Metrics/MethodLength
# rubocop:enable Metrics/AbcSize

# @private
Expand Down
45 changes: 35 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,8 @@ def included(klass)

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

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 +153,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 +181,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_groups" do
config.shared_context_metadata_behavior = :apply_to_host_groups
expect(config.shared_context_metadata_behavior).to eq :apply_to_host_groups
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_groups"
))
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
Loading

0 comments on commit 6fc26d3

Please sign in to comment.