summaryrefslogtreecommitdiff
path: root/spec/mspec/lib/mspec/runner/mspec.rb
diff options
context:
space:
mode:
Diffstat (limited to 'spec/mspec/lib/mspec/runner/mspec.rb')
-rw-r--r--spec/mspec/lib/mspec/runner/mspec.rb391
1 files changed, 391 insertions, 0 deletions
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