summaryrefslogtreecommitdiff
path: root/spec/mspec/lib/mspec/runner/context.rb
blob: 30d8a4ad1b4da2c05281fbd633f114f74ce15bad (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# Holds the state of the +describe+ block that is being
# evaluated. Every example (i.e. +it+ block) is evaluated
# in a context, which may include state set up in <tt>before
# :each</tt> or <tt>before :all</tt> blocks.
#
#--
# A note on naming: this is named _ContextState_ rather
# than _DescribeState_ because +describe+ is the keyword
# in the DSL for referring to the context in which an example
# is evaluated, just as +it+ refers to the example itself.
#++
class ContextState
  attr_reader :state, :parent, :parents, :children, :examples, :to_s

  def initialize(mod, options=nil)
    @to_s = mod.to_s
    if options.is_a? Hash
      @options = options
    else
      @to_s += "#{".:#".include?(options[0,1]) ? "" : " "}#{options}" if options
      @options = { }
    end
    @options[:shared] ||= false

    @parsed   = false
    @before   = { :all => [], :each => [] }
    @after    = { :all => [], :each => [] }
    @pre      = {}
    @post     = {}
    @examples = []
    @parent   = nil
    @parents  = [self]
    @children = []

    @mock_verify         = Proc.new { Mock.verify_count }
    @mock_cleanup        = Proc.new { Mock.cleanup }
    @expectation_missing = Proc.new { raise SpecExpectationNotFoundError }
  end

  # Remove caching when a ContextState is dup'd for shared specs.
  def initialize_copy(other)
    @pre  = {}
    @post = {}
  end

  # Returns true if this is a shared +ContextState+. Essentially, when
  # created with: describe "Something", :shared => true { ... }
  def shared?
    return @options[:shared]
  end

  # Set the parent (enclosing) +ContextState+ for this state. Creates
  # the +parents+ list.
  def parent=(parent)
    @description = nil

    if shared?
      @parent = nil
    else
      @parent = parent
      parent.child self if parent

      @parents = [self]
      state = parent
      while state
        @parents.unshift state
        state = state.parent
      end
    end
  end

  # Add the ContextState instance +child+ to the list of nested
  # describe blocks.
  def child(child)
    @children << child
  end

  # Adds a nested ContextState in a shared ContextState to a containing
  # ContextState.
  #
  # Normal adoption is from the parent's perspective. But adopt is a good
  # verb and it's reasonable for the child to adopt the parent as well. In
  # this case, manipulating state from inside the child avoids needlessly
  # exposing the state to manipulate it externally in the dup. (See
  # #it_should_behave_like)
  def adopt(parent)
    self.parent = parent

    @examples = @examples.map do |example|
      example = example.dup
      example.context = self
      example
    end

    children = @children
    @children = []

    children.each { |child| child.dup.adopt self }
  end

  # Returns a list of all before(+what+) blocks from self and any parents.
  def pre(what)
    @pre[what] ||= parents.inject([]) { |l, s| l.push(*s.before(what)) }
  end

  # Returns a list of all after(+what+) blocks from self and any parents.
  # The list is in reverse order. In other words, the blocks defined in
  # inner describes are in the list before those defined in outer describes,
  # and in a particular describe block those defined later are in the list
  # before those defined earlier.
  def post(what)
    @post[what] ||= parents.inject([]) { |l, s| l.unshift(*s.after(what)) }
  end

  # Records before(:each) and before(:all) blocks.
  def before(what, &block)
    return if MSpec.guarded?
    block ? @before[what].push(block) : @before[what]
  end

  # Records after(:each) and after(:all) blocks.
  def after(what, &block)
    return if MSpec.guarded?
    block ? @after[what].unshift(block) : @after[what]
  end

  # Creates an ExampleState instance for the block and stores it
  # in a list of examples to evaluate unless the example is filtered.
  def it(desc, &block)
    example = ExampleState.new(self, desc, block)
    MSpec.actions :add, example
    return if MSpec.guarded?
    @examples << example
  end

  # Evaluates the block and resets the toplevel +ContextState+ to #parent.
  def describe(&block)
    @parsed = protect @to_s, block, false
    MSpec.register_current parent
    MSpec.register_shared self if shared?
  end

  # Returns a description string generated from self and all parents
  def description
    @description ||= parents.map { |p| p.to_s }.compact.join(" ")
  end

  # Injects the before/after blocks and examples from the shared
  # describe block into this +ContextState+ instance.
  def it_should_behave_like(desc)
    return if MSpec.guarded?

    unless state = MSpec.retrieve_shared(desc)
      raise Exception, "Unable to find shared 'describe' for #{desc}"
    end

    state.before(:all).each { |b| before :all, &b }
    state.before(:each).each { |b| before :each, &b }
    state.after(:each).each { |b| after :each, &b }
    state.after(:all).each { |b| after :all, &b }

    state.examples.each do |example|
      example = example.dup
      example.context = self
      @examples << example
    end

    state.children.each do |child|
      child.dup.adopt self
    end
  end

  # Evaluates each block in +blocks+ using the +MSpec.protect+ method
  # so that exceptions are handled and tallied. Returns true and does
  # NOT evaluate any blocks if +check+ is true and
  # <tt>MSpec.mode?(:pretend)</tt> is true.
  def protect(what, blocks, check=true)
    return true if check and MSpec.mode? :pretend
    Array(blocks).all? { |block| MSpec.protect what, &block }
  end

  # Removes filtered examples. Returns true if there are examples
  # left to evaluate.
  def filter_examples
    filtered, @examples = @examples.partition do |ex|
      ex.filtered?
    end

    filtered.each do |ex|
      MSpec.actions :tagged, ex
    end

    !@examples.empty?
  end

  # Evaluates the examples in a +ContextState+. Invokes the MSpec events
  # for :enter, :before, :after, :leave.
  def process
    MSpec.register_current self

    if @parsed and filter_examples
      MSpec.shuffle @examples if MSpec.randomize?
      MSpec.actions :enter, description

      if protect "before :all", pre(:all)
        @examples.each do |state|
          MSpec.repeat do
            @state  = state
            example = state.example
            MSpec.actions :before, state

            if protect "before :each", pre(:each)
              MSpec.clear_expectations
              if example
                passed = protect nil, example
                MSpec.actions :example, state, example
                protect nil, @expectation_missing unless MSpec.expectation? or !passed
              end
            end
            protect "after :each", post(:each)
            protect "Mock.verify_count", @mock_verify

            protect "Mock.cleanup", @mock_cleanup
            MSpec.actions :after, state
            @state = nil
          end
        end
        protect "after :all", post(:all)
      else
        protect "Mock.cleanup", @mock_cleanup
      end

      MSpec.actions :leave
    end

    MSpec.register_current nil
    children.each { |child| child.process }
  end
end