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