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