summaryrefslogtreecommitdiff
path: root/spec/mspec/lib/mspec/runner/context.rb
diff options
context:
space:
mode:
Diffstat (limited to 'spec/mspec/lib/mspec/runner/context.rb')
-rw-r--r--spec/mspec/lib/mspec/runner/context.rb239
1 files changed, 239 insertions, 0 deletions
diff --git a/spec/mspec/lib/mspec/runner/context.rb b/spec/mspec/lib/mspec/runner/context.rb
new file mode 100644
index 0000000000..2b470f226a
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/context.rb
@@ -0,0 +1,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 refering 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