8d9646b664c1b534f2864d80aba8cd41b147ae5d
[ruby_koans.git] / koans / 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 in place 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         if using_windows?
111           using_win32console
112         end
113       else
114         ENV['ANSI_COLOR'] =~ /^(t|y)/i
115       end
116     end
117
118     def using_windows?
119       File::ALT_SEPARATOR
120     end
121     def using_win32console
122       begin
123         !! Win32::Console::ANSI
124       rescue
125         return false
126       end
127     end
128   end
129
130   class Sensei
131     attr_reader :failure, :failed_test, :pass_count
132
133     in_ruby_version("1.8") do
134       AssertionError = Test::Unit::AssertionFailedError
135     end
136
137     in_ruby_version("1.9") do
138       if defined?(MiniTest)
139         AssertionError = MiniTest::Assertion
140       else
141         AssertionError = Test::Unit::AssertionFailedError
142       end
143     end
144
145     def initialize
146       @pass_count = 0
147       @failure = nil
148       @failed_test = nil
149       @observations = []
150     end
151
152     PROGRESS_FILE_NAME = '.path_progress'
153
154     def add_progress(prog)
155       @_contents = nil
156       exists = File.exists?(PROGRESS_FILE_NAME)
157       File.open(PROGRESS_FILE_NAME,'a+') do |f|
158         f.print "#{',' if exists}#{prog}"
159       end
160     end
161
162     def progress
163       if @_contents.nil?
164         if File.exists?(PROGRESS_FILE_NAME)
165           File.open(PROGRESS_FILE_NAME,'r') do |f|
166             @_contents = f.read.to_s.gsub(/\s/,'').split(',')
167           end
168         else
169           @_contents = []
170         end
171       end
172       @_contents
173     end
174
175     def observe(step)
176       if step.passed?
177         @pass_count += 1
178         if @pass_count > progress.last.to_i
179           @observations << Color.green("#{step.koan_file}##{step.name} has expanded your awareness.")
180         end
181       else
182         @failed_test = step
183         @failure = step.failure
184         add_progress(@pass_count)
185         @observations << Color.red("#{step.koan_file}##{step.name} has damaged your karma.")
186         throw :edgecase_exit
187       end
188     end
189
190     def failed?
191       ! @failure.nil?
192     end
193
194     def assert_failed?
195       failure.is_a?(AssertionError)
196     end
197
198     def instruct
199       if failed?
200         @observations.each{|c| puts c }
201         encourage
202         guide_through_error
203         a_zenlike_statement
204         show_progress
205       else
206         end_screen
207       end
208     end
209
210     def show_progress
211       bar_width = 50
212       total_tests = EdgeCase::Koan.total_tests
213       scale = bar_width.to_f/total_tests
214       print Color.green("your path thus far [")
215       happy_steps = (pass_count*scale).to_i
216       happy_steps = 1 if happy_steps == 0 && pass_count > 0
217       print Color.green('.'*happy_steps)
218       if failed?
219         print Color.red('X')
220         print Color.cyan('_'*(bar_width-1-happy_steps))
221       end
222       print Color.green(']')
223       print " #{pass_count}/#{total_tests}"
224       puts
225     end
226
227     def end_screen
228       if EdgeCase.simple_output
229         boring_end_screen
230       else
231         artistic_end_screen
232       end
233     end
234
235     def boring_end_screen
236       puts "Mountains are again merely mountains"
237     end
238
239     def artistic_end_screen
240       "JRuby 1.9.x Koans"
241       ruby_version = "(in #{'J' if defined?(JRUBY_VERSION)}Ruby #{defined?(JRUBY_VERSION) ? JRUBY_VERSION : RUBY_VERSION})"
242       ruby_version = ruby_version.side_padding(54)
243         completed = <<-ENDTEXT
244                                   ,,   ,  ,,
245                                 :      ::::,    :::,
246                    ,        ,,: :::::::::::::,,  ::::   :  ,
247                  ,       ,,,   ,:::::::::::::::::::,  ,:  ,: ,,
248             :,        ::,  , , :, ,::::::::::::::::::, :::  ,::::
249            :   :    ::,                          ,:::::::: ::, ,::::
250           ,     ,:::::                                  :,:::::::,::::,
251       ,:     , ,:,,:                                       :::::::::::::
252      ::,:   ,,:::,                                           ,::::::::::::,
253     ,:::, :,,:::                                               ::::::::::::,
254    ,::: :::::::,       Mountains are again merely mountains     ,::::::::::::
255    :::,,,::::::                                                   ::::::::::::
256  ,:::::::::::,                                                    ::::::::::::,
257  :::::::::::,                                                     ,::::::::::::
258 :::::::::::::                                                     ,::::::::::::
259 ::::::::::::                      Ruby Koans                       ::::::::::::,
260 ::::::::::::#{                  ruby_version                     },::::::::::::,
261 :::::::::::,                                                      , ::::::::::::
262 ,:::::::::::::,                brought to you by                 ,,::::::::::::,
263 ::::::::::::::                                                    ,::::::::::::
264  ::::::::::::::,                                                 ,:::::::::::::
265  ::::::::::::,             EdgeCase Software Artisans           , ::::::::::::
266   :,::::::::: ::::                                               :::::::::::::
267    ,:::::::::::  ,:                                          ,,:::::::::::::,
268      ::::::::::::                                           ,::::::::::::::,
269       :::::::::::::::::,                                  ::::::::::::::::
270        :::::::::::::::::::,                             ::::::::::::::::
271         ::::::::::::::::::::::,                     ,::::,:, , ::::,:::
272           :::::::::::::::::::::::,               ::,: ::,::, ,,: ::::
273               ,::::::::::::::::::::              ::,,  , ,,  ,::::
274                  ,::::::::::::::::              ::,, ,   ,:::,
275                       ,::::                         , ,,
276                                                   ,,,
277 ENDTEXT
278         puts completed
279     end
280
281     def encourage
282       puts
283       puts "The Master says:"
284       puts Color.cyan("  You have not yet reached enlightenment.")
285       if ((recents = progress.last(5)) && recents.size == 5 && recents.uniq.size == 1)
286         puts Color.cyan("  I sense frustration. Do not be afraid to ask for help.")
287       elsif progress.last(2).size == 2 && progress.last(2).uniq.size == 1
288         puts Color.cyan("  Do not lose hope.")
289       elsif progress.last.to_i > 0
290         puts Color.cyan("  You are progressing. Excellent. #{progress.last} completed.")
291       end
292     end
293
294     def guide_through_error
295       puts
296       puts "The answers you seek..."
297       puts Color.red(indent(failure.message).join)
298       puts
299       puts "Please meditate on the following code:"
300       if assert_failed?
301         puts embolden_first_line_only(indent(find_interesting_lines(failure.backtrace)))
302       else
303         puts embolden_first_line_only(indent(failure.backtrace))
304       end
305       puts
306     end
307
308     def embolden_first_line_only(text)
309       first_line = true
310       text.collect { |t|
311         if first_line
312           first_line = false
313           Color.red(t)
314         else
315           Color.cyan(t)
316         end
317       }
318     end
319
320     def indent(text)
321       text = text.split(/\n/) if text.is_a?(String)
322       text.collect{|t| "  #{t}"}
323     end
324
325     def find_interesting_lines(backtrace)
326       backtrace.reject { |line|
327         line =~ /test\/unit\/|edgecase\.rb|minitest/
328       }
329     end
330
331     # Hat's tip to Ara T. Howard for the zen statements from his
332     # metakoans Ruby Quiz (http://rubyquiz.com/quiz67.html)
333     def a_zenlike_statement
334       if !failed?
335         zen_statement =  "Mountains are again merely mountains"
336       else
337         zen_statement = case (@pass_count % 10)
338         when 0
339           "mountains are merely mountains"
340         when 1, 2
341           "learn the rules so you know how to break them properly"
342         when 3, 4
343           "remember that silence is sometimes the best answer"
344         when 5, 6
345           "sleep is the best meditation"
346         when 7, 8
347           "when you lose, don't lose the lesson"
348         else
349           "things are not what they appear to be: nor are they otherwise"
350         end
351       end
352       puts Color.green(zen_statement)
353     end
354   end
355
356   class Koan
357     include Test::Unit::Assertions
358
359     attr_reader :name, :failure, :koan_count, :step_count, :koan_file
360
361     def initialize(name, koan_file=nil, koan_count=0, step_count=0)
362       @name = name
363       @failure = nil
364       @koan_count = koan_count
365       @step_count = step_count
366       @koan_file = koan_file
367     end
368
369     def passed?
370       @failure.nil?
371     end
372
373     def failed(failure)
374       @failure = failure
375     end
376
377     def setup
378     end
379
380     def teardown
381     end
382
383     def meditate
384       setup
385       begin
386         send(name)
387       rescue StandardError, EdgeCase::Sensei::AssertionError => ex
388         failed(ex)
389       ensure
390         begin
391           teardown
392         rescue StandardError, EdgeCase::Sensei::AssertionError => ex
393           failed(ex) if passed?
394         end
395       end
396       self
397     end
398
399     # Class methods for the EdgeCase test suite.
400     class << self
401       def inherited(subclass)
402         subclasses << subclass
403       end
404
405       def method_added(name)
406         testmethods << name if !tests_disabled? && Koan.test_pattern =~ name.to_s
407       end
408
409       def end_of_enlightenment
410         @tests_disabled = true
411       end
412
413       def command_line(args)
414         args.each do |arg|
415           case arg
416           when /^-n\/(.*)\/$/
417             @test_pattern = Regexp.new($1)
418           when /^-n(.*)$/
419             @test_pattern = Regexp.new(Regexp.quote($1))
420           else
421             if File.exist?(arg)
422               load(arg)
423             else
424               fail "Unknown command line argument '#{arg}'"
425             end
426           end
427         end
428       end
429
430       # Lazy initialize list of subclasses
431       def subclasses
432         @subclasses ||= []
433       end
434
435        # Lazy initialize list of test methods.
436       def testmethods
437         @test_methods ||= []
438       end
439
440       def tests_disabled?
441         @tests_disabled ||= false
442       end
443
444       def test_pattern
445         @test_pattern ||= /^test_/
446       end
447
448       def total_tests
449         self.subclasses.inject(0){|total, k| total + k.testmethods.size }
450       end
451     end
452   end
453
454   class ThePath
455     def walk
456       sensei = EdgeCase::Sensei.new
457       each_step do |step|
458         sensei.observe(step.meditate)
459       end
460       sensei.instruct
461     end
462
463     def each_step
464       catch(:edgecase_exit) {
465         step_count = 0
466         EdgeCase::Koan.subclasses.each_with_index do |koan,koan_index|
467           koan.testmethods.each do |method_name|
468             step = koan.new(method_name, koan.to_s, koan_index+1, step_count+=1)
469             yield step
470           end
471         end
472       }
473     end
474   end
475 end
476
477 END {
478   EdgeCase::Koan.command_line(ARGV)
479   EdgeCase::ThePath.new.walk
480 }