forgot requires
[ruby_koans.git] / koans / edgecase.rb
1 #!/usr/bin/env ruby
2 # -*- ruby -*-
3
4 require 'test/unit/assertions'
5 require 'rubygems'
6 require 'win32console'
7 # --------------------------------------------------------------------
8 # Support code for the Ruby Koans.
9 # --------------------------------------------------------------------
10
11 class FillMeInError < StandardError
12 end
13
14 def ruby_version?(version)
15   RUBY_VERSION =~ /^#{version}/ ||
16     (version == 'jruby' && defined?(JRUBY_VERSION)) ||
17     (version == 'mri' && ! defined?(JRUBY_VERSION))
18 end
19
20 def in_ruby_version(*versions)
21   yield if versions.any? { |v| ruby_version?(v) }
22 end
23
24 # Standard, generic replacement value.
25 # If value19 is given, it is used in place of value for Ruby 1.9.
26 def __(value="FILL ME IN", value19=:mu)
27   if RUBY_VERSION < "1.9"
28     value
29   else
30     (value19 == :mu) ? value : value19
31   end
32 end
33
34 # Numeric replacement value.
35 def _n_(value=999999, value19=:mu)
36   if RUBY_VERSION < "1.9"
37     value
38   else
39     (value19 == :mu) ? value : value19
40   end
41 end
42
43 # Error object replacement value.
44 def ___(value=FillMeInError)
45   value
46 end
47
48 # Method name replacement.
49 class Object
50   def ____(method=nil)
51     if method
52       self.send(method)
53     end
54   end
55
56   in_ruby_version("1.9") do
57     public :method_missing
58   end
59 end
60
61 class String
62   def side_padding(width)
63     extra = width - self.size
64     if width < 0
65       self
66     else
67       left_padding = extra / 2
68       right_padding = (extra+1)/2
69       (" " * left_padding) + self + (" " *right_padding)
70     end
71   end
72 end
73
74 module EdgeCase
75   class << self
76     def simple_output
77       ENV['SIMPLE_KOAN_OUTPUT'] == 'true'
78     end
79   end
80
81   module Color
82     #shamelessly stolen (and modified) from redgreen
83     COLORS = {
84       :clear   => 0,  :black   => 30, :red   => 31,
85       :green   => 32, :yellow  => 33, :blue  => 34,
86       :magenta => 35, :cyan    => 36,
87     }
88
89     module_function
90
91     COLORS.each do |color, value|
92       module_eval "def #{color}(string); colorize(string, #{value}); end"
93       module_function color
94     end
95
96     def colorize(string, color_value)
97       if use_colors?
98         color(color_value) + string + color(COLORS[:clear])
99       else
100         string
101       end
102     end
103
104     def color(color_value)
105       "\e[#{color_value}m"
106     end
107
108     def use_colors?
109       return false if ENV['NO_COLOR']
110       if ENV['ANSI_COLOR'].nil?
111         if using_windows?
112           using_win32console
113         end
114       else
115         ENV['ANSI_COLOR'] =~ /^(t|y)/i
116       end
117     end
118
119     def using_windows?
120       File::ALT_SEPARATOR
121     end
122     def using_win32console
123       begin
124         !! Win32::Console::ANSI
125       rescue
126         return false
127       end
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 }