Updated MSpec source to 1c3ee1c8.
[rbx.git] / mspec / lib / mspec / runner / mspec.rb
blob7c84427cd67577c28bb119eaa20afeec546cf630
1 require 'mspec/runner/context'
2 require 'mspec/runner/exception'
3 require 'mspec/runner/tag'
4 require 'fileutils'
6 module MSpec
8   @exit    = nil
9   @start   = nil
10   @enter   = nil
11   @before  = nil
12   @after   = nil
13   @leave   = nil
14   @finish  = nil
15   @exclude = nil
16   @include = nil
17   @leave   = nil
18   @mode    = nil
19   @load    = nil
20   @unload  = nil
21   @current = nil
22   @shared  = {}
23   @exception    = nil
24   @randomize    = nil
25   @expectation  = nil
26   @expectations = false
28   def self.describe(mod, options=nil, &block)
29     state = ContextState.new mod, options
30     state.parent = current
32     MSpec.register_current state
33     state.describe(&block)
35     state.process unless state.shared? or current
36   end
38   def self.process
39     actions :start
40     files
41     actions :finish
42   end
44   def self.files
45     return unless files = retrieve(:files)
47     shuffle files if randomize?
48     files.each do |file|
49       @env = Object.new
50       @env.extend MSpec
52       store :file, file
53       actions :load
54       protect("loading #{file}") { Kernel.load file }
55       actions :unload
56     end
57   end
59   def self.actions(action, *args)
60     actions = retrieve(action)
61     actions.each { |obj| obj.send action, *args } if actions
62   end
64   def self.protect(location, &block)
65     begin
66       @env.instance_eval(&block)
67       return true
68     rescue SystemExit
69       raise
70     rescue Exception => exc
71       register_exit 1
72       actions :exception, ExceptionState.new(current && current.state, location, exc)
73       return false
74     end
75   end
77   # Sets the toplevel ContextState to +state+.
78   def self.register_current(state)
79     store :current, state
80   end
82   # Sets the toplevel ContextState to +nil+.
83   def self.clear_current
84     store :current, nil
85   end
87   # Returns the toplevel ContextState.
88   def self.current
89     retrieve :current
90   end
92   # Stores the shared ContextState keyed by description.
93   def self.register_shared(state)
94     @shared[state.to_s] = state
95   end
97   # Returns the shared ContextState matching description.
98   def self.retrieve_shared(desc)
99     @shared[desc.to_s]
100   end
102   # Stores the exit code used by the runner scripts.
103   def self.register_exit(code)
104     store :exit, code
105   end
107   # Retrieves the stored exit code.
108   def self.exit_code
109     retrieve(:exit).to_i
110   end
112   # Stores the list of files to be evaluated.
113   def self.register_files(files)
114     store :files, files
115   end
117   # Stores one or more substitution patterns for transforming
118   # a spec filename into a tags filename, where each pattern
119   # has the form:
120   #
121   #   [Regexp, String]
122   #
123   # See also +tags_file+.
124   def self.register_tags_patterns(patterns)
125     store :tags_patterns, patterns
126   end
128   def self.register_mode(mode)
129     store :mode, mode
130   end
132   def self.retrieve(symbol)
133     instance_variable_get :"@#{symbol}"
134   end
136   def self.store(symbol, value)
137     instance_variable_set :"@#{symbol}", value
138   end
140   # This method is used for registering actions that are
141   # run at particular points in the spec cycle:
142   #   :start        before any specs are run
143   #   :load         before a spec file is loaded
144   #   :enter        before a describe block is run
145   #   :before       before a single spec is run
146   #   :expectation  before a 'should', 'should_receive', etc.
147   #   :example      after an example block is run, passed the block
148   #   :exception    after an exception is rescued
149   #   :after        after a single spec is run
150   #   :leave        after a describe block is run
151   #   :unload       after a spec file is run
152   #   :finish       after all specs are run
153   #
154   # Objects registered as actions above should respond to
155   # a method of the same name. For example, if an object
156   # is registered as a :start action, it should respond to
157   # a #start method call.
158   #
159   # Additionally, there are two "action" lists for
160   # filtering specs:
161   #   :include  return true if the spec should be run
162   #   :exclude  return true if the spec should NOT be run
163   #
164   def self.register(symbol, action)
165     unless value = retrieve(symbol)
166       value = store symbol, []
167     end
168     value << action unless value.include? action
169   end
171   def self.unregister(symbol, action)
172     if value = retrieve(symbol)
173       value.delete action
174     end
175   end
177   def self.verify_mode?
178     @mode == :verify
179   end
181   def self.report_mode?
182     @mode == :report
183   end
185   def self.pretend_mode?
186     @mode == :pretend
187   end
189   def self.randomize(flag=true)
190     @randomize = flag
191   end
193   def self.randomize?
194     @randomize == true
195   end
197   def self.shuffle(ary)
198     return if ary.empty?
200     size = ary.size
201     size.times do |i|
202       r = rand(size - i - 1)
203       ary[i], ary[r] = ary[r], ary[i]
204     end
205   end
207   # Records that an expectation has been encountered in an example.
208   def self.expectation
209     store :expectations, true
210   end
212   # Returns true if an expectation has been encountered
213   def self.expectation?
214     retrieve :expectations
215   end
217   # Resets the flag that an expectation has been encountered in an example.
218   def self.clear_expectations
219     store :expectations, false
220   end
222   # Transforms a spec filename into a tags filename by applying each
223   # substitution pattern in :tags_pattern. The default patterns are:
224   #
225   #   [%r(/spec/), '/spec/tags/'], [/_spec.rb$/, '_tags.txt']
226   #
227   # which will perform the following transformation:
228   #
229   #   path/to/spec/class/method_spec.rb => path/to/spec/tags/class/method_tags.txt
230   #
231   # See also +register_tags_patterns+.
232   def self.tags_file
233     patterns = retrieve(:tags_patterns) ||
234                [[%r(spec/), 'spec/tags/'], [/_spec.rb$/, '_tags.txt']]
235     patterns.inject(retrieve(:file).dup) do |file, pattern|
236       file.gsub(*pattern)
237     end
238   end
240   def self.read_tags(*keys)
241     tags = []
242     file = tags_file
243     if File.exist? file
244       File.open(file, "r") do |f|
245         f.each_line do |line|
246           tag = SpecTag.new line.chomp
247           tags << tag if keys.include? tag.tag
248         end
249       end
250     end
251     tags
252   end
254   def self.write_tag(tag)
255     string = tag.to_s
256     file = tags_file
257     path = File.dirname file
258     FileUtils.mkdir_p(path) unless File.exist?(path)
259     if File.exist? file
260       File.open(file, "r") do |f|
261         f.each_line { |line| return false if line.chomp == string }
262       end
263     end
264     File.open(file, "a") { |f| f.puts string }
265     return true
266   end
268   def self.delete_tag(tag)
269     deleted = false
270     pattern = /#{tag.tag}.*#{Regexp.escape tag.description}/
271     file = tags_file
272     if File.exist? file
273       lines = IO.readlines(file)
274       File.open(file, "w") do |f|
275         lines.each do |line|
276           unless pattern =~ line.chomp
277             f.puts line
278           else
279             deleted = true
280           end
281         end
282       end
283       File.delete file unless File.size? file
284     end
285     return deleted
286   end