1# Holds the state of the +describe+ block that is being
2# evaluated. Every example (i.e. +it+ block) is evaluated
3# in a context, which may include state set up in <tt>before
4# :each</tt> or <tt>before :all</tt> blocks.
5#
6#--
7# A note on naming: this is named _ContextState_ rather
8# than _DescribeState_ because +describe+ is the keyword
9# in the DSL for referring to the context in which an example
10# is evaluated, just as +it+ refers to the example itself.
11#++
12class ContextState
13  attr_reader :state, :parent, :parents, :children, :examples, :to_s
14
15  def initialize(mod, options=nil)
16    @to_s = mod.to_s
17    if options.is_a? Hash
18      @options = options
19    else
20      @to_s += "#{".:#".include?(options[0,1]) ? "" : " "}#{options}" if options
21      @options = { }
22    end
23    @options[:shared] ||= false
24
25    @parsed   = false
26    @before   = { :all => [], :each => [] }
27    @after    = { :all => [], :each => [] }
28    @pre      = {}
29    @post     = {}
30    @examples = []
31    @parent   = nil
32    @parents  = [self]
33    @children = []
34
35    @mock_verify         = Proc.new { Mock.verify_count }
36    @mock_cleanup        = Proc.new { Mock.cleanup }
37    @expectation_missing = Proc.new { raise SpecExpectationNotFoundError }
38  end
39
40  # Remove caching when a ContextState is dup'd for shared specs.
41  def initialize_copy(other)
42    @pre  = {}
43    @post = {}
44  end
45
46  # Returns true if this is a shared +ContextState+. Essentially, when
47  # created with: describe "Something", :shared => true { ... }
48  def shared?
49    return @options[:shared]
50  end
51
52  # Set the parent (enclosing) +ContextState+ for this state. Creates
53  # the +parents+ list.
54  def parent=(parent)
55    @description = nil
56
57    if shared?
58      @parent = nil
59    else
60      @parent = parent
61      parent.child self if parent
62
63      @parents = [self]
64      state = parent
65      while state
66        @parents.unshift state
67        state = state.parent
68      end
69    end
70  end
71
72  # Add the ContextState instance +child+ to the list of nested
73  # describe blocks.
74  def child(child)
75    @children << child
76  end
77
78  # Adds a nested ContextState in a shared ContextState to a containing
79  # ContextState.
80  #
81  # Normal adoption is from the parent's perspective. But adopt is a good
82  # verb and it's reasonable for the child to adopt the parent as well. In
83  # this case, manipulating state from inside the child avoids needlessly
84  # exposing the state to manipulate it externally in the dup. (See
85  # #it_should_behave_like)
86  def adopt(parent)
87    self.parent = parent
88
89    @examples = @examples.map do |example|
90      example = example.dup
91      example.context = self
92      example
93    end
94
95    children = @children
96    @children = []
97
98    children.each { |child| child.dup.adopt self }
99  end
100
101  # Returns a list of all before(+what+) blocks from self and any parents.
102  def pre(what)
103    @pre[what] ||= parents.inject([]) { |l, s| l.push(*s.before(what)) }
104  end
105
106  # Returns a list of all after(+what+) blocks from self and any parents.
107  # The list is in reverse order. In other words, the blocks defined in
108  # inner describes are in the list before those defined in outer describes,
109  # and in a particular describe block those defined later are in the list
110  # before those defined earlier.
111  def post(what)
112    @post[what] ||= parents.inject([]) { |l, s| l.unshift(*s.after(what)) }
113  end
114
115  # Records before(:each) and before(:all) blocks.
116  def before(what, &block)
117    return if MSpec.guarded?
118    block ? @before[what].push(block) : @before[what]
119  end
120
121  # Records after(:each) and after(:all) blocks.
122  def after(what, &block)
123    return if MSpec.guarded?
124    block ? @after[what].unshift(block) : @after[what]
125  end
126
127  # Creates an ExampleState instance for the block and stores it
128  # in a list of examples to evaluate unless the example is filtered.
129  def it(desc, &block)
130    example = ExampleState.new(self, desc, block)
131    MSpec.actions :add, example
132    return if MSpec.guarded?
133    @examples << example
134  end
135
136  # Evaluates the block and resets the toplevel +ContextState+ to #parent.
137  def describe(&block)
138    @parsed = protect @to_s, block, false
139    MSpec.register_current parent
140    MSpec.register_shared self if shared?
141  end
142
143  # Returns a description string generated from self and all parents
144  def description
145    @description ||= parents.map { |p| p.to_s }.compact.join(" ")
146  end
147
148  # Injects the before/after blocks and examples from the shared
149  # describe block into this +ContextState+ instance.
150  def it_should_behave_like(desc)
151    return if MSpec.guarded?
152
153    unless state = MSpec.retrieve_shared(desc)
154      raise Exception, "Unable to find shared 'describe' for #{desc}"
155    end
156
157    state.before(:all).each { |b| before :all, &b }
158    state.before(:each).each { |b| before :each, &b }
159    state.after(:each).each { |b| after :each, &b }
160    state.after(:all).each { |b| after :all, &b }
161
162    state.examples.each do |example|
163      example = example.dup
164      example.context = self
165      @examples << example
166    end
167
168    state.children.each do |child|
169      child.dup.adopt self
170    end
171  end
172
173  # Evaluates each block in +blocks+ using the +MSpec.protect+ method
174  # so that exceptions are handled and tallied. Returns true and does
175  # NOT evaluate any blocks if +check+ is true and
176  # <tt>MSpec.mode?(:pretend)</tt> is true.
177  def protect(what, blocks, check=true)
178    return true if check and MSpec.mode? :pretend
179    Array(blocks).all? { |block| MSpec.protect what, &block }
180  end
181
182  # Removes filtered examples. Returns true if there are examples
183  # left to evaluate.
184  def filter_examples
185    filtered, @examples = @examples.partition do |ex|
186      ex.filtered?
187    end
188
189    filtered.each do |ex|
190      MSpec.actions :tagged, ex
191    end
192
193    !@examples.empty?
194  end
195
196  # Evaluates the examples in a +ContextState+. Invokes the MSpec events
197  # for :enter, :before, :after, :leave.
198  def process
199    MSpec.register_current self
200
201    if @parsed and filter_examples
202      MSpec.shuffle @examples if MSpec.randomize?
203      MSpec.actions :enter, description
204
205      if protect "before :all", pre(:all)
206        @examples.each do |state|
207          MSpec.repeat do
208            @state  = state
209            example = state.example
210            MSpec.actions :before, state
211
212            if protect "before :each", pre(:each)
213              MSpec.clear_expectations
214              if example
215                passed = protect nil, example
216                MSpec.actions :example, state, example
217                protect nil, @expectation_missing unless MSpec.expectation? or !passed
218              end
219            end
220            protect "after :each", post(:each)
221            protect "Mock.verify_count", @mock_verify
222
223            protect "Mock.cleanup", @mock_cleanup
224            MSpec.actions :after, state
225            @state = nil
226          end
227        end
228        protect "after :all", post(:all)
229      else
230        protect "Mock.cleanup", @mock_cleanup
231      end
232
233      MSpec.actions :leave
234    end
235
236    MSpec.register_current nil
237    children.each { |child| child.process }
238  end
239end
240