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