summaryrefslogtreecommitdiff
path: root/spec/mspec/lib/mspec/runner
diff options
context:
space:
mode:
authoreregon <eregon@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>2017-05-07 12:04:49 +0000
committereregon <eregon@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>2017-05-07 12:04:49 +0000
commit95e8c48dd3348503a8c7db5d0498894a1b676395 (patch)
tree9eef7f720314ebaff56845a74e203770e62284e4 /spec/mspec/lib/mspec/runner
parented7d803500de38186c74bce94d233e85ef51e503 (diff)
Add in-tree mspec and ruby/spec
* For easier modifications of ruby/spec by MRI developers. * .gitignore: track changes under spec. * spec/mspec, spec/rubyspec: add in-tree mspec and ruby/spec. These files can therefore be updated like any other file in MRI. Instructions are provided in spec/README. [Feature #13156] [ruby-core:79246] git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@58595 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Diffstat (limited to 'spec/mspec/lib/mspec/runner')
-rw-r--r--spec/mspec/lib/mspec/runner/actions.rb6
-rw-r--r--spec/mspec/lib/mspec/runner/actions/filter.rb40
-rw-r--r--spec/mspec/lib/mspec/runner/actions/leakchecker.rb301
-rw-r--r--spec/mspec/lib/mspec/runner/actions/tag.rb133
-rw-r--r--spec/mspec/lib/mspec/runner/actions/taglist.rb56
-rw-r--r--spec/mspec/lib/mspec/runner/actions/tagpurge.rb56
-rw-r--r--spec/mspec/lib/mspec/runner/actions/tally.rb133
-rw-r--r--spec/mspec/lib/mspec/runner/actions/timer.rb22
-rw-r--r--spec/mspec/lib/mspec/runner/context.rb239
-rw-r--r--spec/mspec/lib/mspec/runner/evaluate.rb54
-rw-r--r--spec/mspec/lib/mspec/runner/example.rb34
-rw-r--r--spec/mspec/lib/mspec/runner/exception.rb43
-rw-r--r--spec/mspec/lib/mspec/runner/filters.rb4
-rw-r--r--spec/mspec/lib/mspec/runner/filters/match.rb18
-rw-r--r--spec/mspec/lib/mspec/runner/filters/profile.rb54
-rw-r--r--spec/mspec/lib/mspec/runner/filters/regexp.rb7
-rw-r--r--spec/mspec/lib/mspec/runner/filters/tag.rb29
-rw-r--r--spec/mspec/lib/mspec/runner/formatters.rb12
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/describe.rb24
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/dotted.rb117
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/file.rb19
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/html.rb81
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/junit.rb89
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/method.rb93
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/multi.rb36
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/profile.rb70
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/specdoc.rb41
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/spinner.rb117
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/summary.rb11
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/unit.rb21
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/yaml.rb42
-rw-r--r--spec/mspec/lib/mspec/runner/mspec.rb391
-rw-r--r--spec/mspec/lib/mspec/runner/object.rb28
-rw-r--r--spec/mspec/lib/mspec/runner/shared.rb12
-rw-r--r--spec/mspec/lib/mspec/runner/tag.rb38
35 files changed, 2471 insertions, 0 deletions
diff --git a/spec/mspec/lib/mspec/runner/actions.rb b/spec/mspec/lib/mspec/runner/actions.rb
new file mode 100644
index 0000000000..0a5a05fbd1
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions.rb
@@ -0,0 +1,6 @@
+require 'mspec/runner/actions/tally'
+require 'mspec/runner/actions/timer'
+require 'mspec/runner/actions/filter'
+require 'mspec/runner/actions/tag'
+require 'mspec/runner/actions/taglist'
+require 'mspec/runner/actions/tagpurge'
diff --git a/spec/mspec/lib/mspec/runner/actions/filter.rb b/spec/mspec/lib/mspec/runner/actions/filter.rb
new file mode 100644
index 0000000000..35899c8dc8
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/filter.rb
@@ -0,0 +1,40 @@
+require 'mspec/runner/filters/match'
+
+# ActionFilter is a base class for actions that are triggered by
+# specs that match the filter. The filter may be specified by
+# strings that match spec descriptions or by tags for strings
+# that match spec descriptions.
+#
+# Unlike TagFilter and RegexpFilter, ActionFilter instances do
+# not affect the specs that are run. The filter is only used to
+# trigger the action.
+
+class ActionFilter
+ def initialize(tags=nil, descs=nil)
+ @tags = Array(tags)
+ descs = Array(descs)
+ @sfilter = descs.empty? ? nil : MatchFilter.new(nil, *descs)
+ @tfilter = nil
+ end
+
+ def ===(string)
+ @sfilter === string or @tfilter === string
+ end
+
+ def load
+ return if @tags.empty?
+
+ desc = MSpec.read_tags(@tags).map { |t| t.description }
+ return if desc.empty?
+
+ @tfilter = MatchFilter.new(nil, *desc)
+ end
+
+ def register
+ MSpec.register :load, self
+ end
+
+ def unregister
+ MSpec.unregister :load, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/leakchecker.rb b/spec/mspec/lib/mspec/runner/actions/leakchecker.rb
new file mode 100644
index 0000000000..e947cda9ff
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/leakchecker.rb
@@ -0,0 +1,301 @@
+# Adapted from ruby's test/lib/leakchecker.rb.
+# Ruby's 2-clause BSDL follows.
+
+# Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+class LeakChecker
+ def initialize
+ @fd_info = find_fds
+ @tempfile_info = find_tempfiles
+ @thread_info = find_threads
+ @env_info = find_env
+ @argv_info = find_argv
+ @encoding_info = find_encodings
+ end
+
+ def check(test_name)
+ @no_leaks = true
+ leaks = [
+ check_fd_leak(test_name),
+ check_tempfile_leak(test_name),
+ check_thread_leak(test_name),
+ check_process_leak(test_name),
+ check_env(test_name),
+ check_argv(test_name),
+ check_encodings(test_name)
+ ]
+ GC.start if leaks.any?
+ return leaks.none?
+ end
+
+ private
+ def find_fds
+ fd_dir = "/proc/self/fd"
+ if File.directory?(fd_dir)
+ fds = Dir.open(fd_dir) {|d|
+ a = d.grep(/\A\d+\z/, &:to_i)
+ if d.respond_to? :fileno
+ a -= [d.fileno]
+ end
+ a
+ }
+ fds.sort
+ else
+ []
+ end
+ end
+
+ def check_fd_leak(test_name)
+ leaked = false
+ live1 = @fd_info
+ if IO.respond_to?(:console) and (m = IO.method(:console)).arity.nonzero?
+ m[:close]
+ end
+ live2 = find_fds
+ fd_closed = live1 - live2
+ if !fd_closed.empty?
+ fd_closed.each {|fd|
+ puts "Closed file descriptor: #{test_name}: #{fd}"
+ }
+ end
+ fd_leaked = live2 - live1
+ if !fd_leaked.empty?
+ leaked = true
+ h = {}
+ ObjectSpace.each_object(IO) {|io|
+ inspect = io.inspect
+ begin
+ autoclose = io.autoclose?
+ fd = io.fileno
+ rescue IOError # closed IO object
+ next
+ end
+ (h[fd] ||= []) << [io, autoclose, inspect]
+ }
+ fd_leaked.each {|fd|
+ str = ''
+ if h[fd]
+ str << ' :'
+ h[fd].map {|io, autoclose, inspect|
+ s = ' ' + inspect
+ s << "(not-autoclose)" if !autoclose
+ s
+ }.sort.each {|s|
+ str << s
+ }
+ end
+ puts "Leaked file descriptor: #{test_name}: #{fd}#{str}"
+ }
+ #system("lsof -p #$$") if !fd_leaked.empty?
+ h.each {|fd, list|
+ next if list.length <= 1
+ if 1 < list.count {|io, autoclose, inspect| autoclose }
+ str = list.map {|io, autoclose, inspect| " #{inspect}" + (autoclose ? "(autoclose)" : "") }.sort.join
+ puts "Multiple autoclose IO object for a file descriptor:#{str}"
+ end
+ }
+ end
+ @fd_info = live2
+ return leaked
+ end
+
+ def extend_tempfile_counter
+ return if defined? LeakChecker::TempfileCounter
+ m = Module.new {
+ @count = 0
+ class << self
+ attr_accessor :count
+ end
+
+ def new(data)
+ LeakChecker::TempfileCounter.count += 1
+ super(data)
+ end
+ }
+ LeakChecker.const_set(:TempfileCounter, m)
+
+ class << Tempfile::Remover
+ prepend LeakChecker::TempfileCounter
+ end
+ end
+
+ def find_tempfiles(prev_count=-1)
+ return [prev_count, []] unless defined? Tempfile
+ extend_tempfile_counter
+ count = TempfileCounter.count
+ if prev_count == count
+ [prev_count, []]
+ else
+ tempfiles = ObjectSpace.each_object(Tempfile).find_all {|t| t.path }
+ [count, tempfiles]
+ end
+ end
+
+ def check_tempfile_leak(test_name)
+ return false unless defined? Tempfile
+ count1, initial_tempfiles = @tempfile_info
+ count2, current_tempfiles = find_tempfiles(count1)
+ leaked = false
+ tempfiles_leaked = current_tempfiles - initial_tempfiles
+ if !tempfiles_leaked.empty?
+ leaked = true
+ list = tempfiles_leaked.map {|t| t.inspect }.sort
+ list.each {|str|
+ puts "Leaked tempfile: #{test_name}: #{str}"
+ }
+ tempfiles_leaked.each {|t| t.close! }
+ end
+ @tempfile_info = [count2, initial_tempfiles]
+ return leaked
+ end
+
+ def find_threads
+ Thread.list.find_all {|t|
+ t != Thread.current && t.alive?
+ }
+ end
+
+ def check_thread_leak(test_name)
+ live1 = @thread_info
+ live2 = find_threads
+ thread_finished = live1 - live2
+ leaked = false
+ if !thread_finished.empty?
+ list = thread_finished.map {|t| t.inspect }.sort
+ list.each {|str|
+ puts "Finished thread: #{test_name}: #{str}"
+ }
+ end
+ thread_leaked = live2 - live1
+ if !thread_leaked.empty?
+ leaked = true
+ list = thread_leaked.map {|t| t.inspect }.sort
+ list.each {|str|
+ puts "Leaked thread: #{test_name}: #{str}"
+ }
+ end
+ @thread_info = live2
+ return leaked
+ end
+
+ def check_process_leak(test_name)
+ subprocesses_leaked = Process.waitall
+ subprocesses_leaked.each { |pid, status|
+ puts "Leaked subprocess: #{pid}: #{status}"
+ }
+ return !subprocesses_leaked.empty?
+ end
+
+ def find_env
+ ENV.to_h
+ end
+
+ def check_env(test_name)
+ old_env = @env_info
+ new_env = find_env
+ return false if old_env == new_env
+ (old_env.keys | new_env.keys).sort.each {|k|
+ if old_env.has_key?(k)
+ if new_env.has_key?(k)
+ if old_env[k] != new_env[k]
+ puts "Environment variable changed: #{test_name} : #{k.inspect} changed : #{old_env[k].inspect} -> #{new_env[k].inspect}"
+ end
+ else
+ puts "Environment variable changed: #{test_name} : #{k.inspect} deleted"
+ end
+ else
+ if new_env.has_key?(k)
+ puts "Environment variable changed: #{test_name} : #{k.inspect} added"
+ else
+ flunk "unreachable"
+ end
+ end
+ }
+ @env_info = new_env
+ return true
+ end
+
+ def find_argv
+ ARGV.map { |e| e.dup }
+ end
+
+ def check_argv(test_name)
+ old_argv = @argv_info
+ new_argv = find_argv
+ leaked = false
+ if new_argv != old_argv
+ puts "ARGV changed: #{test_name} : #{old_argv.inspect} to #{new_argv.inspect}"
+ @argv_info = new_argv
+ leaked = true
+ end
+ return leaked
+ end
+
+ def find_encodings
+ [Encoding.default_internal, Encoding.default_external]
+ end
+
+ def check_encodings(test_name)
+ old_internal, old_external = @encoding_info
+ new_internal, new_external = find_encodings
+ leaked = false
+ if new_internal != old_internal
+ leaked = true
+ puts "Encoding.default_internal changed: #{test_name} : #{old_internal} to #{new_internal}"
+ end
+ if new_external != old_external
+ leaked = true
+ puts "Encoding.default_external changed: #{test_name} : #{old_external} to #{new_external}"
+ end
+ @encoding_info = [new_internal, new_external]
+ return leaked
+ end
+
+ def puts(*args)
+ if @no_leaks
+ @no_leaks = false
+ print "\n"
+ end
+ super(*args)
+ end
+end
+
+class LeakCheckerAction
+ def register
+ MSpec.register :start, self
+ MSpec.register :after, self
+ end
+
+ def start
+ @checker = LeakChecker.new
+ end
+
+ def after(state)
+ unless @checker.check(state.description)
+ if state.example
+ puts state.example.source_location.join(':')
+ end
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/tag.rb b/spec/mspec/lib/mspec/runner/actions/tag.rb
new file mode 100644
index 0000000000..760152b2a3
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/tag.rb
@@ -0,0 +1,133 @@
+require 'mspec/runner/actions/filter'
+
+# TagAction - Write tagged spec description string to a
+# tag file associated with each spec file.
+#
+# The action is triggered by specs whose descriptions
+# match the filter created with 'tags' and/or 'desc'
+#
+# The action fires in the :after event, after the spec
+# had been run. The action fires if the outcome of
+# running the spec matches 'outcome'.
+#
+# The arguments are:
+#
+# action: :add, :del
+# outcome: :pass, :fail, :all
+# tag: the tag to create/delete
+# comment: the comment to create
+# tags: zero or more tags to get matching
+# spec description strings from
+# desc: zero or more strings to match the
+# spec description strings
+
+class TagAction < ActionFilter
+ def initialize(action, outcome, tag, comment, tags=nil, descs=nil)
+ super tags, descs
+ @action = action
+ @outcome = outcome
+ @tag = tag
+ @comment = comment
+ @report = []
+ @exception = false
+ end
+
+ # Returns true if there are no _tag_ or _description_ filters. This
+ # means that a TagAction matches any example by default. Otherwise,
+ # returns true if either the _tag_ or the _description_ filter
+ # matches +string+.
+ def ===(string)
+ return true unless @sfilter or @tfilter
+ @sfilter === string or @tfilter === string
+ end
+
+ # Callback for the MSpec :before event. Resets the +#exception?+
+ # flag to false.
+ def before(state)
+ @exception = false
+ end
+
+ # Callback for the MSpec :exception event. Sets the +#exception?+
+ # flag to true.
+ def exception(exception)
+ @exception = true
+ end
+
+ # Callback for the MSpec :after event. Performs the tag action
+ # depending on the type of action and the outcome of evaluating
+ # the example. See +TagAction+ for a description of the actions.
+ def after(state)
+ if self === state.description and outcome?
+ tag = SpecTag.new
+ tag.tag = @tag
+ tag.comment = @comment
+ tag.description = state.description
+
+ case @action
+ when :add
+ changed = MSpec.write_tag tag
+ when :del
+ changed = MSpec.delete_tag tag
+ end
+
+ @report << state.description if changed
+ end
+ end
+
+ # Returns true if the result of evaluating the example matches
+ # the _outcome_ registered for this tag action. See +TagAction+
+ # for a description of the _outcome_ types.
+ def outcome?
+ @outcome == :all or
+ (@outcome == :pass and not exception?) or
+ (@outcome == :fail and exception?)
+ end
+
+ # Returns true if an exception was raised while evaluating the
+ # current example.
+ def exception?
+ @exception
+ end
+
+ def report
+ @report.join("\n") + "\n"
+ end
+ private :report
+
+ # Callback for the MSpec :finish event. Prints the actions
+ # performed while evaluating the examples.
+ def finish
+ case @action
+ when :add
+ if @report.empty?
+ print "\nTagAction: no specs were tagged with '#{@tag}'\n"
+ else
+ print "\nTagAction: specs tagged with '#{@tag}':\n\n"
+ print report
+ end
+ when :del
+ if @report.empty?
+ print "\nTagAction: no tags '#{@tag}' were deleted\n"
+ else
+ print "\nTagAction: tag '#{@tag}' deleted for specs:\n\n"
+ print report
+ end
+ end
+ end
+
+ def register
+ super
+ MSpec.register :before, self
+ MSpec.register :exception, self
+ MSpec.register :after, self
+ MSpec.register :finish, self
+ end
+
+ def unregister
+ super
+ MSpec.unregister :before, self
+ MSpec.unregister :exception, self
+ MSpec.unregister :after, self
+ MSpec.unregister :finish, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/taglist.rb b/spec/mspec/lib/mspec/runner/actions/taglist.rb
new file mode 100644
index 0000000000..c1aba53794
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/taglist.rb
@@ -0,0 +1,56 @@
+require 'mspec/runner/actions/filter'
+
+# TagListAction - prints out the descriptions for any specs
+# tagged with +tags+. If +tags+ is an empty list, prints out
+# descriptions for any specs that are tagged.
+class TagListAction
+ def initialize(tags=nil)
+ @tags = tags.nil? || tags.empty? ? nil : Array(tags)
+ @filter = nil
+ end
+
+ # Returns true. This enables us to match any tag when loading
+ # tags from the file.
+ def include?(arg)
+ true
+ end
+
+ # Returns true if any tagged descriptions matches +string+.
+ def ===(string)
+ @filter === string
+ end
+
+ # Prints a banner about matching tagged specs.
+ def start
+ if @tags
+ print "\nListing specs tagged with #{@tags.map { |t| "'#{t}'" }.join(", ") }\n\n"
+ else
+ print "\nListing all tagged specs\n\n"
+ end
+ end
+
+ # Creates a MatchFilter for specific tags or for all tags.
+ def load
+ @filter = nil
+ desc = MSpec.read_tags(@tags || self).map { |t| t.description }
+ @filter = MatchFilter.new(nil, *desc) unless desc.empty?
+ end
+
+ # Prints the spec description if it matches the filter.
+ def after(state)
+ return unless self === state.description
+ print state.description, "\n"
+ end
+
+ def register
+ MSpec.register :start, self
+ MSpec.register :load, self
+ MSpec.register :after, self
+ end
+
+ def unregister
+ MSpec.unregister :start, self
+ MSpec.unregister :load, self
+ MSpec.unregister :after, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/tagpurge.rb b/spec/mspec/lib/mspec/runner/actions/tagpurge.rb
new file mode 100644
index 0000000000..f4587de6bc
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/tagpurge.rb
@@ -0,0 +1,56 @@
+require 'mspec/runner/actions/filter'
+require 'mspec/runner/actions/taglist'
+
+# TagPurgeAction - removes all tags not matching any spec
+# descriptions.
+class TagPurgeAction < TagListAction
+ attr_reader :matching
+
+ def initialize
+ @matching = []
+ @filter = nil
+ @tags = nil
+ end
+
+ # Prints a banner about purging tags.
+ def start
+ print "\nRemoving tags not matching any specs\n\n"
+ end
+
+ # Creates a MatchFilter for all tags.
+ def load
+ @filter = nil
+ @tags = MSpec.read_tags self
+ desc = @tags.map { |t| t.description }
+ @filter = MatchFilter.new(nil, *desc) unless desc.empty?
+ end
+
+ # Saves any matching tags
+ def after(state)
+ @matching << state.description if self === state.description
+ end
+
+ # Rewrites any matching tags. Prints non-matching tags.
+ # Deletes the tag file if there were no tags (this cleans
+ # up empty or malformed tag files).
+ def unload
+ if @filter
+ matched = @tags.select { |t| @matching.any? { |s| s == t.description } }
+ MSpec.write_tags matched
+
+ (@tags - matched).each { |t| print t.description, "\n" }
+ else
+ MSpec.delete_tags
+ end
+ end
+
+ def register
+ super
+ MSpec.register :unload, self
+ end
+
+ def unregister
+ super
+ MSpec.unregister :unload, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/tally.rb b/spec/mspec/lib/mspec/runner/actions/tally.rb
new file mode 100644
index 0000000000..33f937293c
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/tally.rb
@@ -0,0 +1,133 @@
+class Tally
+ attr_accessor :files, :examples, :expectations, :failures, :errors, :guards, :tagged
+
+ def initialize
+ @files = @examples = @expectations = @failures = @errors = @guards = @tagged = 0
+ end
+
+ def files!(add=1)
+ @files += add
+ end
+
+ def examples!(add=1)
+ @examples += add
+ end
+
+ def expectations!(add=1)
+ @expectations += add
+ end
+
+ def failures!(add=1)
+ @failures += add
+ end
+
+ def errors!(add=1)
+ @errors += add
+ end
+
+ def guards!(add=1)
+ @guards += add
+ end
+
+ def tagged!(add=1)
+ @tagged += add
+ end
+
+ def file
+ pluralize files, "file"
+ end
+
+ def example
+ pluralize examples, "example"
+ end
+
+ def expectation
+ pluralize expectations, "expectation"
+ end
+
+ def failure
+ pluralize failures, "failure"
+ end
+
+ def error
+ pluralize errors, "error"
+ end
+
+ def guard
+ pluralize guards, "guard"
+ end
+
+ def tag
+ "#{tagged} tagged"
+ end
+
+ def format
+ results = [ file, example, expectation, failure, error, tag ]
+ if [:report, :report_on, :verify].any? { |m| MSpec.mode? m }
+ results << guard
+ end
+ results.join(", ")
+ end
+
+ alias_method :to_s, :format
+
+ def pluralize(count, singular)
+ "#{count} #{singular}#{'s' unless count == 1}"
+ end
+ private :pluralize
+end
+
+class TallyAction
+ attr_reader :counter
+
+ def initialize
+ @counter = Tally.new
+ end
+
+ def register
+ MSpec.register :load, self
+ MSpec.register :exception, self
+ MSpec.register :example, self
+ MSpec.register :tagged, self
+ MSpec.register :expectation, self
+ end
+
+ def unregister
+ MSpec.unregister :load, self
+ MSpec.unregister :exception, self
+ MSpec.unregister :example, self
+ MSpec.unregister :tagged, self
+ MSpec.unregister :expectation, self
+ end
+
+ def load
+ @counter.files!
+ end
+
+ # Callback for the MSpec :expectation event. Increments the
+ # tally of expectations (e.g. #should, #should_receive, etc.).
+ def expectation(state)
+ @counter.expectations!
+ end
+
+ # Callback for the MSpec :exception event. Increments the
+ # tally of errors and failures.
+ def exception(exception)
+ exception.failure? ? @counter.failures! : @counter.errors!
+ end
+
+ # Callback for the MSpec :example event. Increments the tally
+ # of examples.
+ def example(state, block)
+ @counter.examples!
+ end
+
+ def tagged(state)
+ @counter.examples!
+ @counter.tagged!
+ end
+
+ def format
+ @counter.format
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/timer.rb b/spec/mspec/lib/mspec/runner/actions/timer.rb
new file mode 100644
index 0000000000..e7ebfebe0d
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/timer.rb
@@ -0,0 +1,22 @@
+class TimerAction
+ def register
+ MSpec.register :start, self
+ MSpec.register :finish, self
+ end
+
+ def start
+ @start = Time.now
+ end
+
+ def finish
+ @stop = Time.now
+ end
+
+ def elapsed
+ @stop - @start
+ end
+
+ def format
+ "Finished in %f seconds" % elapsed
+ end
+end
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
diff --git a/spec/mspec/lib/mspec/runner/evaluate.rb b/spec/mspec/lib/mspec/runner/evaluate.rb
new file mode 100644
index 0000000000..fded84421f
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/evaluate.rb
@@ -0,0 +1,54 @@
+class SpecEvaluate
+ def self.desc=(desc)
+ @desc = desc
+ end
+
+ def self.desc
+ @desc ||= "evaluates "
+ end
+
+ def initialize(ruby, desc)
+ @ruby = ruby.rstrip
+ @desc = desc || self.class.desc
+ end
+
+ # Formats the Ruby source code for reabable output in the -fs formatter
+ # option. If the source contains no newline characters, wraps the source in
+ # single quotes to set if off from the rest of the description string. If
+ # the source does contain newline characters, sets the indent level to four
+ # characters.
+ def format(ruby, newline=true)
+ if ruby.include?("\n")
+ lines = ruby.each_line.to_a
+ if /( *)/ =~ lines.first
+ if $1.size > 4
+ dedent = $1.size - 4
+ ruby = lines.map { |l| l[dedent..-1] }.join
+ else
+ indent = " " * (4 - $1.size)
+ ruby = lines.map { |l| "#{indent}#{l}" }.join
+ end
+ end
+ "\n#{ruby}"
+ else
+ "'#{ruby.lstrip}'"
+ end
+ end
+
+ def define(&block)
+ ruby = @ruby
+ desc = @desc
+ evaluator = self
+
+ specify "#{desc} #{format ruby}" do
+ evaluator.instance_eval(ruby)
+ evaluator.instance_eval(&block)
+ end
+ end
+end
+
+class Object
+ def evaluate(str, desc=nil, &block)
+ SpecEvaluate.new(str, desc).define(&block)
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/example.rb b/spec/mspec/lib/mspec/runner/example.rb
new file mode 100644
index 0000000000..19eb29b079
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/example.rb
@@ -0,0 +1,34 @@
+require 'mspec/runner/mspec'
+
+# Holds some of the state of the example (i.e. +it+ block) that is
+# being evaluated. See also +ContextState+.
+class ExampleState
+ attr_reader :context, :it, :example
+
+ def initialize(context, it, example=nil)
+ @context = context
+ @it = it
+ @example = example
+ end
+
+ def context=(context)
+ @description = nil
+ @context = context
+ end
+
+ def describe
+ @context.description
+ end
+
+ def description
+ @description ||= "#{describe} #{@it}"
+ end
+
+ def filtered?
+ incl = MSpec.retrieve(:include) || []
+ excl = MSpec.retrieve(:exclude) || []
+ included = incl.empty? || incl.any? { |f| f === description }
+ included &&= excl.empty? || !excl.any? { |f| f === description }
+ !included
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/exception.rb b/spec/mspec/lib/mspec/runner/exception.rb
new file mode 100644
index 0000000000..0d9bb43105
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/exception.rb
@@ -0,0 +1,43 @@
+# Initialize $MSPEC_DEBUG
+$MSPEC_DEBUG ||= false
+
+class ExceptionState
+ attr_reader :description, :describe, :it, :exception
+
+ def initialize(state, location, exception)
+ @exception = exception
+
+ @description = location ? "An exception occurred during: #{location}" : ""
+ if state
+ @description += "\n" unless @description.empty?
+ @description += state.description
+ @describe = state.describe
+ @it = state.it
+ else
+ @describe = @it = ""
+ end
+ end
+
+ def failure?
+ [SpecExpectationNotMetError, SpecExpectationNotFoundError].any? { |e| @exception.is_a? e }
+ end
+
+ def message
+ if @exception.message.empty?
+ "<No message>"
+ elsif @exception.class == SpecExpectationNotMetError ||
+ @exception.class == SpecExpectationNotFoundError
+ @exception.message
+ else
+ "#{@exception.class}: #{@exception.message}"
+ end
+ end
+
+ def backtrace
+ @backtrace_filter ||= MSpecScript.config[:backtrace_filter]
+
+ bt = @exception.backtrace || []
+
+ bt.select { |line| $MSPEC_DEBUG or @backtrace_filter !~ line }.join("\n")
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/filters.rb b/spec/mspec/lib/mspec/runner/filters.rb
new file mode 100644
index 0000000000..d0420faca6
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/filters.rb
@@ -0,0 +1,4 @@
+require 'mspec/runner/filters/match'
+require 'mspec/runner/filters/regexp'
+require 'mspec/runner/filters/tag'
+require 'mspec/runner/filters/profile'
diff --git a/spec/mspec/lib/mspec/runner/filters/match.rb b/spec/mspec/lib/mspec/runner/filters/match.rb
new file mode 100644
index 0000000000..539fd02d01
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/filters/match.rb
@@ -0,0 +1,18 @@
+class MatchFilter
+ def initialize(what, *strings)
+ @what = what
+ @strings = strings
+ end
+
+ def ===(string)
+ @strings.any? { |s| string.include?(s) }
+ end
+
+ def register
+ MSpec.register @what, self
+ end
+
+ def unregister
+ MSpec.unregister @what, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/filters/profile.rb b/spec/mspec/lib/mspec/runner/filters/profile.rb
new file mode 100644
index 0000000000..a59722c451
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/filters/profile.rb
@@ -0,0 +1,54 @@
+class ProfileFilter
+ def initialize(what, *files)
+ @what = what
+ @methods = load(*files)
+ @pattern = /([^ .#]+[.#])([^ ]+)/
+ end
+
+ def find(name)
+ return name if File.exist?(File.expand_path(name))
+
+ ["spec/profiles", "spec", "profiles", "."].each do |dir|
+ file = File.join dir, name
+ return file if File.exist? file
+ end
+ end
+
+ def parse(file)
+ pattern = /(\S+):\s*/
+ key = ""
+ file.inject(Hash.new { |h,k| h[k] = [] }) do |hash, line|
+ line.chomp!
+ if line[0,2] == "- "
+ hash[key] << line[2..-1].gsub(/[ '"]/, "")
+ elsif m = pattern.match(line)
+ key = m[1]
+ end
+ hash
+ end
+ end
+
+ def load(*files)
+ files.inject({}) do |hash, file|
+ next hash unless name = find(file)
+
+ File.open name, "r" do |f|
+ hash.merge parse(f)
+ end
+ end
+ end
+
+ def ===(string)
+ return false unless m = @pattern.match(string)
+ return false unless l = @methods[m[1]]
+ l.include? m[2]
+ end
+
+ def register
+ MSpec.register @what, self
+ end
+
+ def unregister
+ MSpec.unregister @what, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/filters/regexp.rb b/spec/mspec/lib/mspec/runner/filters/regexp.rb
new file mode 100644
index 0000000000..2bd1448d3f
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/filters/regexp.rb
@@ -0,0 +1,7 @@
+require 'mspec/runner/filters/match'
+
+class RegexpFilter < MatchFilter
+ def to_regexp(*strings)
+ strings.map { |str| Regexp.new str }
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/filters/tag.rb b/spec/mspec/lib/mspec/runner/filters/tag.rb
new file mode 100644
index 0000000000..c641c01606
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/filters/tag.rb
@@ -0,0 +1,29 @@
+class TagFilter
+ def initialize(what, *tags)
+ @what = what
+ @tags = tags
+ end
+
+ def load
+ @descriptions = MSpec.read_tags(@tags).map { |t| t.description }
+ MSpec.register @what, self
+ end
+
+ def unload
+ MSpec.unregister @what, self
+ end
+
+ def ===(string)
+ @descriptions.include?(string)
+ end
+
+ def register
+ MSpec.register :load, self
+ MSpec.register :unload, self
+ end
+
+ def unregister
+ MSpec.unregister :load, self
+ MSpec.unregister :unload, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters.rb b/spec/mspec/lib/mspec/runner/formatters.rb
new file mode 100644
index 0000000000..d085031a12
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters.rb
@@ -0,0 +1,12 @@
+require 'mspec/runner/formatters/describe'
+require 'mspec/runner/formatters/dotted'
+require 'mspec/runner/formatters/file'
+require 'mspec/runner/formatters/specdoc'
+require 'mspec/runner/formatters/html'
+require 'mspec/runner/formatters/summary'
+require 'mspec/runner/formatters/unit'
+require 'mspec/runner/formatters/spinner'
+require 'mspec/runner/formatters/method'
+require 'mspec/runner/formatters/yaml'
+require 'mspec/runner/formatters/profile'
+require 'mspec/runner/formatters/junit'
diff --git a/spec/mspec/lib/mspec/runner/formatters/describe.rb b/spec/mspec/lib/mspec/runner/formatters/describe.rb
new file mode 100644
index 0000000000..176bd79279
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/describe.rb
@@ -0,0 +1,24 @@
+require 'mspec/runner/formatters/dotted'
+require 'mspec/runner/actions/tally'
+
+class DescribeFormatter < DottedFormatter
+ # Callback for the MSpec :finish event. Prints a summary of
+ # the number of errors and failures for each +describe+ block.
+ def finish
+ describes = Hash.new { |h,k| h[k] = Tally.new }
+
+ @exceptions.each do |exc|
+ desc = describes[exc.describe]
+ exc.failure? ? desc.failures! : desc.errors!
+ end
+
+ print "\n"
+ describes.each do |d, t|
+ text = d.size > 40 ? "#{d[0,37]}..." : d.ljust(40)
+ print "\n#{text} #{t.failure}, #{t.error}"
+ end
+ print "\n" unless describes.empty?
+
+ print "\n#{@timer.format}\n\n#{@tally.format}\n"
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/dotted.rb b/spec/mspec/lib/mspec/runner/formatters/dotted.rb
new file mode 100644
index 0000000000..61c8e4c27c
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/dotted.rb
@@ -0,0 +1,117 @@
+require 'mspec/expectations/expectations'
+require 'mspec/runner/actions/timer'
+require 'mspec/runner/actions/tally'
+require 'mspec/runner/actions/leakchecker' if ENV['CHECK_LEAKS']
+
+class DottedFormatter
+ attr_reader :exceptions, :timer, :tally
+
+ def initialize(out=nil)
+ @exception = @failure = false
+ @exceptions = []
+ @count = 0 # For subclasses
+ if out.nil?
+ @out = $stdout
+ else
+ @out = File.open out, "w"
+ end
+
+ @current_state = nil
+ end
+
+ # Creates the +TimerAction+ and +TallyAction+ instances and
+ # registers them. Registers +self+ for the +:exception+,
+ # +:before+, +:after+, and +:finish+ actions.
+ def register
+ (@timer = TimerAction.new).register
+ (@tally = TallyAction.new).register
+ LeakCheckerAction.new.register if ENV['CHECK_LEAKS']
+ @counter = @tally.counter
+
+ MSpec.register :exception, self
+ MSpec.register :before, self
+ MSpec.register :after, self
+ MSpec.register :finish, self
+ MSpec.register :abort, self
+ end
+
+ def abort
+ if @current_state
+ puts "\naborting example: #{@current_state.description}"
+ end
+ end
+
+ # Returns true if any exception is raised while running
+ # an example. This flag is reset before each example
+ # is evaluated.
+ def exception?
+ @exception
+ end
+
+ # Returns true if all exceptions during the evaluation
+ # of an example are failures rather than errors. See
+ # <tt>ExceptionState#failure</tt>. This flag is reset
+ # before each example is evaluated.
+ def failure?
+ @failure
+ end
+
+ # Callback for the MSpec :before event. Resets the
+ # +#exception?+ and +#failure+ flags.
+ def before(state=nil)
+ @current_state = state
+ @failure = @exception = false
+ end
+
+ # Callback for the MSpec :exception event. Stores the
+ # +ExceptionState+ object to generate the list of backtraces
+ # after all the specs are run. Also updates the internal
+ # +#exception?+ and +#failure?+ flags.
+ def exception(exception)
+ @count += 1
+ @failure = @exception ? @failure && exception.failure? : exception.failure?
+ @exception = true
+ @exceptions << exception
+ end
+
+ # Callback for the MSpec :after event. Prints an indicator
+ # for the result of evaluating this example as follows:
+ # . = No failure or error
+ # F = An SpecExpectationNotMetError was raised
+ # E = Any exception other than SpecExpectationNotMetError
+ def after(state = nil)
+ @current_state = nil
+
+ unless exception?
+ print "."
+ else
+ print failure? ? "F" : "E"
+ end
+ end
+
+ # Callback for the MSpec :finish event. Prints a description
+ # and backtrace for every exception that occurred while
+ # evaluating the examples.
+ def finish
+ print "\n"
+ count = 0
+ @exceptions.each do |exc|
+ count += 1
+ print_exception(exc, count)
+ end
+ print "\n#{@timer.format}\n\n#{@tally.format}\n"
+ end
+
+ def print_exception(exc, count)
+ outcome = exc.failure? ? "FAILED" : "ERROR"
+ print "\n#{count})\n#{exc.description} #{outcome}\n"
+ print exc.message, "\n"
+ print exc.backtrace, "\n"
+ end
+
+ # A convenience method to allow printing to different outputs.
+ def print(*args)
+ @out.print(*args)
+ @out.flush
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/file.rb b/spec/mspec/lib/mspec/runner/formatters/file.rb
new file mode 100644
index 0000000000..6db72af4ff
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/file.rb
@@ -0,0 +1,19 @@
+require 'mspec/runner/formatters/dotted'
+
+class FileFormatter < DottedFormatter
+ # Unregisters DottedFormatter#before, #after methods and
+ # registers #load, #unload, which perform the same duties
+ # as #before, #after in DottedFormatter.
+ def register
+ super
+
+ MSpec.unregister :before, self
+ MSpec.unregister :after, self
+
+ MSpec.register :load, self
+ MSpec.register :unload, self
+ end
+
+ alias_method :load, :before
+ alias_method :unload, :after
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/html.rb b/spec/mspec/lib/mspec/runner/formatters/html.rb
new file mode 100644
index 0000000000..060d2732f0
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/html.rb
@@ -0,0 +1,81 @@
+require 'mspec/expectations/expectations'
+require 'mspec/runner/formatters/dotted'
+
+class HtmlFormatter < DottedFormatter
+ def register
+ super
+ MSpec.register :start, self
+ MSpec.register :enter, self
+ MSpec.register :leave, self
+ end
+
+ def start
+ print <<-EOH
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
+ "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+<title>Spec Output For #{RUBY_NAME} (#{RUBY_VERSION})</title>
+<style type="text/css">
+ul {
+ list-style: none;
+}
+.fail {
+ color: red;
+}
+.pass {
+ color: green;
+}
+#details :target {
+ background-color: #ffffe0;
+}
+</style>
+</head>
+<body>
+EOH
+ end
+
+ def enter(describe)
+ print "<div><p>#{describe}</p>\n<ul>\n"
+ end
+
+ def leave
+ print "</ul>\n</div>\n"
+ end
+
+ def exception(exception)
+ super
+ outcome = exception.failure? ? "FAILED" : "ERROR"
+ print %[<li class="fail">- #{exception.it} (<a href="#details-#{@count}">]
+ print %[#{outcome} - #{@count}</a>)</li>\n]
+ end
+
+ def after(state)
+ print %[<li class="pass">- #{state.it}</li>\n] unless exception?
+ end
+
+ def finish
+ success = @exceptions.empty?
+ unless success
+ print "<hr>\n"
+ print %[<ol id="details">]
+ count = 0
+ @exceptions.each do |exc|
+ outcome = exc.failure? ? "FAILED" : "ERROR"
+ print %[\n<li id="details-#{count += 1}"><p>#{escape(exc.description)} #{outcome}</p>\n<p>]
+ print escape(exc.message)
+ print "</p>\n<pre>\n"
+ print escape(exc.backtrace)
+ print "</pre>\n</li>\n"
+ end
+ print "</ol>\n"
+ end
+ print %[<p>#{@timer.format}</p>\n]
+ print %[<p class="#{success ? "pass" : "fail"}">#{@tally.format}</p>\n]
+ print "</body>\n</html>\n"
+ end
+
+ def escape(string)
+ string.gsub("&", "&nbsp;").gsub("<", "&lt;").gsub(">", "&gt;")
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/junit.rb b/spec/mspec/lib/mspec/runner/formatters/junit.rb
new file mode 100644
index 0000000000..647deee7e1
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/junit.rb
@@ -0,0 +1,89 @@
+require 'mspec/expectations/expectations'
+require 'mspec/utils/ruby_name'
+require 'mspec/runner/formatters/yaml'
+
+class JUnitFormatter < YamlFormatter
+ def initialize(out=nil)
+ super
+ @tests = []
+ end
+
+ def after(state = nil)
+ super
+ @tests << {:test => state, :exception => false} unless exception?
+ end
+
+ def exception(exception)
+ super
+ @tests << {:test => exception, :exception => true}
+ end
+
+ def finish
+ switch
+
+ time = @timer.elapsed
+ tests = @tally.counter.examples
+ errors = @tally.counter.errors
+ failures = @tally.counter.failures
+
+ printf <<-XML
+
+<?xml version="1.0" encoding="UTF-8" ?>
+ <testsuites
+ testCount="#{tests}"
+ errorCount="#{errors}"
+ failureCount="#{failures}"
+ timeCount="#{time}" time="#{time}">
+ <testsuite
+ tests="#{tests}"
+ errors="#{errors}"
+ failures="#{failures}"
+ time="#{time}"
+ name="Spec Output For #{::RUBY_NAME} (#{::RUBY_VERSION})">
+ XML
+ @tests.each do |h|
+ description = encode_for_xml h[:test].description
+
+ printf <<-XML, "Spec", description, 0.0
+ <testcase classname="%s" name="%s" time="%f">
+ XML
+ if h[:exception]
+ outcome = h[:test].failure? ? "failure" : "error"
+ message = encode_for_xml h[:test].message
+ backtrace = encode_for_xml h[:test].backtrace
+ print <<-XML
+ <#{outcome} message="error in #{description}" type="#{outcome}">
+ #{message}
+ #{backtrace}
+ </#{outcome}>
+ XML
+ end
+ print <<-XML
+ </testcase>
+ XML
+ end
+
+ print <<-XML
+ </testsuite>
+ </testsuites>
+ XML
+ end
+
+ private
+ LT = "&lt;"
+ GT = "&gt;"
+ QU = "&quot;"
+ AP = "&apos;"
+ AM = "&amp;"
+ TARGET_ENCODING = "ISO-8859-1"
+
+ def encode_for_xml(str)
+ encode_as_latin1(str).gsub("<", LT).gsub(">", GT).
+ gsub('"', QU).gsub("'", AP).gsub("&", AM).
+ tr("\x00-\x08", "?")
+ end
+
+ def encode_as_latin1(str)
+ str.encode(TARGET_ENCODING, :undef => :replace, :invalid => :replace)
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/method.rb b/spec/mspec/lib/mspec/runner/formatters/method.rb
new file mode 100644
index 0000000000..ff115193fd
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/method.rb
@@ -0,0 +1,93 @@
+require 'mspec/runner/formatters/dotted'
+
+class MethodFormatter < DottedFormatter
+ attr_accessor :methods
+
+ def initialize(out=nil)
+ super
+ @methods = Hash.new do |h, k|
+ hash = {}
+ hash[:examples] = 0
+ hash[:expectations] = 0
+ hash[:failures] = 0
+ hash[:errors] = 0
+ hash[:exceptions] = []
+ h[k] = hash
+ end
+ end
+
+ # Returns the type of method as a "class", "instance",
+ # or "unknown".
+ def method_type(sep)
+ case sep
+ when '.', '::'
+ "class"
+ when '#'
+ "instance"
+ else
+ "unknown"
+ end
+ end
+
+ # Callback for the MSpec :before event. Parses the
+ # describe string into class and method if possible.
+ # Resets the tallies so the counts are only for this
+ # example.
+ def before(state)
+ super
+
+ # The pattern for a method name is not correctly
+ # restrictive but it is simplistic and useful
+ # for our purpose.
+ /^([A-Za-z_]+\w*)(\.|#|::)([^ ]+)/ =~ state.describe
+ @key = $1 && $2 && $3 ? "#{$1}#{$2}#{$3}" : state.describe
+
+ unless methods.key? @key
+ h = methods[@key]
+ h[:class] = "#{$1}"
+ h[:method] = "#{$3}"
+ h[:type] = method_type $2
+ h[:description] = state.description
+ end
+
+ tally.counter.examples = 0
+ tally.counter.expectations = 0
+ tally.counter.failures = 0
+ tally.counter.errors = 0
+
+ @exceptions = []
+ end
+
+ # Callback for the MSpec :after event. Sets or adds to
+ # tallies for the example block.
+ def after(state)
+ h = methods[@key]
+ h[:examples] += tally.counter.examples
+ h[:expectations] += tally.counter.expectations
+ h[:failures] += tally.counter.failures
+ h[:errors] += tally.counter.errors
+ @exceptions.each do |exc|
+ h[:exceptions] << "#{exc.message}\n#{exc.backtrace}\n"
+ end
+ end
+
+ # Callback for the MSpec :finish event. Prints out the
+ # summary information in YAML format for all the methods.
+ def finish
+ print "---\n"
+
+ methods.each do |key, hash|
+ print key.inspect, ":\n"
+ print " class: ", hash[:class].inspect, "\n"
+ print " method: ", hash[:method].inspect, "\n"
+ print " type: ", hash[:type], "\n"
+ print " description: ", hash[:description].inspect, "\n"
+ print " examples: ", hash[:examples], "\n"
+ print " expectations: ", hash[:expectations], "\n"
+ print " failures: ", hash[:failures], "\n"
+ print " errors: ", hash[:errors], "\n"
+ print " exceptions:\n"
+ hash[:exceptions].each { |exc| print " - ", exc.inspect, "\n" }
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/multi.rb b/spec/mspec/lib/mspec/runner/formatters/multi.rb
new file mode 100644
index 0000000000..bcc5411e6f
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/multi.rb
@@ -0,0 +1,36 @@
+require 'mspec/runner/formatters/spinner'
+require 'yaml'
+
+class MultiFormatter < SpinnerFormatter
+ def initialize(out=nil)
+ super(out)
+ @counter = @tally = Tally.new
+ @timer = TimerAction.new
+ @timer.start
+ end
+
+ def aggregate_results(files)
+ @timer.finish
+ @exceptions = []
+
+ files.each do |file|
+ d = File.open(file, "r") { |f| YAML.load f }
+ File.delete file
+
+ @exceptions += Array(d['exceptions'])
+ @tally.files! d['files']
+ @tally.examples! d['examples']
+ @tally.expectations! d['expectations']
+ @tally.errors! d['errors']
+ @tally.failures! d['failures']
+ end
+ end
+
+ def print_exception(exc, count)
+ print "\n#{count})\n#{exc}\n"
+ end
+
+ def finish
+ super(false)
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/profile.rb b/spec/mspec/lib/mspec/runner/formatters/profile.rb
new file mode 100644
index 0000000000..498cd4a3b7
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/profile.rb
@@ -0,0 +1,70 @@
+require 'mspec/expectations/expectations'
+require 'mspec/runner/formatters/dotted'
+
+class ProfileFormatter < DottedFormatter
+ def initialize(out=nil)
+ super
+
+ @describe_name = nil
+ @describe_time = nil
+ @describes = []
+ @its = []
+ end
+
+ def register
+ super
+ MSpec.register :enter, self
+ end
+
+ # Callback for the MSpec :enter event. Prints the
+ # +describe+ block string.
+ def enter(describe)
+ if @describe_time
+ @describes << [@describe_name, Time.now.to_f - @describe_time]
+ end
+
+ @describe_name = describe
+ @describe_time = Time.now.to_f
+ end
+
+ # Callback for the MSpec :before event. Prints the
+ # +it+ block string.
+ def before(state)
+ super
+
+ @it_name = state.it
+ @it_time = Time.now.to_f
+ end
+
+ # Callback for the MSpec :after event. Prints a
+ # newline to finish the description string output.
+ def after(state)
+ @its << [@describe_name, @it_name, Time.now.to_f - @it_time]
+ super
+ end
+
+ def finish
+ puts "\nProfiling info:"
+
+ desc = @describes.sort { |a,b| b.last <=> a.last }
+ desc.delete_if { |a| a.last <= 0.001 }
+ show = desc[0, 100]
+
+ puts "Top #{show.size} describes:"
+
+ show.each do |des, time|
+ printf "%3.3f - %s\n", time, des
+ end
+
+ its = @its.sort { |a,b| b.last <=> a.last }
+ its.delete_if { |a| a.last <= 0.001 }
+ show = its[0, 100]
+
+ puts "\nTop #{show.size} its:"
+ show.each do |des, it, time|
+ printf "%3.3f - %s %s\n", time, des, it
+ end
+
+ super
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/specdoc.rb b/spec/mspec/lib/mspec/runner/formatters/specdoc.rb
new file mode 100644
index 0000000000..29adde3c5c
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/specdoc.rb
@@ -0,0 +1,41 @@
+require 'mspec/expectations/expectations'
+require 'mspec/runner/formatters/dotted'
+
+class SpecdocFormatter < DottedFormatter
+ def register
+ super
+ MSpec.register :enter, self
+ end
+
+ # Callback for the MSpec :enter event. Prints the
+ # +describe+ block string.
+ def enter(describe)
+ print "\n#{describe}\n"
+ end
+
+ # Callback for the MSpec :before event. Prints the
+ # +it+ block string.
+ def before(state)
+ super
+ print "- #{state.it}"
+ end
+
+ # Callback for the MSpec :exception event. Prints
+ # either 'ERROR - X' or 'FAILED - X' where _X_ is
+ # the sequential number of the exception raised. If
+ # there has already been an exception raised while
+ # evaluating this example, it prints another +it+
+ # block description string so that each discription
+ # string has an associated 'ERROR' or 'FAILED'
+ def exception(exception)
+ print "\n- #{exception.it}" if exception?
+ super
+ print " (#{exception.failure? ? 'FAILED' : 'ERROR'} - #{@count})"
+ end
+
+ # Callback for the MSpec :after event. Prints a
+ # newline to finish the description string output.
+ def after(state)
+ print "\n"
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/spinner.rb b/spec/mspec/lib/mspec/runner/formatters/spinner.rb
new file mode 100644
index 0000000000..f6f35cc476
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/spinner.rb
@@ -0,0 +1,117 @@
+require 'mspec/expectations/expectations'
+require 'mspec/runner/formatters/dotted'
+
+class SpinnerFormatter < DottedFormatter
+ attr_reader :length
+
+ Spins = %w!| / - \\!
+ HOUR = 3600
+ MIN = 60
+
+ def initialize(out=nil)
+ super(nil)
+
+ @which = 0
+ @loaded = 0
+ self.length = 40
+ @percent = 0
+ @start = Time.now
+
+ term = ENV['TERM']
+ @color = (term != "dumb")
+ @fail_color = "32"
+ @error_color = "32"
+ end
+
+ def register
+ super
+
+ MSpec.register :start, self
+ MSpec.register :unload, self
+ MSpec.unregister :before, self
+ end
+
+ def length=(length)
+ @length = length
+ @ratio = 100.0 / length
+ @position = length / 2 - 2
+ end
+
+ def compute_etr
+ return @etr = "00:00:00" if @percent == 0
+ elapsed = Time.now - @start
+ remain = (100 * elapsed / @percent) - elapsed
+
+ hour = remain >= HOUR ? (remain / HOUR).to_i : 0
+ remain -= hour * HOUR
+ min = remain >= MIN ? (remain / MIN).to_i : 0
+ sec = remain - min * MIN
+
+ @etr = "%02d:%02d:%02d" % [hour, min, sec]
+ end
+
+ def compute_percentage
+ @percent = @loaded * 100 / @total
+ bar = ("=" * (@percent / @ratio)).ljust @length
+ label = "%d%%" % @percent
+ bar[@position, label.size] = label
+ @bar = bar
+ end
+
+ def compute_progress
+ compute_percentage
+ compute_etr
+ end
+
+ def progress_line
+ @which = (@which + 1) % Spins.size
+ data = [Spins[@which], @bar, @etr, @counter.failures, @counter.errors]
+ if @color
+ "\r[%s | %s | %s] \e[0;#{@fail_color}m%6dF \e[0;#{@error_color}m%6dE\e[0m " % data
+ else
+ "\r[%s | %s | %s] %6dF %6dE " % data
+ end
+ end
+
+ def clear_progress_line
+ print "\r#{' '*progress_line.length}"
+ end
+
+ # Callback for the MSpec :start event. Stores the total
+ # number of files that will be processed.
+ def start
+ @total = MSpec.retrieve(:files).size
+ compute_progress
+ print progress_line
+ end
+
+ # Callback for the MSpec :unload event. Increments the number
+ # of files that have been run.
+ def unload
+ @loaded += 1
+ compute_progress
+ print progress_line
+ end
+
+ # Callback for the MSpec :exception event. Changes the color
+ # used to display the tally of errors and failures
+ def exception(exception)
+ super
+ @fail_color = "31" if exception.failure?
+ @error_color = "33" unless exception.failure?
+
+ clear_progress_line
+ print_exception(exception, @count)
+ end
+
+ # Callback for the MSpec :after event. Updates the spinner.
+ def after(state)
+ print progress_line
+ end
+
+ def finish(printed_exceptions = true)
+ # We already printed the exceptions
+ @exceptions = [] if printed_exceptions
+ super()
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/summary.rb b/spec/mspec/lib/mspec/runner/formatters/summary.rb
new file mode 100644
index 0000000000..0c9207194c
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/summary.rb
@@ -0,0 +1,11 @@
+require 'mspec/expectations/expectations'
+require 'mspec/runner/formatters/dotted'
+
+class SummaryFormatter < DottedFormatter
+ # Callback for the MSpec :after event. Overrides the
+ # callback provided by +DottedFormatter+ and does not
+ # print any output for each example evaluated.
+ def after(state)
+ # do nothing
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/unit.rb b/spec/mspec/lib/mspec/runner/formatters/unit.rb
new file mode 100644
index 0000000000..69b68dc0d5
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/unit.rb
@@ -0,0 +1,21 @@
+require 'mspec/expectations/expectations'
+require 'mspec/runner/formatters/dotted'
+
+class UnitdiffFormatter < DottedFormatter
+ def finish
+ print "\n\n#{@timer.format}\n"
+ count = 0
+ @exceptions.each do |exc|
+ outcome = exc.failure? ? "FAILED" : "ERROR"
+ print "\n#{count += 1})\n#{exc.description} #{outcome}\n"
+ print exc.message, ": \n"
+ print exc.backtrace, "\n"
+ end
+ print "\n#{@tally.format}\n"
+ end
+
+ def backtrace(exc)
+ exc.backtrace && exc.backtrace.join("\n")
+ end
+ private :backtrace
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/yaml.rb b/spec/mspec/lib/mspec/runner/formatters/yaml.rb
new file mode 100644
index 0000000000..090a9b1b9d
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/yaml.rb
@@ -0,0 +1,42 @@
+require 'mspec/expectations/expectations'
+require 'mspec/runner/formatters/dotted'
+
+class YamlFormatter < DottedFormatter
+ def initialize(out=nil)
+ super(nil)
+
+ if out.nil?
+ @finish = $stdout
+ else
+ @finish = File.open out, "w"
+ end
+ end
+
+ def switch
+ @out = @finish
+ end
+
+ def after(state)
+ end
+
+ def finish
+ switch
+
+ print "---\n"
+ print "exceptions:\n"
+ @exceptions.each do |exc|
+ outcome = exc.failure? ? "FAILED" : "ERROR"
+ str = "#{exc.description} #{outcome}\n"
+ str << exc.message << "\n" << exc.backtrace
+ print "- ", str.inspect, "\n"
+ end
+
+ print "time: ", @timer.elapsed, "\n"
+ print "files: ", @tally.counter.files, "\n"
+ print "examples: ", @tally.counter.examples, "\n"
+ print "expectations: ", @tally.counter.expectations, "\n"
+ print "failures: ", @tally.counter.failures, "\n"
+ print "errors: ", @tally.counter.errors, "\n"
+ print "tagged: ", @tally.counter.tagged, "\n"
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/mspec.rb b/spec/mspec/lib/mspec/runner/mspec.rb
new file mode 100644
index 0000000000..0ff0de36ca
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/mspec.rb
@@ -0,0 +1,391 @@
+require 'mspec/runner/context'
+require 'mspec/runner/exception'
+require 'mspec/runner/tag'
+
+module MSpec
+
+ @exit = nil
+ @abort = nil
+ @start = nil
+ @enter = nil
+ @before = nil
+ @add = nil
+ @after = nil
+ @leave = nil
+ @finish = nil
+ @exclude = nil
+ @include = nil
+ @leave = nil
+ @load = nil
+ @unload = nil
+ @tagged = nil
+ @current = nil
+ @example = nil
+ @modes = []
+ @shared = {}
+ @guarded = []
+ @features = {}
+ @exception = nil
+ @randomize = nil
+ @repeat = nil
+ @expectation = nil
+ @expectations = false
+
+ def self.describe(mod, options=nil, &block)
+ state = ContextState.new mod, options
+ state.parent = current
+
+ MSpec.register_current state
+ state.describe(&block)
+
+ state.process unless state.shared? or current
+ end
+
+ def self.process
+ STDOUT.puts RUBY_DESCRIPTION
+
+ actions :start
+ files
+ actions :finish
+ end
+
+ def self.each_file(&block)
+ if ENV["MSPEC_MULTI"]
+ STDOUT.print "."
+ STDOUT.flush
+ while (file = STDIN.gets.chomp) != "QUIT"
+ yield file
+ STDOUT.print "."
+ STDOUT.flush
+ end
+ else
+ return unless files = retrieve(:files)
+ shuffle files if randomize?
+ files.each(&block)
+ end
+ end
+
+ def self.files
+ each_file do |file|
+ setup_env
+ store :file, file
+ actions :load
+ protect("loading #{file}") { Kernel.load file }
+ actions :unload
+ end
+ end
+
+ def self.setup_env
+ @env = Object.new
+ @env.extend MSpec
+ end
+
+ def self.actions(action, *args)
+ actions = retrieve(action)
+ actions.each { |obj| obj.send action, *args } if actions
+ end
+
+ def self.protect(location, &block)
+ begin
+ @env.instance_eval(&block)
+ return true
+ rescue SystemExit => e
+ raise e
+ rescue Exception => exc
+ register_exit 1
+ actions :exception, ExceptionState.new(current && current.state, location, exc)
+ return false
+ end
+ end
+
+ # Guards can be nested, so a stack is necessary to know when we have
+ # exited the toplevel guard.
+ def self.guard
+ @guarded << true
+ end
+
+ def self.unguard
+ @guarded.pop
+ end
+
+ def self.guarded?
+ !@guarded.empty?
+ end
+
+ # Sets the toplevel ContextState to +state+.
+ def self.register_current(state)
+ store :current, state
+ end
+
+ # Sets the toplevel ContextState to +nil+.
+ def self.clear_current
+ store :current, nil
+ end
+
+ # Returns the toplevel ContextState.
+ def self.current
+ retrieve :current
+ end
+
+ # Stores the shared ContextState keyed by description.
+ def self.register_shared(state)
+ @shared[state.to_s] = state
+ end
+
+ # Returns the shared ContextState matching description.
+ def self.retrieve_shared(desc)
+ @shared[desc.to_s]
+ end
+
+ # Stores the exit code used by the runner scripts.
+ def self.register_exit(code)
+ store :exit, code
+ end
+
+ # Retrieves the stored exit code.
+ def self.exit_code
+ retrieve(:exit).to_i
+ end
+
+ # Stores the list of files to be evaluated.
+ def self.register_files(files)
+ store :files, files
+ end
+
+ # Stores one or more substitution patterns for transforming
+ # a spec filename into a tags filename, where each pattern
+ # has the form:
+ #
+ # [Regexp, String]
+ #
+ # See also +tags_file+.
+ def self.register_tags_patterns(patterns)
+ store :tags_patterns, patterns
+ end
+
+ # Registers an operating mode. Modes recognized by MSpec:
+ #
+ # :pretend - actions execute but specs are not run
+ # :verify - specs are run despite guards and the result is
+ # verified to match the expectation of the guard
+ # :report - specs that are guarded are reported
+ # :unguarded - all guards are forced off
+ def self.register_mode(mode)
+ modes = retrieve :modes
+ modes << mode unless modes.include? mode
+ end
+
+ # Clears all registered modes.
+ def self.clear_modes
+ store :modes, []
+ end
+
+ # Returns +true+ if +mode+ is registered.
+ def self.mode?(mode)
+ retrieve(:modes).include? mode
+ end
+
+ def self.enable_feature(feature)
+ retrieve(:features)[feature] = true
+ end
+
+ def self.disable_feature(feature)
+ retrieve(:features)[feature] = false
+ end
+
+ def self.feature_enabled?(feature)
+ retrieve(:features)[feature] || false
+ end
+
+ def self.retrieve(symbol)
+ instance_variable_get :"@#{symbol}"
+ end
+
+ def self.store(symbol, value)
+ instance_variable_set :"@#{symbol}", value
+ end
+
+ # This method is used for registering actions that are
+ # run at particular points in the spec cycle:
+ # :start before any specs are run
+ # :load before a spec file is loaded
+ # :enter before a describe block is run
+ # :before before a single spec is run
+ # :add while a describe block is adding examples to run later
+ # :expectation before a 'should', 'should_receive', etc.
+ # :example after an example block is run, passed the block
+ # :exception after an exception is rescued
+ # :after after a single spec is run
+ # :leave after a describe block is run
+ # :unload after a spec file is run
+ # :finish after all specs are run
+ #
+ # Objects registered as actions above should respond to
+ # a method of the same name. For example, if an object
+ # is registered as a :start action, it should respond to
+ # a #start method call.
+ #
+ # Additionally, there are two "action" lists for
+ # filtering specs:
+ # :include return true if the spec should be run
+ # :exclude return true if the spec should NOT be run
+ #
+ def self.register(symbol, action)
+ unless value = retrieve(symbol)
+ value = store symbol, []
+ end
+ value << action unless value.include? action
+ end
+
+ def self.unregister(symbol, action)
+ if value = retrieve(symbol)
+ value.delete action
+ end
+ end
+
+ def self.randomize(flag=true)
+ @randomize = flag
+ end
+
+ def self.randomize?
+ @randomize == true
+ end
+
+ def self.repeat=(times)
+ @repeat = times
+ end
+
+ def self.repeat
+ (@repeat || 1).times do
+ yield
+ end
+ end
+
+ def self.shuffle(ary)
+ return if ary.empty?
+
+ size = ary.size
+ size.times do |i|
+ r = rand(size - i - 1)
+ ary[i], ary[r] = ary[r], ary[i]
+ end
+ end
+
+ # Records that an expectation has been encountered in an example.
+ def self.expectation
+ store :expectations, true
+ end
+
+ # Returns true if an expectation has been encountered
+ def self.expectation?
+ retrieve :expectations
+ end
+
+ # Resets the flag that an expectation has been encountered in an example.
+ def self.clear_expectations
+ store :expectations, false
+ end
+
+ # Transforms a spec filename into a tags filename by applying each
+ # substitution pattern in :tags_pattern. The default patterns are:
+ #
+ # [%r(/spec/), '/spec/tags/'], [/_spec.rb$/, '_tags.txt']
+ #
+ # which will perform the following transformation:
+ #
+ # path/to/spec/class/method_spec.rb => path/to/spec/tags/class/method_tags.txt
+ #
+ # See also +register_tags_patterns+.
+ def self.tags_file
+ patterns = retrieve(:tags_patterns) ||
+ [[%r(spec/), 'spec/tags/'], [/_spec.rb$/, '_tags.txt']]
+ patterns.inject(retrieve(:file).dup) do |file, pattern|
+ file.gsub(*pattern)
+ end
+ end
+
+ # Returns a list of tags matching any tag string in +keys+ based
+ # on the return value of <tt>keys.include?("tag_name")</tt>
+ def self.read_tags(keys)
+ tags = []
+ file = tags_file
+ if File.exist? file
+ File.open(file, "r:utf-8") do |f|
+ f.each_line do |line|
+ line.chomp!
+ next if line.empty?
+ tag = SpecTag.new line
+ tags << tag if keys.include? tag.tag
+ end
+ end
+ end
+ tags
+ end
+
+ def self.make_tag_dir(path)
+ parent = File.dirname(path)
+ return if File.exist? parent
+ begin
+ Dir.mkdir(parent)
+ rescue SystemCallError
+ make_tag_dir(parent)
+ Dir.mkdir(parent)
+ end
+ end
+
+ # Writes each tag in +tags+ to the tag file. Overwrites the
+ # tag file if it exists.
+ def self.write_tags(tags)
+ file = tags_file
+ make_tag_dir(file)
+ File.open(file, "w:utf-8") do |f|
+ tags.each { |t| f.puts t }
+ end
+ end
+
+ # Writes +tag+ to the tag file if it does not already exist.
+ # Returns +true+ if the tag is written, +false+ otherwise.
+ def self.write_tag(tag)
+ tags = read_tags([tag.tag])
+ tags.each do |t|
+ if t.tag == tag.tag and t.description == tag.description
+ return false
+ end
+ end
+
+ file = tags_file
+ make_tag_dir(file)
+ File.open(file, "a:utf-8") { |f| f.puts tag.to_s }
+ return true
+ end
+
+ # Deletes +tag+ from the tag file if it exists. Returns +true+
+ # if the tag is deleted, +false+ otherwise. Deletes the tag
+ # file if it is empty.
+ def self.delete_tag(tag)
+ deleted = false
+ desc = tag.escape(tag.description)
+ file = tags_file
+ if File.exist? file
+ lines = IO.readlines(file)
+ File.open(file, "w:utf-8") do |f|
+ lines.each do |line|
+ line = line.chomp
+ if line.start_with?(tag.tag) and line.end_with?(desc)
+ deleted = true
+ else
+ f.puts line unless line.empty?
+ end
+ end
+ end
+ File.delete file unless File.size? file
+ end
+ return deleted
+ end
+
+ # Removes the tag file associated with a spec file.
+ def self.delete_tags
+ file = tags_file
+ File.delete file if File.exist? file
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/object.rb b/spec/mspec/lib/mspec/runner/object.rb
new file mode 100644
index 0000000000..018e356149
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/object.rb
@@ -0,0 +1,28 @@
+class Object
+ def before(at=:each, &block)
+ MSpec.current.before at, &block
+ end
+
+ def after(at=:each, &block)
+ MSpec.current.after at, &block
+ end
+
+ def describe(mod, msg=nil, options=nil, &block)
+ MSpec.describe mod, msg, &block
+ end
+
+ def it(msg, &block)
+ MSpec.current.it msg, &block
+ end
+
+ def it_should_behave_like(desc)
+ MSpec.current.it_should_behave_like desc
+ end
+
+ # For ReadRuby compatiability
+ def doc(*a)
+ end
+
+ alias_method :context, :describe
+ alias_method :specify, :it
+end
diff --git a/spec/mspec/lib/mspec/runner/shared.rb b/spec/mspec/lib/mspec/runner/shared.rb
new file mode 100644
index 0000000000..336e35f6ac
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/shared.rb
@@ -0,0 +1,12 @@
+require 'mspec/runner/mspec'
+
+class Object
+ def it_behaves_like(desc, meth, obj=nil)
+ send :before, :all do
+ @method = meth
+ @object = obj
+ end
+
+ send :it_should_behave_like, desc.to_s
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/tag.rb b/spec/mspec/lib/mspec/runner/tag.rb
new file mode 100644
index 0000000000..e2275ad3a6
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/tag.rb
@@ -0,0 +1,38 @@
+class SpecTag
+ attr_accessor :tag, :comment, :description
+
+ def initialize(string=nil)
+ parse(string) if string
+ end
+
+ def parse(string)
+ m = /^([^()#:]+)(\(([^)]+)?\))?:(.*)$/.match string
+ @tag, @comment, description = m.values_at(1, 3, 4) if m
+ @description = unescape description
+ end
+
+ def unescape(str)
+ return unless str
+ if str[0] == ?" and str[-1] == ?"
+ str[1..-2].gsub('\n', "\n")
+ else
+ str
+ end
+ end
+
+ def escape(str)
+ if str.include? "\n"
+ %["#{str.gsub("\n", '\n')}"]
+ else
+ str
+ end
+ end
+
+ def to_s
+ "#{@tag}#{ "(#{@comment})" if @comment }:#{escape @description}"
+ end
+
+ def ==(o)
+ @tag == o.tag and @comment == o.comment and @description == o.description
+ end
+end