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