Skip to content

Commit

Permalink
Revise before(:all)/let behavior based on feedback from @dchelimsky.
Browse files Browse the repository at this point in the history
- This is closer to the behavior we've had in 2.12 and earlier. `let`
  declarations that are accessed in `before(:all)` will preserve their
  memoized value for all examples in the group.
- On 2.12 and earlier, this would cause additional `let` declarations
  that were not accessed in `before(:all)` but were accessed in individual
  examples to "leak", because the same `__@memoized` ivar got shared
  with each example in the group, and would be mutated each time an
  additional `let` declaration was accessed. This commit changes
  that behavior. The fact that one `let` declaration is accessed in
  `before(:all)` does not affect the lifecycle of other `let` declarations
  in any way.
  • Loading branch information
myronmarston committed Mar 9, 2013
1 parent 53d3c0b commit e6714c0
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 41 deletions.
14 changes: 1 addition & 13 deletions lib/rspec/core/example_group.rb
Original file line number Diff line number Diff line change
Expand Up @@ -316,26 +316,14 @@ def self.run_before_all_hooks(example_group_instance)
begin
assign_before_all_ivars(superclass.before_all_ivars, example_group_instance)

handle_memoized_methods(example_group_instance) do
BeforeAllMemoizedHash.isolate_for_before_all(example_group_instance) do
run_hook(:before, :all, example_group_instance)
end
ensure
store_before_all_ivars(example_group_instance)
end
end

def self.handle_memoized_methods(example_group_instance)
example_group_instance.instance_eval do
@__memoized = BeforeAllMemoizedHash.new

begin
yield
ensure
@__memoized = nil
end
end
end

# @private
def self.run_around_each_hooks(example, initial_procsy)
run_hook(:around, :each, example, initial_procsy)
Expand Down
32 changes: 30 additions & 2 deletions lib/rspec/core/memoized_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,24 @@ def __memoized
#
# @private
class BeforeAllMemoizedHash
def initialize
def initialize(example_group_instance)
@example_group_instance = example_group_instance
@hash = {}
end

def self.isolate_for_before_all(example_group_instance)
example_group_instance.instance_eval do
@__memoized = BeforeAllMemoizedHash.new(self)

begin
yield
ensure
@__memoized.preserve_accessed_lets
@__memoized = nil
end
end
end

def fetch(key, &block)
description = if key == :subject
"subject"
Expand All @@ -98,8 +112,12 @@ def fetch(key, &block)
WARNING: #{description} accessed in a `before(:all)` hook at:
#{caller[1]}
This is deprecated behavior that will not be supported in RSpec 3.
`let` and `subject` declarations are not intended to be called
in a `before(:all)` hook. The memoized value will be discarded.
in a `before(:all)` hook, as they exist to define state that
is reset between each example, while `before(:all)` exists to
define state that is shared across examples in an example group.
EOS

@hash.fetch(key, &block)
Expand All @@ -108,6 +126,16 @@ def fetch(key, &block)
def []=(key, value)
@hash[key] = value
end

def preserve_accessed_lets
hash = @hash

@example_group_instance.class.class_eval do
hash.each do |key, value|
define_method(key) { value }
end
end
end
end

def self.included(mod)
Expand Down
82 changes: 56 additions & 26 deletions spec/rspec/core/memoized_helpers_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,40 +128,55 @@ def subject_value_for(describe_arg, &block)
end

def define_and_run_group
final_subject_value_in_before_all = subject_value_in_example = nil
reference_lines = []
values = { :reference_lines => [] }

ExampleGroup.describe do
subject { [1, 2] }
let(:list) { %w[ a b ] }

before(:all) do
subject << 3; reference_lines << __LINE__
final_subject_value_in_before_all = subject; reference_lines << __LINE__
subject << 3; values[:reference_lines] << __LINE__
values[:final_subject_value_in_before_all] = subject; values[:reference_lines] << __LINE__
end

example { subject_value_in_example = subject }
example do
list << '1'
values[:list_in_ex_1] = list
values[:subject_value_in_example] = subject
end

example do
list << '2'
values[:list_in_ex_2] = list
end
end.run

return final_subject_value_in_before_all, subject_value_in_example, reference_lines
values
end

it 'memoizes the value within the before(:all) hook' do
value, _, _ = define_and_run_group
expect(value).to eq([1, 2, 3])
values = define_and_run_group
expect(values.fetch(:final_subject_value_in_before_all)).to eq([1, 2, 3])
end

it 'preserves the memoization into the individual examples' do
values = define_and_run_group
expect(values.fetch(:subject_value_in_example)).to eq([1, 2, 3])
end

it 'clears the memoization before the first example' do
_, value, _ = define_and_run_group
expect(value).to eq([1, 2])
it 'does not cause other lets to be shared across examples' do
values = define_and_run_group
expect(values.fetch(:list_in_ex_1)).to eq(%w[ a b 1 ])
expect(values.fetch(:list_in_ex_2)).to eq(%w[ a b 2 ])
end

it 'prints a warning since `subject` declarations are not intended to be used in :all hooks' do
msgs = []
::RSpec.stub(:warn_deprecation) { |msg| msgs << msg }

_, _, lines = define_and_run_group
values = define_and_run_group

expect(msgs).to include(*lines.map { |line|
expect(msgs).to include(*values[:reference_lines].map { |line|
match(/subject accessed.*#{__FILE__}:#{line}/m)
})
end
Expand Down Expand Up @@ -520,40 +535,55 @@ def count
end

def define_and_run_group
line = final_list_value_in_before_all = list_value_in_example = nil
reference_lines = []
values = { :reference_lines => [] }

ExampleGroup.describe do
let(:list) { [1, 2] }
subject { %w[ a b ] }

before(:all) do
list << 3; reference_lines << __LINE__
final_list_value_in_before_all = list; reference_lines << __LINE__
list << 3; values[:reference_lines] << __LINE__
values[:final_list_value_in_before_all] = list; values[:reference_lines] << __LINE__
end

example do
subject << "1"
values[:subject_in_ex_1] = subject
values[:list_value_in_example] = list
end

example { list_value_in_example = list }
example do
subject << "2"
values[:subject_in_ex_2] = subject
end
end.run

return final_list_value_in_before_all, list_value_in_example, reference_lines
values
end

it 'memoizes the value within the before(:all) hook' do
value, _, _ = define_and_run_group
expect(value).to eq([1, 2, 3])
values = define_and_run_group
expect(values.fetch(:final_list_value_in_before_all)).to eq([1, 2, 3])
end

it 'preserves the memoized value into the examples' do
values = define_and_run_group
expect(values.fetch(:list_value_in_example)).to eq([1, 2, 3])
end

it 'clears the memoization before the first example' do
_, value, _ = define_and_run_group
expect(value).to eq([1, 2])
it 'does not cause the subject to be shared across examples' do
values = define_and_run_group
expect(values.fetch(:subject_in_ex_1)).to eq(%w[ a b 1 ])
expect(values.fetch(:subject_in_ex_2)).to eq(%w[ a b 2 ])
end

it 'prints a warning since `let` declarations are not intended to be used in :all hooks' do
msgs = []
::RSpec.stub(:warn_deprecation) { |msg| msgs << msg }

_, _, lines = define_and_run_group
values = define_and_run_group

expect(msgs).to include(*lines.map { |line|
expect(msgs).to include(*values[:reference_lines].map { |line|
match(/let declaration `list` accessed.*#{__FILE__}:#{line}/m)
})
end
Expand Down

0 comments on commit e6714c0

Please sign in to comment.