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 = {} @exception = nil @randomize = 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 # 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.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 # :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.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 keys.include?("tag_name") def self.read_tags(keys) tags = [] file = tags_file if File.exist? file File.open(file, "r") 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, "w") 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, "r") do |f| f.each_line { |line| return false if line.chomp == string } end end File.open(file, "a") { |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.description}/ file = tags_file if File.exist? file lines = IO.readlines(file) File.open(file, "w") 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.exists? file end end