-
-
Notifications
You must be signed in to change notification settings - Fork 754
Remove monkey patching #1036
Remove monkey patching #1036
Changes from all commits
2075b62
98fc0ed
a0e0a65
bdf3f1c
c032328
4bd801e
125d939
dd7eabb
0005890
55a7064
c425dca
1ac02ab
aa9c1ff
a7fee30
8740e3b
1da935e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
Feature: Global namespace DSL | ||
|
||
RSpec has a few top-level constructs that allow you to begin describing | ||
behaviour: | ||
|
||
* `RSpec.describe`: Define a named context for a group of examples. | ||
* `RSpec.shared_examples_for`: Define a set of shared examples that can later be included in an example group. | ||
* `RSpec.shared_context`: define some common context (using `before`, `let`, helper methods, etc) that can later be included in an example group. | ||
|
||
Historically, these constructs have been available directly off of the main | ||
object, so that you could use these at the start of a file without the | ||
`RSpec.` prefix. They have also been available off of any class or module so | ||
that you can scope your examples within a particular constant namespace. | ||
|
||
RSpec 3 now provides an option to disable this global monkey patching: | ||
|
||
`config.expose_dsl_globally = false`. | ||
|
||
For backwards compatibility it defaults to true. | ||
|
||
Scenario: by default RSpec allows the DSL to be used globally | ||
Given a file named "spec/example_spec.rb" with: | ||
"""ruby | ||
describe "specs here" do | ||
it "passes" do | ||
end | ||
end | ||
""" | ||
When I run `rspec` | ||
Then the output should contain "1 example, 0 failures" | ||
|
||
Scenario: when exposing globally is disabled the top level DSL no longer works | ||
Given a file named "spec/example_spec.rb" with: | ||
"""ruby | ||
RSpec.configure { |c| c.expose_dsl_globally = false } | ||
describe "specs here" do | ||
it "passes" do | ||
end | ||
end | ||
""" | ||
When I run `rspec` | ||
Then the output should contain "undefined method `describe'" | ||
|
||
Scenario: regardless of setting | ||
Given a file named "spec/example_spec.rb" with: | ||
"""ruby | ||
RSpec.configure { |c| c.expose_dsl_globally = true } | ||
RSpec.describe "specs here" do | ||
it "passes" do | ||
end | ||
end | ||
""" | ||
When I run `rspec` | ||
Then the output should contain "1 example, 0 failures" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,26 +1,66 @@ | ||
module RSpec | ||
|
||
# Defines a named context for one or more examples. The given block | ||
# is evaluated in the context of a generated subclass of | ||
# {RSpec::Core::ExampleGroup} | ||
# | ||
# ## Examples: | ||
# | ||
# describe "something" do | ||
# it "does something" do | ||
# # example code goes here | ||
# end | ||
# end | ||
# | ||
# @see ExampleGroup | ||
# @see ExampleGroup.describe | ||
def self.describe(*args, &example_group_block) | ||
RSpec::Core::ExampleGroup.describe(*args, &example_group_block).register | ||
end | ||
|
||
module Core | ||
# Adds the `describe` method to the top-level namespace. | ||
module DSL | ||
# Generates a subclass of {ExampleGroup} | ||
# | ||
# ## Examples: | ||
# | ||
# describe "something" do | ||
# it "does something" do | ||
# # example code goes here | ||
# end | ||
# end | ||
# | ||
# @see ExampleGroup | ||
# @see ExampleGroup.describe | ||
def describe(*args, &example_group_block) | ||
RSpec::Core::ExampleGroup.describe(*args, &example_group_block).register | ||
|
||
class << self | ||
# @private | ||
attr_accessor :top_level | ||
end | ||
|
||
# @private | ||
def self.exposed_globally? | ||
@exposed_globally ||= false | ||
end | ||
|
||
# Add's the describe method to Module and the top level binding | ||
def self.expose_globally! | ||
return if exposed_globally? | ||
|
||
to_define = proc do | ||
def describe(*args, &block) | ||
::RSpec.describe(*args, &block) | ||
end | ||
end | ||
|
||
top_level.instance_eval(&to_define) | ||
Module.class_exec(&to_define) | ||
@exposed_globally = true | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One suggestion here...I think this can be simplified with an alternate approch:
That way, you don't have to apply all of this to two different contexts. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The point is not to monkey patch! By doing that we'd always monkey patch and then have to control that... |
||
end | ||
|
||
def self.remove_globally! | ||
return unless exposed_globally? | ||
|
||
to_undefine = proc do | ||
undef describe | ||
end | ||
|
||
top_level.instance_eval(&to_undefine) | ||
Module.class_exec(&to_undefine) | ||
@exposed_globally = false | ||
end | ||
|
||
end | ||
end | ||
end | ||
|
||
extend RSpec::Core::DSL | ||
Module.send(:include, RSpec::Core::DSL) | ||
|
||
# cature main without an eval | ||
::RSpec::Core::DSL.top_level = self |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,17 +41,49 @@ def shared_example_groups | |
end | ||
|
||
module TopLevelDSL | ||
def shared_examples(*args, &block) | ||
SharedExampleGroup.registry.add_group('main', *args, &block) | ||
def self.definitions | ||
proc do | ||
def shared_examples(*args, &block) | ||
SharedExampleGroup.registry.add_group('main', *args, &block) | ||
end | ||
alias :shared_context :shared_examples | ||
alias :share_examples_for :shared_examples | ||
alias :shared_examples_for :shared_examples | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Huh, I never knew you could pass symbols to Nothing needs to be changed here...I just found it interesting :). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI the difference is the bare name will error if the method doesn't exist. I just got used to using symbols... |
||
def shared_example_groups | ||
SharedExampleGroup.registry.shared_example_groups_for('main') | ||
end | ||
end | ||
end | ||
|
||
alias_method :shared_context, :shared_examples | ||
alias_method :share_examples_for, :shared_examples | ||
alias_method :shared_examples_for, :shared_examples | ||
# @private | ||
def self.exposed_globally? | ||
@exposed_globally ||= false | ||
end | ||
|
||
def shared_example_groups | ||
SharedExampleGroup.registry.shared_example_groups_for('main') | ||
def self.expose_globally! | ||
return if exposed_globally? | ||
|
||
Core::DSL.top_level.instance_eval(&definitions) | ||
Module.class_exec(&definitions) | ||
@exposed_globally = true | ||
end | ||
|
||
def self.remove_globally! | ||
return unless exposed_globally? | ||
|
||
to_undefine = proc do | ||
undef shared_examples | ||
undef shared_context | ||
undef share_examples_for | ||
undef shared_examples_for | ||
undef shared_example_groups | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't realize we had so many methods here. In particular, I didn't realize I feel like 3.0 is a good time to consider cleaning up having so many aliases for the same thing. Honestly, I'm not sure on the history of how they all came about... Thoughts? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BTW, I definitely don't want to hold up this PR for this -- mostly I was just thinking aloud. If you like, we can open an issue for this and come back to it later. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thats the current public api, which also all do the same thing incidentally. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know that's the current public API. My point is that it seems excessive to have so many methods that do the same thing. However, Regardless, as you said, it's what we currently have so I'm fine punting on this for another day as long as we open an issue so we don't forget about it. |
||
end | ||
|
||
Core::DSL.top_level.instance_eval(&to_undefine) | ||
Module.class_exec(&to_undefine) | ||
@exposed_globally = false | ||
end | ||
|
||
end | ||
|
||
def self.registry | ||
|
@@ -140,7 +172,6 @@ def ensure_block_has_source_location(block, caller_line) | |
end | ||
end | ||
end | ||
end | ||
|
||
extend RSpec::Core::SharedExampleGroup::TopLevelDSL | ||
Module.send(:include, RSpec::Core::SharedExampleGroup::TopLevelDSL) | ||
instance_eval &Core::SharedExampleGroup::TopLevelDSL.definitions | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
||=
has funny semantics withfalse
. It doesn't memoize like truthy values do. Should this just be@exposed_globally
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's just preventing a warning being issued.