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