#!/usr/bin/env ruby # -*- ruby -*- require 'test/unit/assertions' # -------------------------------------------------------------------- # Support code for the Ruby Koans. # -------------------------------------------------------------------- class FillMeInError < StandardError end 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 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? ! 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, :pass_count 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 observe(step) if step.passed? @pass_count += 1 if @pass_count > progress.last.to_i @observations << Color.green("#{step.koan_file}##{step.name} has expanded your awareness.") end else @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 def failed? ! @failure.nil? end def assert_failed? failure.is_a?(AssertionError) end 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? 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 Color.cyan(t) end } 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|minitest/ } end # Hat's tip to Ara T. Howard for the zen statements from his # metakoans Ruby Quiz (http://rubyquiz.com/quiz67.html) def a_zenlike_statement if !failed? zen_statement = "Mountains are again merely mountains" else zen_statement = case (@pass_count % 10) when 0 "mountains are merely mountains" when 1, 2 "learn the rules so you know how to break them properly" when 3, 4 "remember that silence is sometimes the best answer" when 5, 6 "sleep is the best meditation" when 7, 8 "when you lose, don't lose the lesson" else "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, :koan_count, :step_count, :koan_file 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? @failure.nil? end def failed(failure) @failure = failure end def setup end 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) subclasses << subclass end def method_added(name) testmethods << name if !tests_disabled? && Koan.test_pattern =~ name.to_s end def end_of_enlightenment @tests_disabled = true end def command_line(args) args.each do |arg| case arg when /^-n\/(.*)\/$/ @test_pattern = Regexp.new($1) when /^-n(.*)$/ @test_pattern = Regexp.new(Regexp.quote($1)) else if File.exist?(arg) load(arg) else fail "Unknown command line argument '#{arg}'" end end end end # Lazy initialize list of subclasses def subclasses @subclasses ||= [] end # Lazy initialize list of test methods. def testmethods @test_methods ||= [] end def tests_disabled? @tests_disabled ||= false end def test_pattern @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) EdgeCase::ThePath.new.walk }