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