1136218b51c801741b73bfca0b0cb67478faba08
[ruby_koans.git] / src / 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, value19=:mu)
48   if RUBY_VERSION < "1.9"
49     value
50   else
51     (value19 == :mu) ? value : value19
52   end
53 end
54
55 # Method name replacement.
56 class Object
57   def ____(method=nil)
58     if method
59       self.send(method)
60     end
61   end
62
63   in_ruby_version("1.9") do
64     public :method_missing
65   end
66 end
67
68 class String
69   def side_padding(width)
70     extra = width - self.size
71     if width < 0
72       self
73     else
74       left_padding = extra / 2
75       right_padding = (extra+1)/2
76       (" " * left_padding) + self + (" " *right_padding)
77     end
78   end
79 end
80
81 module EdgeCase
82   class << self
83     def simple_output
84       ENV['SIMPLE_KOAN_OUTPUT'] == 'true'
85     end
86   end
87
88   module Color
89     #shamelessly stolen (and modified) from redgreen
90     COLORS = {
91       :clear   => 0,  :black   => 30, :red   => 31,
92       :green   => 32, :yellow  => 33, :blue  => 34,
93       :magenta => 35, :cyan    => 36,
94     }
95
96     module_function
97
98     COLORS.each do |color, value|
99       module_eval "def #{color}(string); colorize(string, #{value}); end"
100       module_function color
101     end
102
103     def colorize(string, color_value)
104       if use_colors?
105         color(color_value) + string + color(COLORS[:clear])
106       else
107         string
108       end
109     end
110
111     def color(color_value)
112       "\e[#{color_value}m"
113     end
114
115     def use_colors?
116       return false if ENV['NO_COLOR']
117       if ENV['ANSI_COLOR'].nil?
118         if using_windows?
119           using_win32console
120         end
121       else
122         ENV['ANSI_COLOR'] =~ /^(t|y)/i
123       end
124     end
125
126     def using_windows?
127       File::ALT_SEPARATOR
128     end
129
130     def using_win32console
131       defined? Win32::Console
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 }