Typos.
[ruby_koans.git] / src / edgecase.rb
index d0aa616..ba49956 100644 (file)
@@ -3,13 +3,25 @@
 
 require 'test/unit/assertions'
 
+# --------------------------------------------------------------------
+# Support code for the Ruby Koans.
+# --------------------------------------------------------------------
+
 class FillMeInError < StandardError
 end
 
-def in_ruby_version(version)
-  yield if RUBY_VERSION =~ /^#{version}/
+def ruby_version?(version)
+  RUBY_VERSION =~ /^#{version}/ ||
+    (version == 'jruby' && defined?(JRUBY_VERSION)) ||
+    (version == 'mri' && ! defined?(JRUBY_VERSION))
+end
+
+def in_ruby_version(*versions)
+  yield if versions.any? { |v| ruby_version?(v) }
 end
 
+# Standard, generic replacement value.
+# If value19 is given, it is used in place of value for Ruby 1.9.
 def __(value="FILL ME IN", value19=:mu)
   if RUBY_VERSION < "1.9"
     value
@@ -18,6 +30,7 @@ def __(value="FILL ME IN", value19=:mu)
   end
 end
 
+# Numeric replacement value.
 def _n_(value=999999, value19=:mu)
   if RUBY_VERSION < "1.9"
     value
@@ -26,10 +39,12 @@ def _n_(value=999999, value19=:mu)
   end
 end
 
+# Error object replacement value.
 def ___(value=FillMeInError)
   value
 end
 
+# Method name replacement.
 class Object
   def ____(method=nil)
     if method
@@ -42,26 +57,123 @@ class Object
   end
 end
 
+class String
+  def side_padding(width)
+    extra = width - self.size
+    if width < 0
+      self
+    else
+      left_padding = extra / 2
+      right_padding = (extra+1)/2
+      (" " * left_padding) + self + (" " *right_padding)
+    end
+  end
+end
+
 module EdgeCase
+  class << self
+    def simple_output
+      ENV['SIMPLE_KOAN_OUTPUT'] == 'true'
+    end
+  end
+
+  module Color
+    #shamelessly stolen (and modified) from redgreen
+    COLORS = {
+      :clear   => 0,  :black   => 30, :red   => 31,
+      :green   => 32, :yellow  => 33, :blue  => 34,
+      :magenta => 35, :cyan    => 36,
+    }
+
+    module_function
+
+    COLORS.each do |color, value|
+      module_eval "def #{color}(string); colorize(string, #{value}); end"
+      module_function color
+    end
+
+    def colorize(string, color_value)
+      if use_colors?
+        color(color_value) + string + color(COLORS[:clear])
+      else
+        string
+      end
+    end
+
+    def color(color_value)
+      "\e[#{color_value}m"
+    end
+
+    def use_colors?
+      return false if ENV['NO_COLOR']
+      if ENV['ANSI_COLOR'].nil?
+        ! using_windows?
+      else
+        ENV['ANSI_COLOR'] =~ /^(t|y)/i
+      end
+    end
+
+    def using_windows?
+      File::ALT_SEPARATOR
+    end
+  end
+
   class Sensei
-    attr_reader :failure, :failed_test
+    attr_reader :failure, :failed_test, :pass_count
+
+    in_ruby_version("1.8") do
+      AssertionError = Test::Unit::AssertionFailedError
+    end
 
-    AssertionError = Test::Unit::AssertionFailedError
+    in_ruby_version("1.9") do
+      if defined?(MiniTest)
+        AssertionError = MiniTest::Assertion
+      else
+        AssertionError = Test::Unit::AssertionFailedError
+      end
+    end
 
     def initialize
       @pass_count = 0
       @failure = nil
       @failed_test = nil
+      @observations = []
     end
 
-    def accumulate(test)
-      if test.passed?
+    PROGRESS_FILE_NAME = '.path_progress'
+
+    def add_progress(prog)
+      @_contents = nil
+      exists = File.exists?(PROGRESS_FILE_NAME)
+      File.open(PROGRESS_FILE_NAME,'a+') do |f|
+        f.print "#{',' if exists}#{prog}"
+      end
+    end
+
+    def progress
+      if @_contents.nil?
+        if File.exists?(PROGRESS_FILE_NAME)
+          File.open(PROGRESS_FILE_NAME,'r') do |f|
+            @_contents = f.read.to_s.gsub(/\s/,'').split(',')
+          end
+        else
+          @_contents = []
+        end
+      end
+      @_contents
+    end
+
+    def observe(step)
+      if step.passed?
         @pass_count += 1
-        puts "  #{test.name} has expanded your awareness."
+        if @pass_count > progress.last.to_i
+          @observations << Color.green("#{step.koan_file}##{step.name} has expanded your awareness.")
+        end
       else
-        puts "  #{test.name} has damaged your karma."
-        @failed_test = test
-        @failure = test.failure
+        @failed_test = step
+        @failure = step.failure
+        add_progress(@pass_count)
+        @observations << Color.red("#{step.koan_file}##{step.name} has damaged your karma.")
         throw :edgecase_exit
       end
     end
@@ -74,62 +186,175 @@ module EdgeCase
       failure.is_a?(AssertionError)
     end
 
-    def report
+    def instruct
+      if failed?
+        @observations.each{|c| puts c }
+        encourage
+        guide_through_error
+        a_zenlike_statement
+        show_progress
+      else
+        end_screen
+      end
+    end
+
+    def show_progress
+      bar_width = 50
+      total_tests = EdgeCase::Koan.total_tests
+      scale = bar_width.to_f/total_tests
+      print Color.green("your path thus far [")
+      happy_steps = (pass_count*scale).to_i
+      happy_steps = 1 if happy_steps == 0 && pass_count > 0
+      print Color.green('.'*happy_steps)
       if failed?
-        puts
-        puts "You have not yet reached enlightenment ..."
-        puts failure.message
-        puts
-        puts "Please meditate on the following code:"
-        if assert_failed?
-          puts find_interesting_lines(failure.backtrace)
+        print Color.red('X')
+        print Color.cyan('_'*(bar_width-1-happy_steps))
+      end
+      print Color.green(']')
+      print " #{pass_count}/#{total_tests}"
+      puts
+    end
+
+    def end_screen
+      if EdgeCase.simple_output
+        boring_end_screen
+      else
+        artistic_end_screen
+      end
+    end
+
+    def boring_end_screen
+      puts "Mountains are again merely mountains"
+    end
+
+    def artistic_end_screen
+      "JRuby 1.9.x Koans"
+      ruby_version = "(in #{'J' if defined?(JRUBY_VERSION)}Ruby #{defined?(JRUBY_VERSION) ? JRUBY_VERSION : RUBY_VERSION})"
+      ruby_version = ruby_version.side_padding(54)
+        completed = <<-ENDTEXT
+                                  ,,   ,  ,,
+                                :      ::::,    :::,
+                   ,        ,,: :::::::::::::,,  ::::   :  ,
+                 ,       ,,,   ,:::::::::::::::::::,  ,:  ,: ,,
+            :,        ::,  , , :, ,::::::::::::::::::, :::  ,::::
+           :   :    ::,                          ,:::::::: ::, ,::::
+          ,     ,:::::                                  :,:::::::,::::,
+      ,:     , ,:,,:                                       :::::::::::::
+     ::,:   ,,:::,                                           ,::::::::::::,
+    ,:::, :,,:::                                               ::::::::::::,
+   ,::: :::::::,       Mountains are again merely mountains     ,::::::::::::
+   :::,,,::::::                                                   ::::::::::::
+ ,:::::::::::,                                                    ::::::::::::,
+ :::::::::::,                                                     ,::::::::::::
+:::::::::::::                                                     ,::::::::::::
+::::::::::::                      Ruby Koans                       ::::::::::::,
+::::::::::::#{                  ruby_version                     },::::::::::::,
+:::::::::::,                                                      , ::::::::::::
+,:::::::::::::,                brought to you by                 ,,::::::::::::,
+::::::::::::::                                                    ,::::::::::::
+ ::::::::::::::,                                                 ,:::::::::::::
+ ::::::::::::,             EdgeCase Software Artisans           , ::::::::::::
+  :,::::::::: ::::                                               :::::::::::::
+   ,:::::::::::  ,:                                          ,,:::::::::::::,
+     ::::::::::::                                           ,::::::::::::::,
+      :::::::::::::::::,                                  ::::::::::::::::
+       :::::::::::::::::::,                             ::::::::::::::::
+        ::::::::::::::::::::::,                     ,::::,:, , ::::,:::
+          :::::::::::::::::::::::,               ::,: ::,::, ,,: ::::
+              ,::::::::::::::::::::              ::,,  , ,,  ,::::
+                 ,::::::::::::::::              ::,, ,   ,:::,
+                      ,::::                         , ,,
+                                                  ,,,
+ENDTEXT
+        puts completed
+    end
+
+    def encourage
+      puts
+      puts "The Master says:"
+      puts Color.cyan("  You have not yet reached enlightenment.")
+      if ((recents = progress.last(5)) && recents.size == 5 && recents.uniq.size == 1)
+        puts Color.cyan("  I sense frustration. Do not be afraid to ask for help.")
+      elsif progress.last(2).size == 2 && progress.last(2).uniq.size == 1
+        puts Color.cyan("  Do not lose hope.")
+      elsif progress.last.to_i > 0
+        puts Color.cyan("  You are progressing. Excellent. #{progress.last} completed.")
+      end
+    end
+
+    def guide_through_error
+      puts
+      puts "The answers you seek..."
+      puts Color.red(indent(failure.message).join)
+      puts
+      puts "Please meditate on the following code:"
+      if assert_failed?
+        puts embolden_first_line_only(indent(find_interesting_lines(failure.backtrace)))
+      else
+        puts embolden_first_line_only(indent(failure.backtrace))
+      end
+      puts
+    end
+
+    def embolden_first_line_only(text)
+      first_line = true
+      text.collect { |t|
+        if first_line
+          first_line = false
+          Color.red(t)
         else
-          puts failure.backtrace
+          Color.cyan(t)
         end
-        puts
-      end
-      say_something_zenlike
+      }
+    end
+
+    def indent(text)
+      text = text.split(/\n/) if text.is_a?(String)
+      text.collect{|t| "  #{t}"}
     end
 
     def find_interesting_lines(backtrace)
       backtrace.reject { |line|
-        line =~ /test\/unit\/|edgecase\.rb/
+        line =~ /test\/unit\/|edgecase\.rb|minitest/
       }
     end
 
     # Hat's tip to Ara T. Howard for the zen statements from his
     # metakoans Ruby Quiz (http://rubyquiz.com/quiz67.html)
-    def say_something_zenlike
-      puts
+    def a_zenlike_statement
       if !failed?
-        puts "Mountains are again merely mountains"
+        zen_statement =  "Mountains are again merely mountains"
       else
-        case (@pass_count % 10)
+        zen_statement = case (@pass_count % 10)
         when 0
-          puts "mountains are merely mountains"
+          "mountains are merely mountains"
         when 1, 2
-          puts "learn the rules so you know how to break them properly"
+          "learn the rules so you know how to break them properly"
         when 3, 4
-          puts "remember that silence is sometimes the best answer"
+          "remember that silence is sometimes the best answer"
         when 5, 6
-          puts "sleep is the best meditation"
+          "sleep is the best meditation"
         when 7, 8
-          puts "when you lose, don't lose the lesson"
+          "when you lose, don't lose the lesson"
         else
-          puts "things are not what they appear to be: nor are they otherwise"
+          "things are not what they appear to be: nor are they otherwise"
         end
       end
+      puts Color.green(zen_statement)
     end
   end
 
   class Koan
     include Test::Unit::Assertions
 
-    attr_reader :name, :failure
+    attr_reader :name, :failure, :koan_count, :step_count, :koan_file
 
-    def initialize(name)
+    def initialize(name, koan_file=nil, koan_count=0, step_count=0)
       @name = name
       @failure = nil
+      @koan_count = koan_count
+      @step_count = step_count
+      @koan_file = koan_file
     end
 
     def passed?
@@ -146,6 +371,22 @@ module EdgeCase
     def teardown
     end
 
+    def meditate
+      setup
+      begin
+        send(name)
+      rescue StandardError, EdgeCase::Sensei::AssertionError => ex
+        failed(ex)
+      ensure
+        begin
+          teardown
+        rescue StandardError, EdgeCase::Sensei::AssertionError => ex
+          failed(ex) if passed?
+        end
+      end
+      self
+    end
+
     # Class methods for the EdgeCase test suite.
     class << self
       def inherited(subclass)
@@ -153,32 +394,7 @@ module EdgeCase
       end
 
       def method_added(name)
-        testmethods << name unless tests_disabled?
-      end
-
-      def run_tests(accumulator)
-        puts
-        puts "Thinking #{self}"
-        testmethods.each do |m|
-          self.run_test(m, accumulator) if Koan.test_pattern =~ m.to_s
-        end
-      end
-
-      def run_test(method, accumulator)
-        test = self.new(method)
-        test.setup
-        begin
-          test.send(method)
-        rescue StandardError => ex
-          test.failed(ex)
-        ensure
-          begin
-            test.teardown
-          rescue StandardError => ex
-            test.failed(ex) if test.passed?
-          end
-        end
-        accumulator.accumulate(test)
+        testmethods << name if !tests_disabled? && Koan.test_pattern =~ name.to_s
       end
 
       def end_of_enlightenment
@@ -220,17 +436,36 @@ module EdgeCase
         @test_pattern ||= /^test_/
       end
 
+      def total_tests
+        self.subclasses.inject(0){|total, k| total + k.testmethods.size }
+      end
+    end
+  end
+
+  class ThePath
+    def walk
+      sensei = EdgeCase::Sensei.new
+      each_step do |step|
+        sensei.observe(step.meditate)
+      end
+      sensei.instruct
+    end
+
+    def each_step
+      catch(:edgecase_exit) {
+        step_count = 0
+        EdgeCase::Koan.subclasses.each_with_index do |koan,koan_index|
+          koan.testmethods.each do |method_name|
+            step = koan.new(method_name, koan.to_s, koan_index+1, step_count+=1)
+            yield step
+          end
+        end
+      }
     end
   end
 end
 
 END {
   EdgeCase::Koan.command_line(ARGV)
-  zen_master = EdgeCase::Sensei.new
-  catch(:edgecase_exit) {
-    EdgeCase::Koan.subclasses.each do |sc|
-      sc.run_tests(zen_master)
-    end
-  }
-  zen_master.report
+  EdgeCase::ThePath.new.walk
 }