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

Add config.when_first_matching_example_defined. #2175

Merged
merged 3 commits into from
Mar 8, 2016
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 @@ -9,6 +9,8 @@ Enhancements:
(Myron Marston, #2189)
* `RSpec::Core::Configuration#reporter` is now public API under semver.
(Jon Rowe, #2193)
* Add new `config.when_first_matching_example_defined` hook. (Myron
Marston, #2175)

### 3.5.0.beta1 / 2016-02-06
[Full Changelog](http://github.com/rspec/rspec-core/compare/v3.4.3...v3.5.0.beta1)
Expand Down
1 change: 1 addition & 0 deletions features/.nav
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
- before_and_after_hooks.feature
- around_hooks.feature
- filtering.feature
- when_first_matching_example_defined.feature
- subject:
- implicit_subject.feature
- explicit_subject.feature
Expand Down
70 changes: 70 additions & 0 deletions features/hooks/when_first_matching_example_defined.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
Feature: `when_first_matching_example_defined` hook

In large projects that use RSpec, it's common to have some expensive setup logic
that is only needed when certain kinds of specs have been loaded. If that kind of
spec has not been loaded, you'd prefer to avoid the cost of doing the setup.

The `when_first_matching_example_defined` hook makes it easy to conditionally
perform some logic when the first example is defined with matching metadata,
allowing you to ensure the necessary setup is performed only when needed.

Background:
Given a file named "spec/spec_helper.rb" with:
"""ruby
RSpec.configure do |config|
config.when_first_matching_example_defined(:db) do
require "support/db"
end
end
"""
And a file named "spec/support/db.rb" with:
"""ruby
RSpec.configure do |config|
config.before(:suite) do
puts "Bootstrapped the DB."
end
config.around(:example, :db) do |example|
puts "Starting a DB transaction."
example.run
puts "Rolling back a DB transaction."
end
end
"""
And a file named ".rspec" with:
"""
--require spec_helper
"""
And a file named "spec/unit_spec.rb" with:
"""
RSpec.describe "A unit spec" do
it "does not require a database" do
puts "in unit example"
end
end
"""
And a file named "spec/integration_spec.rb" with:
"""
RSpec.describe "An integration spec", :db do
it "requires a database" do
puts "in integration example"
end
end
"""

Scenario: Running the entire suite loads the DB setup
When I run `rspec`
Then it should pass with:
"""
Bootstrapped the DB.
Starting a DB transaction.
in integration example
Rolling back a DB transaction.
.in unit example
.
"""

Scenario: Running just the unit spec does not load the DB setup
When I run `rspec spec/unit_spec.rb`
Then the examples should all pass
And the output should not contain "DB"
35 changes: 35 additions & 0 deletions lib/rspec/core/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1553,6 +1553,41 @@ def define_derived_metadata(*filters, &block)
@derived_metadata_blocks.append(block, meta)
end

# Defines a callback that runs after the first example with matching
# metadata is defined. If no examples are defined with matching metadata,
# it will not get called at all.
#
# This can be used to ensure some setup is performed (such as bootstrapping
# a DB or loading a specific file that adds significantly to the boot time)
# if needed (as indicated by the presence of an example with matching metadata)
# but avoided otherwise.
#
# @example
# RSpec.configure do |config|
# config.when_first_matching_example_defined(:db) do
# # Load a support file that does some heavyweight setup,
# # including bootstrapping the DB, but only if we have loaded
# # any examples tagged with `:db`.
# require 'support/db'
# end
# end
def when_first_matching_example_defined(*filters, &block)
specified_meta = Metadata.build_hash_from(filters, :warn_about_example_group_filtering)

callback = lambda do |example_or_group_meta|
# Example groups do not have `:example_group` metadata
# (instead they have `:parent_example_group` metadata).
return unless example_or_group_meta.key?(:example_group)

# Ensure the callback only fires once.
@derived_metadata_blocks.items_for(specified_meta).delete(callback)

block.call
end

@derived_metadata_blocks.append(callback, specified_meta)
end

# @private
def apply_derived_metadata_to(metadata)
@derived_metadata_blocks.items_for(metadata).each do |block|
Expand Down
4 changes: 3 additions & 1 deletion lib/rspec/core/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ def self.expose_example_group_alias(name)
example_group_aliases << name

(class << RSpec; self; end).__send__(:define_method, name) do |*args, &example_group_block|
RSpec.world.register RSpec::Core::ExampleGroup.__send__(name, *args, &example_group_block)
group = RSpec::Core::ExampleGroup.__send__(name, *args, &example_group_block)
RSpec.world.record(group)
group
end

expose_example_group_alias_globally(name) if exposed_globally?
Expand Down
10 changes: 10 additions & 0 deletions lib/rspec/core/example.rb
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,16 @@ def initialize(example_group_class, description, user_metadata, example_block=ni
@example_group_class = example_group_class
@example_block = example_block

# Register the example with the group before creating the metadata hash.
# This is necessary since creating the metadata hash triggers
# `when_first_matching_example_defined` callbacks, in which users can
# load RSpec support code which defines hooks. For that to work, the
# examples and example groups must be registered at the time the
# support code is called or be defined afterwards.
# Begin defined beforehand but registered afterwards causes hooks to
# not be applied where they should.
example_group_class.examples << self

@metadata = Metadata::ExampleHash.create(
@example_group_class.metadata, user_metadata,
example_group_class.method(:next_runnable_index_for),
Expand Down
59 changes: 38 additions & 21 deletions lib/rspec/core/example_group.rb
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,7 @@ def self.define_example_method(name, extra_options={})
options.update(:skip => RSpec::Core::Pending::NOT_YET_IMPLEMENTED) unless block
options.update(extra_options)

example = RSpec::Core::Example.new(self, desc, options, block)
examples << example
example
RSpec::Core::Example.new(self, desc, options, block)
end
end

Expand Down Expand Up @@ -235,27 +233,27 @@ def self.define_example_group_method(name, metadata={})
thread_data = RSpec::Support.thread_local_data
top_level = self == ExampleGroup

if top_level
if thread_data[:in_example_group]
raise "Creating an isolated context from within a context is " \
"not allowed. Change `RSpec.#{name}` to `#{name}` or " \
"move this to a top-level scope."
registration_collection =
if top_level
if thread_data[:in_example_group]
raise "Creating an isolated context from within a context is " \
"not allowed. Change `RSpec.#{name}` to `#{name}` or " \
"move this to a top-level scope."
end

thread_data[:in_example_group] = true
RSpec.world.example_groups
else
children
end

thread_data[:in_example_group] = true
end

begin

description = args.shift
combined_metadata = metadata.dup
combined_metadata.merge!(args.pop) if args.last.is_a? Hash
args << combined_metadata

subclass(self, description, args, &example_group_block).tap do |child|
children << child
end

subclass(self, description, args, registration_collection, &example_group_block)
ensure
thread_data.delete(:in_example_group) if top_level
end
Expand Down Expand Up @@ -379,9 +377,9 @@ def self.find_and_eval_shared(label, name, inclusion_location, *args, &customiza
# @!endgroup

# @private
def self.subclass(parent, description, args, &example_group_block)
def self.subclass(parent, description, args, registration_collection, &example_group_block)
subclass = Class.new(parent)
subclass.set_it_up(description, *args, &example_group_block)
subclass.set_it_up(description, args, registration_collection, &example_group_block)
subclass.module_exec(&example_group_block) if example_group_block

# The LetDefinitions module must be included _after_ other modules
Expand All @@ -394,7 +392,7 @@ def self.subclass(parent, description, args, &example_group_block)
end

# @private
def self.set_it_up(description, *args, &example_group_block)
def self.set_it_up(description, args, registration_collection, &example_group_block)
# Ruby 1.9 has a bug that can lead to infinite recursion and a
# SystemStackError if you include a module in a superclass after
# including it in a subclass: https://gist.github.com/845896
Expand All @@ -405,6 +403,16 @@ def self.set_it_up(description, *args, &example_group_block)
# here.
ensure_example_groups_are_configured

# Register the example with the group before creating the metadata hash.
# This is necessary since creating the metadata hash triggers
# `when_first_matching_example_defined` callbacks, in which users can
# load RSpec support code which defines hooks. For that to work, the
# examples and example groups must be registered at the time the
# support code is called or be defined afterwards.
# Begin defined beforehand but registered afterwards causes hooks to
# not be applied where they should.
registration_collection << self

user_metadata = Metadata.build_hash_from(args)

@metadata = Metadata::ExampleGroupHash.create(
Expand Down Expand Up @@ -444,10 +452,19 @@ def self.children
# @private
def self.next_runnable_index_for(file)
if self == ExampleGroup
RSpec.world.num_example_groups_defined_in(file)
# We add 1 so the ids start at 1 instead of 0. This is
# necessary for this branch (but not for the other one)
# because we register examples and groups with the
# `childeren` and `examples` collection BEFORE this
Copy link
Member

Choose a reason for hiding this comment

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

s/childeren/children/

Copy link
Member

Choose a reason for hiding this comment

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

Cleaned it up post merge :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks :).

# method is called as part of metadata hash creation,
# but the example group is recorded with
# `RSpec.world.example_group_counts_by_spec_file` AFTER
# the metadata hash is created and the group is returned
# to the caller.
RSpec.world.num_example_groups_defined_in(file) + 1
else
children.count + examples.count
end + 1
end
end

# @private
Expand Down
6 changes: 2 additions & 4 deletions lib/rspec/core/world.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,10 @@ def registered_example_group_files

# @api private
#
# Register an example group.
def register(example_group)
# Records an example group.
def record(example_group)
@configuration.on_example_group_definition_callbacks.each { |block| block.call(example_group) }
example_groups << example_group
@example_group_counts_by_spec_file[example_group.metadata[:absolute_file_path]] += 1
example_group
end

# @private
Expand Down
Loading