mspec/lib/mspec/runner/mspec.rb
require 'mspec/runner/context'
require 'mspec/runner/exception'
require 'mspec/runner/tag'
require 'fileutils'
module MSpec
@exit = nil
@start = nil
@enter = nil
@before = nil
@after = nil
@leave = nil
@finish = nil
@exclude = nil
@include = nil
@leave = nil
@load = nil
@unload = nil
@current = 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
actions :start
files
actions :finish
end
def self.files
return unless files = retrieve(:files)
shuffle files if randomize?
files.each do |file|
@env = Object.new
@env.extend MSpec
store :file, file
actions :load
protect("loading #{file}") { Kernel.load file }
actions :unload
end
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
raise
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?
not @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, "rb") do |f|
f.each_line do |line|
line.chomp!
next if line.empty?
tag = SpecTag.new line.chomp
tags << tag if keys.include? tag.tag
end
end
end
tags
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
path = File.dirname file
FileUtils.mkdir_p path unless File.exist? path
File.open(file, "wb") 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)
string = tag.to_s
file = tags_file
path = File.dirname file
FileUtils.mkdir_p path unless File.exist? path
if File.exist? file
File.open(file, "rb") do |f|
f.each_line { |line| return false if line.chomp == string }
end
end
File.open(file, "ab") { |f| f.puts string }
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
pattern = /#{tag.tag}.*#{Regexp.escape(tag.escape(tag.description))}/
file = tags_file
if File.exist? file
lines = IO.readlines(file)
File.open(file, "wb") do |f|
lines.each do |line|
unless pattern =~ line.chomp
f.puts line unless line.empty?
else
deleted = true
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