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

Aliasing API for #describe #870

Closed
wants to merge 15 commits into from
Closed
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
107 changes: 107 additions & 0 deletions features/example_groups/aliasing.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
Feature: aliasing
`describe` and `context` are the default aliases for `example_group`.
`describe` is defined at the top level, i.e. on the main Object.
`context` is only available from within an example group, i.e. within
a describe block. You can describe your own aliases for `example_group`
and give those custom aliases default meta data.

You can also make these aliases available at the top-level of your
specs, just add :toplevel_alias as an option.

By default, top level aliases are included in the main- and the
Module-namespace. This can be avoided by running with the option
`--no-toplevel-dsl`. You can always access the DSL-methods through the
RSpec-Module, e.g. `RSpec.describe`.

Scenario: custom example group aliases with metadata
Given a file named "nested_example_group_aliases_spec.rb" with:
"""ruby
RSpec.configure do |c|
c.alias_example_group_to :detail, :detailed => true, :focused => false
end

describe "a thing" do
describe "in broad strokes" do
it "can do things" do
end
end

detail "something less important" do
it "can do an unimportant thing" do
end
end
end
"""
When I run `rspec nested_example_group_aliases_spec.rb --tag detailed -fdoc`
Then the output should contain:
"""
a thing
something less important
"""
Copy link
Member

Choose a reason for hiding this comment

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

I think it's also important to have a step that says:

And the output should not contain "in broad strokes"

As this scenario stands, it could feasible produce the output you've specified here while not actually skipping the "broad strokes" example -- i.e. if the randomization feature was being used.

And the output should not contain:
"""
in broad strokes
"""

Scenario: custom example group alias at the top-level
Given a file named "top_level_example_group_aliases_spec.rb" with:
"""ruby
RSpec.configure do |c|
c.toplevel_alias_example_group_to :detail
end

detail "a thing" do
it "works" do
end
end
"""
When I run `rspec top_level_example_group_aliases_spec.rb -fdoc`
Then the output should contain:
"""
a thing
works
"""

Scenario: Turn off toplevel methods
Given a file named "top_level_example_group_aliases_spec.rb" with:
"""ruby
describe "is not available" do
end
"""
When I run `rspec --no-toplevel-dsl top_level_example_group_aliases_spec.rb -fdoc`
Then the output should contain:
"""
undefined method `describe'
"""
Copy link
Member

Choose a reason for hiding this comment

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

I believe this option will also disable other top-level DSL methods like shared_examples_for and shared_context, right? Is there a reasonable way to demonstrate/document that here?


Scenario: Turn off toplevel methods
Given a file named "top_level_example_group_aliases_spec.rb" with:
"""ruby
shared_context "is not available" do
end
"""
When I run `rspec --no-toplevel-dsl top_level_example_group_aliases_spec.rb -fdoc`
Then the output should contain:
"""
undefined method `shared_context'
"""

Scenario: Access example group aliases through RSpec
Given a file named "example_group_aliases_in_namespace_spec.rb" with:
"""ruby
RSpec.configure do |c|
c.toplevel_alias_example_group_to :detail
end

RSpec.detail "aliases" do
it "are available in the RSpec module" do
end
end
"""
When I run `rspec --no-toplevel-dsl example_group_aliases_in_namespace_spec.rb -fdoc`
Then the output should contain:
"""
aliases
are available in the RSpec module
"""

1 change: 1 addition & 0 deletions lib/rspec/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

module RSpec
autoload :SharedContext, 'rspec/core/shared_context'
extend Core::DSL

# @private
def self.wants_to_quit
Expand Down
13 changes: 12 additions & 1 deletion lib/rspec/core/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ def self.add_setting(name, opts={})
# `"spec"`). Allows you to just type `rspec` instead of `rspec spec` to
# run all the examples in the `spec` directory.
add_setting :default_path
add_setting :toplevel_dsl

# Run examples over DRb (default: `false`). RSpec doesn't supply the DRb
# server, but you can use tools like spork.
Expand Down Expand Up @@ -574,7 +575,7 @@ def profile_examples
# @private
def files_or_directories_to_run=(*files)
files = files.flatten
files << default_path if (command == 'rspec' || Runner.running_in_drb?) && default_path && files.empty?
files << default_path if (command == 'rspec' || Runner.instance.running_in_drb?) && default_path && files.empty?
self.files_to_run = get_files_to_run(files)
end

Expand Down Expand Up @@ -604,6 +605,16 @@ def alias_example_to(new_name, *args)
RSpec::Core::ExampleGroup.alias_example_to(new_name, extra_options)
end

def alias_example_group_to(new_name, *metadata)
extra_options = build_metadata_hash_from(metadata)
RSpec::Core::ExampleGroup.alias_example_group_to(new_name, extra_options)
end

def toplevel_alias_example_group_to(new_name, *metadata)
alias_example_group_to(new_name, metadata)
RSpec::Core::DSL.register_example_group_alias(new_name)
end

# Define an alias for it_should_behave_like that allows different
# language (like "it_has_behavior" or "it_behaves_like") to be
# employed when including shared examples.
Expand Down
2 changes: 1 addition & 1 deletion lib/rspec/core/configuration_options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def filter_manager

NON_FORCED_OPTIONS = [
:debug, :requires, :libs, :profile, :drb, :files_or_directories_to_run,
:line_numbers, :full_description, :full_backtrace, :tty
:line_numbers, :full_description, :full_backtrace, :tty, :toplevel_dsl
].to_set

MERGED_OPTIONS = [:requires, :libs].to_set
Expand Down
22 changes: 14 additions & 8 deletions lib/rspec/core/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@ module RSpec
module Core
# Adds the `describe` method to the top-level namespace.
module DSL
# Generates a subclass of {ExampleGroup}
# Generates a method that passes on the message to
# generate a subclass of {ExampleGroup}
#
def self.register_example_group_alias(name)
Copy link
Member

Choose a reason for hiding this comment

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

Any reason for the blank line between the comment and here? I'm not sure if YARD picks up on the comment applying to this method if there's a blank line in between...

define_method(name) do |*args, &example_group_block|
RSpec::Core::ExampleGroup.send(name, *args, &example_group_block).register
end
Copy link
Member

Choose a reason for hiding this comment

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

This syntax causes 1.8.6 to blow up. (Its parser can't handle blocks accepting blocks).

We're planning on dropping 1.8.6 support in 3.0, and starting on 3.0 as soon as 2.14 ships, so we can just hold off on merging this until we get started on 3.0. If you want to get this in 2.14 you'll have to change stuff so it's 1.8.6 compatible, but that would be a bunch of work that we would undo as soon as 3.0 starts, because we don't want to keep the 1.8.6 hacks in 3.0.

Thoughts?

end

# By default, #describe is available at the top-level
# to generate subclasses of {ExampleGroup}
#
# ## Examples:
#
Expand All @@ -13,14 +23,10 @@ module DSL
# end
#
# @see ExampleGroup
# @see ExampleGroup.describe
def describe(*args, &example_group_block)
RSpec::Core::ExampleGroup.describe(*args, &example_group_block).register
end
# @see ExampleGroup.example_group
register_example_group_alias(:describe)

end
end
end

extend RSpec::Core::DSL
Module.send(:include, RSpec::Core::DSL)

18 changes: 16 additions & 2 deletions lib/rspec/core/example_group.rb
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,19 @@ def #{new_name}(name, *args, &customization_block)
def alias_it_behaves_like_to name, *args, &block
(class << self; self; end).define_nested_shared_group_method name, *args, &block
end

# Works like `alias_method :name, :example_group` with the added benefit of
# assigning default metadata to the generated example group.
#
# @note Use with caution. This extends the language used in your
# specs, but does not add any additional documentation.
def alias_example_group_to(name, metadata={})
(class << self; self; end).send(:define_method, name) do |*args, &block|
combined_metadata = metadata.dup
combined_metadata.merge!(args.pop) if args.last.is_a? Hash
example_group(*(args << combined_metadata), &block)
end
end
end

# Includes shared content mapped to `name` directly in the group in which
Expand Down Expand Up @@ -216,7 +229,7 @@ def self.superclass_metadata
# end
#
# @see DSL#describe
def self.describe(*args, &example_group_block)
def self.example_group(*args, &example_group_block)
@_subclass_count ||= 0
@_subclass_count += 1
args << {} unless args.last.is_a?(Hash)
Expand All @@ -232,7 +245,8 @@ def self.describe(*args, &example_group_block)
end

class << self
alias_method :context, :describe
alias_method :describe, :example_group
alias_method :context, :example_group
end

# @private
Expand Down
7 changes: 5 additions & 2 deletions lib/rspec/core/option_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ class << self
end

def parse!(args)
return {} if args.empty?

convert_deprecated_args(args)

options = args.delete('--tty') ? {:tty => true} : {}
Expand Down Expand Up @@ -95,6 +93,11 @@ def parser(options)
options[:drb_port] = o.to_i
end

options[:toplevel_dsl] = true
parser.on('--[no-]toplevel-dsl', "Include DSL methods into main Object (on by default)") do |o|
options[:toplevel_dsl] = o
end

parser.on('--init', 'Initialize your project with RSpec.') do |cmd|
ProjectInitializer.new(cmd).run
exit
Expand Down
91 changes: 64 additions & 27 deletions lib/rspec/core/runner.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
module RSpec
module Core
class Runner
class << self
#we need access to the main object in .set_up_dsl
attr_accessor :main_object
end

def self.instance
@instance ||= new
end

# Register an at_exit hook that runs the suite.
def self.autorun
return if autorun_disabled? || installed_at_exit? || running_in_drb?
instance.autorun
end

# Register an at_exit hook that runs the suite.
def autorun
return if installed_at_exit? || running_in_drb?

configure_and_set_up(ARGV)
at_exit do
# Don't bother running any specs and just let the program terminate
# if we got here due to an unrescued exception (anything other than
Expand All @@ -14,39 +28,19 @@ def self.autorun
# We got here because either the end of the program was reached or
# somebody called Kernel#exit. Run the specs and then override any
# existing exit status with RSpec's exit status if any specs failed.
status = run(ARGV, $stderr, $stdout).to_i
status = run.to_i
exit status if status != 0
end
@installed_at_exit = true
end
AT_EXIT_HOOK_BACKTRACE_LINE = "#{__FILE__}:#{__LINE__ - 2}:in `autorun'"

def self.disable_autorun!
@autorun_disabled = true
end

def self.autorun_disabled?
@autorun_disabled ||= false
end

def self.installed_at_exit?
@installed_at_exit ||= false
end

def self.running_in_drb?
def running_in_drb?
defined?(DRb) &&
(DRb.current_server rescue false) &&
DRb.current_server.uri =~ /druby\:\/\/127.0.0.1\:/
end

def self.trap_interrupt
trap('INT') do
exit!(1) if RSpec.wants_to_quit
RSpec.wants_to_quit = true
STDERR.puts "\nExiting... Interrupt again to exit immediately."
end
end

# Run a suite of RSpec examples.
#
# This is used internally by RSpec to run a suite, but is available
Expand All @@ -63,10 +57,13 @@ def self.trap_interrupt
#
# #### Returns
# * +Fixnum+ - exit status code (0/1)
def self.run(args, err=$stderr, out=$stdout)
def self.run(*args)
instance.run(*args)
end

def run(args=[], err=$stderr, out=$stdout)
trap_interrupt
options = ConfigurationOptions.new(args)
options.parse_options
configure_and_set_up(args)

if options.options[:drb]
require 'rspec/core/drb_command_line'
Expand All @@ -82,6 +79,46 @@ def self.run(args, err=$stderr, out=$stdout)
ensure
RSpec.reset
end

private
def options
@options
end

def installed_at_exit?
@installed_at_exit ||= false
end

def trap_interrupt
trap('INT') do
exit!(1) if RSpec.wants_to_quit
RSpec.wants_to_quit = true
STDERR.puts "\nExiting... Interrupt again to exit immediately."
end
end

def set_up_dsl
return if !options.options[:toplevel_dsl] || @dsl_setup_done

self.class.main_object.send(:extend, RSpec::Core::DSL)
Module.send(:include, RSpec::Core::DSL)
@dsl_setup_done = true
end

def configure_and_set_up(args)
return if args.empty? && !@options.nil?
@options = begin
options = ConfigurationOptions.new(args)
options.parse_options

options
end

set_up_dsl
end
end
end
end

RSpec::Core::Runner.main_object = self

4 changes: 1 addition & 3 deletions lib/rspec/core/shared_example_group.rb
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,4 @@ def ensure_block_has_source_location(block, caller_line)
end
end

extend RSpec::Core::SharedExampleGroup
Module.send(:include, RSpec::Core::SharedExampleGroup)

RSpec::Core::DSL.send(:include, RSpec::Core::SharedExampleGroup)
Loading