Skip to content

Commit

Permalink
Implement RSpec.current_scope w/ introspection
Browse files Browse the repository at this point in the history
Demonstrating possible extension
  • Loading branch information
odinhb committed Jun 20, 2021
1 parent 132f5e1 commit 76f85c1
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 0 deletions.
49 changes: 49 additions & 0 deletions lib/rspec/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,55 @@ def self.current_example=(example)
RSpec::Support.thread_local_data[:current_example] = example
end

class ScopeSymbol
# @api private
EXAMPLE_CONTEXT = [:before_each_hook, :example, :after_each_hook]

# @api private
# Used whenever you set RSpec.current_scope = :something
def self.wrap(thing)
thing.is_a?(self) ? thing : new(thing)
end

# @private
def initialize(sym)
raise ArgumentError, "I only wrap symbols" unless sym.is_a?(Symbol) || sym.nil?
@sym = sym
end

# Allows comparisons w/ real symbols
def ==(other)
@sym == other
end

# Find out whether you're in the execution context of a single example (either before, during or after)
def in_example_context?
EXAMPLE_CONTEXT.include?(@sym)
end
end

# Set the current scope rspec is executing in
# @api private
def self.current_scope=(scope)
RSpec::Support.thread_local_data[:current_scope] = ScopeSymbol.wrap(scope)
end
RSpec.current_scope = :suite

# Get the current scope rspec is currently executing in.
#
# Returns `:suite` while loading your tests
#
# Returns `:before_each_hook`/`:before_all_hook`/`:before_suite_hook`/`:after_each_hook`/`:after_all_hook`/`:after_suite_hook`
# in the respective hooks
#
# Returns `:example` inside it/example blocks
#
# Returns `nil` after your suite and all hooks are done
# @return [ScopeSymbol/Nil]
def self.current_scope
RSpec::Support.thread_local_data[:current_scope]
end

# @private
# Internal container for global non-configuration data.
def self.world
Expand Down
3 changes: 3 additions & 0 deletions lib/rspec/core/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2063,10 +2063,13 @@ def with_suite_hooks
return yield if dry_run?

begin
RSpec.current_scope = :before_suite_hook
run_suite_hooks("a `before(:suite)` hook", @before_suite_hooks)
yield
ensure
RSpec.current_scope = :after_suite_hook
run_suite_hooks("an `after(:suite)` hook", @after_suite_hooks)
RSpec.current_scope = nil
end
end

Expand Down
7 changes: 7 additions & 0 deletions lib/rspec/core/example.rb
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ def skipped?
# the instance of {ExampleGroup}.
# @param example_group_instance the instance of an ExampleGroup subclass
def run(example_group_instance, reporter)
prev_scope = RSpec.current_scope
# RSpec.current_scope = :example
@example_group_instance = example_group_instance
@reporter = reporter
RSpec.configuration.configure_example(self, hooks)
Expand All @@ -259,6 +261,7 @@ def run(example_group_instance, reporter)
with_around_and_singleton_context_hooks do
begin
run_before_example
RSpec.current_scope = :example
@example_group_instance.instance_exec(self, &@example_block)

if pending?
Expand All @@ -278,6 +281,7 @@ def run(example_group_instance, reporter)
rescue AllExceptionsExcludingDangerousOnesOnRubiesThatAllowIt => e
set_exception(e)
ensure
RSpec.current_scope = :after_each_hook
run_after_example
end
end
Expand All @@ -286,12 +290,14 @@ def run(example_group_instance, reporter)
set_exception(e)
ensure
@example_group_instance = nil # if you love something... let it go
RSpec.current_scope = prev_scope
end

finish(reporter)
ensure
execution_result.ensure_timing_set(clock)
RSpec.current_example = nil
RSpec.current_scope = prev_scope
end

if RSpec::Support::Ruby.jruby? || RUBY_VERSION.to_f < 1.9
Expand Down Expand Up @@ -462,6 +468,7 @@ def hooks
end

def with_around_example_hooks
RSpec.current_scope = :before_each_hook
hooks.run(:around, :example, self) { yield }
rescue Support::AllExceptionsExceptOnesWeMustNotRescue => e
set_exception(e)
Expand Down
4 changes: 4 additions & 0 deletions lib/rspec/core/example_group.rb
Original file line number Diff line number Diff line change
Expand Up @@ -598,10 +598,12 @@ def self.run_after_context_hooks(example_group_instance)
# Runs all the examples in this group.
def self.run(reporter=RSpec::Core::NullReporter)
return if RSpec.world.wants_to_quit
prev_scope = RSpec.current_scope
reporter.example_group_started(self)

should_run_context_hooks = descendant_filtered_examples.any?
begin
RSpec.current_scope = :before_all_hook
run_before_context_hooks(new('before(:context) hook')) if should_run_context_hooks
result_for_this_group = run_examples(reporter)
results_for_descendants = ordering_strategy.order(children).map { |child| child.run(reporter) }.all?
Expand All @@ -614,8 +616,10 @@ def self.run(reporter=RSpec::Core::NullReporter)
RSpec.world.wants_to_quit = true if reporter.fail_fast_limit_met?
false
ensure
RSpec.current_scope = :after_all_hook
run_after_context_hooks(new('after(:context) hook')) if should_run_context_hooks
reporter.example_group_finished(self)
RSpec.current_scope = prev_scope
end
end

Expand Down
86 changes: 86 additions & 0 deletions spec/rspec/core_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,92 @@
end
end

describe ".current_scope" do
unless RSpec.current_scope == :suite
raise "bad current scope: #{RSpec.current_scope.inspect}"
end

if RSpec.current_scope.in_example_context?
raise "this is not an example context"
end

RSpec.configure do |c|
c.before :suite do
expect(RSpec.current_scope).to eq(:before_suite_hook)
expect(RSpec.current_scope.in_example_context?).to eq(false)
end

c.before :all do
expect(RSpec.current_scope).to eq(:before_all_hook)
expect(RSpec.current_scope.in_example_context?).to eq(false)
end

c.before :each do
expect(RSpec.current_scope).to eq(:before_each_hook)
expect(RSpec.current_scope.in_example_context?).to eq(true)
end

c.around :each do |ex|
expect(RSpec.current_scope).to eq(:before_each_hook)
expect(RSpec.current_scope.in_example_context?).to eq(true)
ex.run
expect(RSpec.current_scope.in_example_context?).to eq(true)
expect(RSpec.current_scope).to eq(:after_each_hook)
end

c.after :each do
expect(RSpec.current_scope).to eq(:after_each_hook)
expect(RSpec.current_scope.in_example_context?).to eq(true)
end

c.after :all do
expect(RSpec.current_scope).to eq(:after_all_hook)
expect(RSpec.current_scope.in_example_context?).to eq(false)
end

c.after :suite do
expect(RSpec.current_scope).to eq(:after_suite_hook)
expect(RSpec.current_scope.in_example_context?).to eq(false)
end
end

before :all do
expect(RSpec.current_scope.in_example_context?).to eq(false)
expect(RSpec.current_scope).to eq(:before_all_hook)
end

before :each do
expect(RSpec.current_scope).to eq(:before_each_hook)
expect(RSpec.current_scope.in_example_context?).to eq(true)
end

around :each do |ex|
expect(RSpec.current_scope).to eq(:before_each_hook)
ex.run
expect(RSpec.current_scope).to eq(:after_each_hook)
end

after :each do
expect(RSpec.current_scope.in_example_context?).to eq(true)
expect(RSpec.current_scope).to eq(:after_each_hook)
end

after :all do
expect(RSpec.current_scope).to eq(:after_all_hook)
expect(RSpec.current_scope.in_example_context?).to eq(false)
end

it "returns :example inside an example" do
expect(RSpec.current_scope).to eq(:example)
expect(RSpec.current_scope.in_example_context?).to eq(true)
end

it "works for more than one example in a describe block" do
expect(RSpec.current_scope).to eq(:example)
expect(RSpec.current_scope.in_example_context?).to eq(true)
end
end

describe ".reset" do
it "resets the configuration and world objects" do
config_before_reset = RSpec.configuration
Expand Down

0 comments on commit 76f85c1

Please sign in to comment.