require 'test/unit/assertions'
+# --------------------------------------------------------------------
+# Support code for the Ruby Koans.
+# --------------------------------------------------------------------
+
class FillMeInError < StandardError
end
-def __(value="FILL ME IN")
- value
+def ruby_version?(version)
+ RUBY_VERSION =~ /^#{version}/ ||
+ (version == 'jruby' && defined?(JRUBY_VERSION)) ||
+ (version == 'mri' && ! defined?(JRUBY_VERSION))
end
-def _n_(value=999999)
- value
+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
+ else
+ (value19 == :mu) ? value : value19
+ end
+end
+
+# Numeric replacement value.
+def _n_(value=999999, value19=:mu)
+ if RUBY_VERSION < "1.9"
+ value
+ else
+ (value19 == :mu) ? value : value19
+ end
end
+# Error object replacement value.
def ___(value=FillMeInError)
value
end
+# Method name replacement.
class Object
def ____(method=nil)
if method
self.send(method)
end
end
+
+ in_ruby_version("1.9") do
+ public :method_missing
+ 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?
+ if using_windows?
+ using_win32console
+ end
+ else
+ ENV['ANSI_COLOR'] =~ /^(t|y)/i
+ end
+ end
+
+ def using_windows?
+ File::ALT_SEPARATOR
+ end
+ def using_win32console
+ begin
+ !! Win32::Console::ANSI
+ rescue
+ return false
+ end
+ end
+ end
+
class Sensei
- attr_reader :failure, :failed_test
+ attr_reader :failure, :failed_test, :pass_count
- AssertionError = Test::Unit::AssertionFailedError
+ in_ruby_version("1.8") do
+ AssertionError = Test::Unit::AssertionFailedError
+ end
+
+ 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
+
+ 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 accumulate(test)
- if test.passed?
+ 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
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
+ 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?
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)
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
load(arg)
else
fail "Unknown command line argument '#{arg}'"
- end
+ end
end
end
end
@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
}