diff --git a/.rubocop.yml b/.rubocop.yml index 29d6341570..9a3abf924a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -24,7 +24,7 @@ Metrics/LineLength: # This should go down over time. Metrics/MethodLength: - Max: 37 + Max: 39 # This should go down over time. Metrics/CyclomaticComplexity: diff --git a/Changelog.md b/Changelog.md index fdf6a57e9b..2c62bfded9 100644 --- a/Changelog.md +++ b/Changelog.md @@ -6,6 +6,8 @@ Enhancements: * Improve pluralisation of words ending with `s` (like process). (Joshua Pinter, #2779) * Add ordering by file modification time (most recent first). (Matheus Richard, #2778) * Add `to_s` to reserved names for #let and #subject. (Nick Flückiger, #2886) +* Introduce `RSpec.current_scope` to expose the current scope in which + RSpec is executing. e.g. `:before_example_hook`, `:example` etc. (@odinhb, #2895) Bug fixes: diff --git a/features/metadata/current_scope.feature b/features/metadata/current_scope.feature new file mode 100644 index 0000000000..4dacd710ef --- /dev/null +++ b/features/metadata/current_scope.feature @@ -0,0 +1,87 @@ +Feature: RSpec provides the current scope as RSpec.current_scope + + You can detect which rspec scope your helper methods or library code is executing in. + This is useful if for example, your method only makes sense to call in a certain context. + + Scenario: Detecting the current scope + Given a file named "current_scope_spec.rb" with: + """ruby + # Outside of the test lifecycle, the current scope is `:suite` + exit(1) unless RSpec.current_scope == :suite + + at_exit do + exit(1) unless RSpec.current_scope == :suite + end + + RSpec.configure do |c| + c.before :suite do + expect(RSpec.current_scope).to eq(:before_suite_hook) + end + + c.before :context do + expect(RSpec.current_scope).to eq(:before_context_hook) + end + + c.before :example do + expect(RSpec.current_scope).to eq(:before_example_hook) + end + + c.around :example do |ex| + expect(RSpec.current_scope).to eq(:before_example_hook) + ex.run + expect(RSpec.current_scope).to eq(:after_example_hook) + end + + c.after :example do + expect(RSpec.current_scope).to eq(:after_example_hook) + end + + c.after :context do + expect(RSpec.current_scope).to eq(:after_context_hook) + end + + c.after :suite do + expect(RSpec.current_scope).to eq(:after_suite_hook) + end + end + + RSpec.describe "RSpec.current_scope" do + before :context do + expect(RSpec.current_scope).to eq(:before_context_hook) + end + + before :example do + expect(RSpec.current_scope).to eq(:before_example_hook) + end + + around :example do |ex| + expect(RSpec.current_scope).to eq(:before_example_hook) + ex.run + expect(RSpec.current_scope).to eq(:after_example_hook) + end + + after :example do + expect(RSpec.current_scope).to eq(:after_example_hook) + end + + after :context do + expect(RSpec.current_scope).to eq(:after_context_hook) + end + + it "is :example in an example" do + expect(RSpec.current_scope).to eq(:example) + end + + it "works for multiple examples" do + expect(RSpec.current_scope).to eq(:example) + end + + describe "in nested describe blocks" do + it "still works" do + expect(RSpec.current_scope).to eq(:example) + end + end + end + """ + When I run `rspec current_scope_spec.rb` + Then the examples should all pass diff --git a/lib/rspec/core.rb b/lib/rspec/core.rb index 2f10014b2e..ad9553c9bd 100644 --- a/lib/rspec/core.rb +++ b/lib/rspec/core.rb @@ -129,6 +129,32 @@ def self.current_example=(example) RSpec::Support.thread_local_data[:current_example] = example end + # Set the current scope rspec is executing in + # @api private + def self.current_scope=(scope) + RSpec::Support.thread_local_data[:current_scope] = scope + end + RSpec.current_scope = :suite + + # Get the current RSpec execution scope + # + # Returns (in order of lifecycle): + # * `:suite` as an initial value, this is outside of the test lifecycle. + # * `:before_suite_hook` during `before(:suite)` hooks. + # * `:before_context_hook` during `before(:context)` hooks. + # * `:before_example_hook` during `before(:example)` hooks and `around(:example)` before `example.run`. + # * `:example` within the example run. + # * `:after_example_hook` during `after(:example)` hooks and `around(:example)` after `example.run`. + # * `:after_context_hook` during `after(:context)` hooks. + # * `:after_suite_hook` during `after(:suite)` hooks. + # * `:suite` as a final value, again this is outside of the test lifecycle. + # + # Reminder, `:context` hooks have `:all` alias and `:example` hooks have `:each` alias. + # @return [Symbol] + def self.current_scope + RSpec::Support.thread_local_data[:current_scope] + end + # @private # Internal container for global non-configuration data. def self.world diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index bb38a8de4f..399ae15d6c 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -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 = :suite end end diff --git a/lib/rspec/core/example.rb b/lib/rspec/core/example.rb index d3b891fa92..eb061ee8fa 100644 --- a/lib/rspec/core/example.rb +++ b/lib/rspec/core/example.rb @@ -259,6 +259,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? @@ -278,6 +279,7 @@ def run(example_group_instance, reporter) rescue AllExceptionsExcludingDangerousOnesOnRubiesThatAllowIt => e set_exception(e) ensure + RSpec.current_scope = :after_example_hook run_after_example end end @@ -462,6 +464,7 @@ def hooks end def with_around_example_hooks + RSpec.current_scope = :before_example_hook hooks.run(:around, :example, self) { yield } rescue Support::AllExceptionsExceptOnesWeMustNotRescue => e set_exception(e) diff --git a/lib/rspec/core/example_group.rb b/lib/rspec/core/example_group.rb index 5efee9f386..8a2d7cb381 100644 --- a/lib/rspec/core/example_group.rb +++ b/lib/rspec/core/example_group.rb @@ -602,6 +602,7 @@ def self.run(reporter=RSpec::Core::NullReporter) should_run_context_hooks = descendant_filtered_examples.any? begin + RSpec.current_scope = :before_context_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? @@ -614,6 +615,7 @@ 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_context_hook run_after_context_hooks(new('after(:context) hook')) if should_run_context_hooks reporter.example_group_finished(self) end diff --git a/spec/rspec/core_spec.rb b/spec/rspec/core_spec.rb index 085acbbbab..d9a02e97e7 100644 --- a/spec/rspec/core_spec.rb +++ b/spec/rspec/core_spec.rb @@ -129,6 +129,20 @@ end end + describe ".current_scope" do + before :context do + expect(RSpec.current_scope).to eq(:before_context_hook) + end + + before do + expect(RSpec.current_scope).to eq(:before_example_hook) + end + + it "returns :example inside an example" do + expect(RSpec.current_scope).to eq(:example) + end + end + describe ".reset" do it "resets the configuration and world objects" do config_before_reset = RSpec.configuration