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