c024438f489f8abf1a5b45325c04516e6bb4228a
[ruby_koans.git] / src / edgecase.rb
1 #!/usr/bin/env ruby
2 # -*- ruby -*-
3
4 require 'test/unit/assertions'
5
6 # --------------------------------------------------------------------
7 # Support code for the Ruby Koans.
8 # --------------------------------------------------------------------
9
10 class FillMeInError < StandardError
11 end
12
13 def ruby_version?(version)
14   RUBY_VERSION =~ /^#{version}/ ||
15     (version == 'jruby' && defined?(JRUBY_VERSION)) ||
16     (version == 'mri' && ! defined?(JRUBY_VERSION))
17 end
18
19 def in_ruby_version(*versions)
20   yield if versions.any? { |v| ruby_version?(v) }
21 end
22
23 # Standard, generic replacement value.
24 # If value19 is given, it is used inplace of value for Ruby 1.9.
25 def __(value="FILL ME IN", value19=:mu)
26   if RUBY_VERSION < "1.9"
27     value
28   else
29     (value19 == :mu) ? value : value19
30   end
31 end
32
33 # Numeric replacement value.
34 def _n_(value=999999, value19=:mu)
35   if RUBY_VERSION < "1.9"
36     value
37   else
38     (value19 == :mu) ? value : value19
39   end
40 end
41
42 # Error object replacement value.
43 def ___(value=FillMeInError)
44   value
45 end
46
47 # Method name replacement.
48 class Object
49   def ____(method=nil)
50     if method
51       self.send(method)
52     end
53   end
54
55   in_ruby_version("1.9") do
56     public :method_missing
57   end
58 end
59
60 class String
61   def side_padding(width)
62     extra = width - self.size
63     if width < 0
64       self
65     else
66       left_padding = extra / 2
67       right_padding = (extra+1)/2
68       (" " * left_padding) + self + (" " *right_padding)
69     end
70   end
71 end
72
73 module EdgeCase
74   class << self
75     def simple_output
76       ENV['SIMPLE_KOAN_OUTPUT'] == 'true'
77     end
78   end
79
80   module Color
81     #shamelessly stolen (and modified) from redgreen
82     COLORS = {
83       :clear   => 0,  :black   => 30, :red   => 31,
84       :green   => 32, :yellow  => 33, :blue  => 34,
85       :magenta => 35, :cyan    => 36,
86     }
87
88     module_function
89
90     COLORS.each do |color, value|
91       module_eval "def #{color}(string); colorize(string, #{value}); end"
92       module_function color
93     end
94
95     def colorize(string, color_value)
96       if use_colors?
97         color(color_value) + string + color(COLORS[:clear])
98       else
99         string
100       end
101     end
102
103     def color(color_value)
104       "\e[#{color_value}m"
105     end
106
107     def use_colors?
108       return false if ENV['NO_COLOR']
109       if ENV['ANSI_COLOR'].nil?
110         ! using_windows?
111       else
112         ENV['ANSI_COLOR'] =~ /^(t|y)/i
113       end
114     end
115
116     def using_windows?
117       File::ALT_SEPARATOR
118     end
119   end
120
121   class Sensei
122     attr_reader :failure, :failed_test, :pass_count
123
124     in_ruby_version("1.8") do
125       AssertionError = Test::Unit::AssertionFailedError
126     end
127
128     in_ruby_version("1.9") do
129       if defined?(MiniTest)
130         AssertionError = MiniTest::Assertion
131       else
132         AssertionError = Test::Unit::AssertionFailedError
133       end
134     end
135
136     def initialize
137       @pass_count = 0
138       @failure = nil
139       @failed_test = nil
140       @observations = []
141     end
142
143     PROGRESS_FILE_NAME = '.path_progress'
144
145     def add_progress(prog)
146       @_contents = nil
147       exists = File.exists?(PROGRESS_FILE_NAME)
148       File.open(PROGRESS_FILE_NAME,'a+') do |f|
149         f.print "#{',' if exists}#{prog}"
150       end
151     end
152
153     def progress
154       if @_contents.nil?
155         if File.exists?(PROGRESS_FILE_NAME)
156           File.open(PROGRESS_FILE_NAME,'r') do |f|
157             @_contents = f.read.to_s.gsub(/\s/,'').split(',')
158           end
159         else
160           @_contents = []
161         end
162       end
163       @_contents
164     end
165
166     def observe(step)
167       if step.passed?
168         @pass_count += 1
169         if @pass_count > progress.last.to_i
170           @observations << Color.green("#{step.koan_file}##{step.name} has expanded your awareness.")
171         end
172       else
173         @failed_test = step
174         @failure = step.failure
175         add_progress(@pass_count)
176         @observations << Color.red("#{step.koan_file}##{step.name} has damaged your karma.")
177         throw :edgecase_exit
178       end
179     end
180
181     def failed?
182       ! @failure.nil?
183     end
184
185     def assert_failed?
186       failure.is_a?(AssertionError)
187     end
188
189     def instruct
190       if failed?
191         @observations.each{|c| puts c }
192         encourage
193         guide_through_error
194         a_zenlike_statement
195         show_progress
196       else
197         end_screen
198       end
199     end
200
201     def show_progress
202       bar_width = 50
203       total_tests = EdgeCase::Koan.total_tests
204       scale = bar_width.to_f/total_tests
205       print Color.green("your path thus far [")
206       happy_steps = (pass_count*scale).to_i
207       happy_steps = 1 if happy_steps == 0 && pass_count > 0
208       print Color.green('.'*happy_steps)
209       if failed?
210         print Color.red('X')
211         print Color.cyan('_'*(bar_width-1-happy_steps))
212       end
213       print Color.green(']')
214       print " #{pass_count}/#{total_tests}"
215       puts
216     end
217
218     def end_screen
219       if EdgeCase.simple_output
220         boring_end_screen
221       else
222         artistic_end_screen
223       end
224     end
225
226     def boring_end_screen
227       puts "Mountains are again merely mountains"
228     end
229
230     def artistic_end_screen
231       "JRuby 1.9.x Koans"
232       ruby_version = "(in #{'J' if defined?(JRUBY_VERSION)}Ruby #{defined?(JRUBY_VERSION) ? JRUBY_VERSION : RUBY_VERSION})"
233       ruby_version = ruby_version.side_padding(54)
234         completed = <<-ENDTEXT
235                                   ,,   ,  ,,
236                                 :      ::::,    :::,
237                    ,        ,,: :::::::::::::,,  ::::   :  ,
238                  ,       ,,,   ,:::::::::::::::::::,  ,:  ,: ,,
239             :,        ::,  , , :, ,::::::::::::::::::, :::  ,::::
240            :   :    ::,                          ,:::::::: ::, ,::::
241           ,     ,:::::                                  :,:::::::,::::,
242       ,:     , ,:,,:                                       :::::::::::::
243      ::,:   ,,:::,                                           ,::::::::::::,
244     ,:::, :,,:::                                               ::::::::::::,
245    ,::: :::::::,       Mountains are again merely mountains     ,::::::::::::
246    :::,,,::::::                                                   ::::::::::::
247  ,:::::::::::,                                                    ::::::::::::,
248  :::::::::::,                                                     ,::::::::::::
249 :::::::::::::                                                     ,::::::::::::
250 ::::::::::::                      Ruby Koans                       ::::::::::::,
251 ::::::::::::#{                  ruby_version                     },::::::::::::,
252 :::::::::::,                                                      , ::::::::::::
253 ,:::::::::::::,                brought to you by                 ,,::::::::::::,
254 ::::::::::::::                                                    ,::::::::::::
255  ::::::::::::::,                                                 ,:::::::::::::
256  ::::::::::::,             EdgeCase Software Artisans           , ::::::::::::
257   :,::::::::: ::::                                               :::::::::::::
258    ,:::::::::::  ,:                                          ,,:::::::::::::,
259      ::::::::::::                                           ,::::::::::::::,
260       :::::::::::::::::,                                  ::::::::::::::::
261        :::::::::::::::::::,                             ::::::::::::::::
262         ::::::::::::::::::::::,                     ,::::,:, , ::::,:::
263           :::::::::::::::::::::::,               ::,: ::,::, ,,: ::::
264               ,::::::::::::::::::::              ::,,  , ,,  ,::::
265                  ,::::::::::::::::              ::,, ,   ,:::,
266                       ,::::                         , ,,
267                                                   ,,,
268 ENDTEXT
269         puts completed
270     end
271
272     def encourage
273       puts
274       puts "The Master says:"
275       puts Color.cyan("  You have not yet reached enlightenment.")
276       if ((recents = progress.last(5)) && recents.size == 5 && recents.uniq.size == 1)
277         puts Color.cyan("  I sense frustration. Do not be afraid to ask for help.")
278       elsif progress.last(2).size == 2 && progress.last(2).uniq.size == 1
279         puts Color.cyan("  Do not lose hope.")
280       elsif progress.last.to_i > 0
281         puts Color.cyan("  You are progressing. Excellent. #{progress.last} completed.")
282       end
283     end
284
285     def guide_through_error
286       puts
287       puts "The answers you seek..."
288       puts Color.red(indent(failure.message).join)
289       puts
290       puts "Please meditate on the following code:"
291       if assert_failed?
292         puts embolden_first_line_only(indent(find_interesting_lines(failure.backtrace)))
293       else
294         puts embolden_first_line_only(indent(failure.backtrace))
295       end
296       puts
297     end
298
299     def embolden_first_line_only(text)
300       first_line = true
301       text.collect { |t|
302         if first_line
303           first_line = false
304           Color.red(t)
305         else
306           Color.cyan(t)
307         end
308       }
309     end
310
311     def indent(text)
312       text = text.split(/\n/) if text.is_a?(String)
313       text.collect{|t| "  #{t}"}
314     end
315
316     def find_interesting_lines(backtrace)
317       backtrace.reject { |line|
318         line =~ /test\/unit\/|edgecase\.rb|minitest/
319       }
320     end
321
322     # Hat's tip to Ara T. Howard for the zen statements from his
323     # metakoans Ruby Quiz (http://rubyquiz.com/quiz67.html)
324     def a_zenlike_statement
325       if !failed?
326         zen_statement =  "Mountains are again merely mountains"
327       else
328         zen_statement = case (@pass_count % 10)
329         when 0
330           "mountains are merely mountains"
331         when 1, 2
332           "learn the rules so you know how to break them properly"
333         when 3, 4
334           "remember that silence is sometimes the best answer"
335         when 5, 6
336           "sleep is the best meditation"
337         when 7, 8
338           "when you lose, don't lose the lesson"
339         else
340           "things are not what they appear to be: nor are they otherwise"
341         end
342       end
343       puts Color.green(zen_statement)
344     end
345   end
346
347   class Koan
348     include Test::Unit::Assertions
349
350     attr_reader :name, :failure, :koan_count, :step_count, :koan_file
351
352     def initialize(name, koan_file=nil, koan_count=0, step_count=0)
353       @name = name
354       @failure = nil
355       @koan_count = koan_count
356       @step_count = step_count
357       @koan_file = koan_file
358     end
359
360     def passed?
361       @failure.nil?
362     end
363
364     def failed(failure)
365       @failure = failure
366     end
367
368     def setup
369     end
370
371     def teardown
372     end
373
374     def meditate
375       setup
376       begin
377         send(name)
378       rescue StandardError, EdgeCase::Sensei::AssertionError => ex
379         failed(ex)
380       ensure
381         begin
382           teardown
383         rescue StandardError, EdgeCase::Sensei::AssertionError => ex
384           failed(ex) if passed?
385         end
386       end
387       self
388     end
389
390     # Class methods for the EdgeCase test suite.
391     class << self
392       def inherited(subclass)
393         subclasses << subclass
394       end
395
396       def method_added(name)
397         testmethods << name if !tests_disabled? && Koan.test_pattern =~ name.to_s
398       end
399
400       def end_of_enlightenment
401         @tests_disabled = true
402       end
403
404       def command_line(args)
405         args.each do |arg|
406           case arg
407           when /^-n\/(.*)\/$/
408             @test_pattern = Regexp.new($1)
409           when /^-n(.*)$/
410             @test_pattern = Regexp.new(Regexp.quote($1))
411           else
412             if File.exist?(arg)
413               load(arg)
414             else
415               fail "Unknown command line argument '#{arg}'"
416             end
417           end
418         end
419       end
420
421       # Lazy initialize list of subclasses
422       def subclasses
423         @subclasses ||= []
424       end
425
426        # Lazy initialize list of test methods.
427       def testmethods
428         @test_methods ||= []
429       end
430
431       def tests_disabled?
432         @tests_disabled ||= false
433       end
434
435       def test_pattern
436         @test_pattern ||= /^test_/
437       end
438
439       def total_tests
440         self.subclasses.inject(0){|total, k| total + k.testmethods.size }
441       end
442     end
443   end
444
445   class ThePath
446     def walk
447       sensei = EdgeCase::Sensei.new
448       each_step do |step|
449         sensei.observe(step.meditate)
450       end
451       sensei.instruct
452     end
453
454     def each_step
455       catch(:edgecase_exit) {
456         step_count = 0
457         EdgeCase::Koan.subclasses.each_with_index do |koan,koan_index|
458           koan.testmethods.each do |method_name|
459             step = koan.new(method_name, koan.to_s, koan_index+1, step_count+=1)
460             yield step
461           end
462         end
463       }
464     end
465   end
466 end
467
468 END {
469   EdgeCase::Koan.command_line(ARGV)
470   EdgeCase::ThePath.new.walk
471 }