#require 'pathname' #p Pathname(caller.last.split(':').first) require 'ostruct' unless defined?(TRYOUTS_LIB_HOME) TRYOUTS_LIB_HOME = File.expand_path File.dirname(__FILE__) end class Tryouts module VERSION def self.to_s load_config [@version[:MAJOR], @version[:MINOR], @version[:PATCH]].join('.') end def self.inspect load_config [@version[:MAJOR], @version[:MINOR], @version[:PATCH], @version[:BUILD]].join('.') end def self.load_config require 'yaml' @version ||= YAML.load_file(File.join(TRYOUTS_LIB_HOME, '..', 'VERSION.yml')) end end end class Tryouts @debug = false @quiet = false @container = Class.new @cases = [] @sysinfo = nil class << self attr_accessor :debug, :container, :quiet attr_reader :cases def sysinfo require 'sysinfo' @sysinfo ||= SysInfo.new @sysinfo end def debug?() @debug == true end def run_all *paths batches = paths.collect do |path| parse path end all, skipped_tests, failed_tests = 0, 0, 0 skipped_batches, failed_batches = 0, 0 msg 'Ruby %s @ %-40s' % [RUBY_VERSION, Time.now], $/ batches.each do |batch| path = batch.path.gsub(/#{Dir.pwd}\/?/, '') msg '%-60s %s' % [path, ''] unless Tryouts.quiet # status before_handler = Proc.new do |t| msg Console.reverse(' %-58s ' % [t.desc.to_s]) unless Tryouts.quiet msg t.test.inspect, t.exps.inspect unless Tryouts.quiet end batch.run(before_handler) do |t| if t.failed? failed_tests += 1 msg Console.color(:red, t.failed.join($/)), $/ unless Tryouts.quiet elsif t.skipped? || !t.run? skipped_tests += 1 msg Console.bright(t.skipped.join($/)), $/ unless Tryouts.quiet else msg Console.color(:green, t.passed.join($/)), $/ unless Tryouts.quiet end all += 1 end end msg unless Tryouts.quiet if all > 0 suffix = 'tests passed' suffix << " (and #{skipped_tests} skipped)" if skipped_tests > 0 msg cformat(all-failed_tests-skipped_tests, all-skipped_tests, suffix) if all-skipped_tests > 0 end if batches.size > 1 if batches.size-skipped_batches > 0 suffix = "batches passed" suffix << " (and #{skipped_batches} skipped)" if skipped_batches > 0 msg cformat(batches.size-skipped_batches-failed_batches, batches.size-skipped_batches, suffix) end end failed_tests # 0 means success end def cformat(*args) Console.bright '%3d of %d %s' % args end def run path batch = parse path batch.run batch end def parse path #debug "Loading #{path}" lines = File.readlines path skip_ahead = 0 batch = TestBatch.new path, lines lines.size.times do |idx| skip_ahead -= 1 and next if skip_ahead > 0 line = lines[idx].chomp #debug('%-4d %s' % [idx, line]) if expectation? line offset = 0 exps = Section.new(path, idx+1) exps << line.chomp while (idx+offset < lines.size) offset += 1 this_line = lines[idx+offset] break if ignore?(this_line) if expectation?(this_line) exps << this_line.chomp skip_ahead += 1 end exps.last += 1 end offset = 0 buffer, desc = Section.new(path), Section.new(path) test = Section.new(path, idx) # test start the line before the exp. blank_buffer = Section.new(path) while (idx-offset >= 0) offset += 1 this_line = lines[idx-offset].chomp buffer.unshift this_line if ignore?(this_line) if comment?(this_line) buffer.unshift this_line end if test?(this_line) test.unshift(*buffer) && buffer.clear test.unshift this_line end if test_begin?(this_line) while test_begin?(lines[idx-(offset+1)].chomp) offset += 1 buffer.unshift lines[idx-offset].chomp end end if test_begin?(this_line) || idx-offset == 0 || expectation?(this_line) adjust = expectation?(this_line) ? 2 : 1 test.first = idx-offset+buffer.size+adjust desc.unshift *buffer desc.last = test.first-1 desc.first = desc.last-desc.size+1 # remove empty lines between the description # and the previous expectation while !desc.empty? && desc[0].empty? desc.shift desc.first += 1 end break end end batch << TestCase.new(desc, test, exps) end end batch end def print str STDOUT.print str STDOUT.flush end def msg *msg STDOUT.puts *msg end def err *msg msg.each do |line| STDERR.puts Console.color :red, line end end def debug *msg STDERR.puts *msg if @debug end def eval(str, path, line) begin Kernel.eval str, @container.send(:binding), path, line rescue SyntaxError, LoadError => ex Tryouts.err Console.color(:red, ex.message), Console.color(:red, ex.backtrace.first) nil end end private def expectation? str !ignore?(str) && str.strip.match(/\A\#+\s*=>/) end def comment? str !str.strip.match(/^\#+/).nil? && !expectation?(str) end def test? str !ignore?(str) && !expectation?(str) && !comment?(str) end def ignore? str str.to_s.strip.chomp.empty? end def test_begin? str ret = !str.strip.match(/\#+\s*TEST/i).nil? || !str.strip.match(/\A\#\#+[\s\w]+/i).nil? ret end end class TestBatch < Array class Container def metaclass class << self; end end end attr_reader :path attr_reader :failed attr_reader :lines def initialize(p,l) @path, @lines = p, l @container = Container.new.metaclass @run = false end def run(before_test, &after_test) return if empty? setup ret = self.select { |tc| before_test.call(tc) unless before_test.nil? ret = !tc.run after_test.call(tc) ret # select failed tests } @failed = ret.size @run = true clean !failed? end def failed? !@failed.nil? && @failed > 0 end def setup return if empty? start = first.desc.nil? ? first.test.first : first.desc.first-1 Tryouts.eval lines[0..start-1].join, path, 0 if start > 0 end def clean return if empty? last_line = last.exps.last+1 if last_line < lines.size Tryouts.eval lines[last_line..-1].join, path, last_line end end def run? @run end end class TestCase attr_reader :desc, :test, :exps, :failed, :passed, :skipped def initialize(d,t,e) @desc, @test, @exps, @path = d,t,e end def inspect [@desc.inspect, @test.inspect, @exps.inspect].join end def to_s [@desc.to_s, @test.to_s, @exps.to_s].join end def run Tryouts.debug '%s:%d' % [@test.path, @test.first] Tryouts.debug inspect, $/ expectations = exps.collect { |exp,idx| exp =~ /\A\#?\s*=>\s*(.+)\Z/ $1 # this will be nil if the expectation is commented out } # Evaluate test block only if there are valid expectations unless expectations.compact.empty? test_value = Tryouts.eval @test.to_s, @test.path, @test.first @has_run = true end @passed, @failed, @skipped = [], [], [] expectations.each_with_index { |exp,idx| if exp.nil? @skipped << ' [skipped]' else exp_value = Tryouts.eval(exp, @exps.path, @exps.first+idx) if test_value == exp_value @passed << ' == %s' % [test_value.inspect] else @failed << ' != %s' % [test_value.inspect] end end } Tryouts.debug @failed.empty? end def skipped? !@skipped.nil? && !@skipped.empty? end def run? @has_run == true end def failed? !@failed.nil? && !@failed.empty? end private def create_proc str, path, line eval("Proc.new {\n #{str}\n}", binding, path, line) end end class Section < Array attr_accessor :path, :first, :last def initialize path, start=0 @path = path @first, @last = start, start end def range @first..@last end def inspect range.to_a.zip(self).collect do |line| "%-4d %s\n" % line end.join end def to_s self.join($/) end end module Console # ANSI escape sequence numbers for text attributes ATTRIBUTES = { :normal => 0, :bright => 1, :dim => 2, :underline => 4, :blink => 5, :reverse => 7, :hidden => 8, :default => 0, }.freeze unless defined? ATTRIBUTES # ANSI escape sequence numbers for text colours COLOURS = { :black => 30, :red => 31, :green => 32, :yellow => 33, :blue => 34, :magenta => 35, :cyan => 36, :white => 37, :default => 39, :random => 30 + rand(10).to_i }.freeze unless defined? COLOURS # ANSI escape sequence numbers for background colours BGCOLOURS = { :black => 40, :red => 41, :green => 42, :yellow => 43, :blue => 44, :magenta => 45, :cyan => 46, :white => 47, :default => 49, :random => 40 + rand(10).to_i }.freeze unless defined? BGCOLOURS module InstanceMethods def bright Console.bright(self) end def reverse Console.reverse(self) end def color(col) Console.color(col, self) end def att(col) Console.att(col, self) end def bgcolor(col) Console.bgcolor(col, self) end end def self.bright(str) str = [style(ATTRIBUTES[:bright]), str, default_style].join str.extend Console::InstanceMethods str end def self.reverse(str) str = [style(ATTRIBUTES[:reverse]), str, default_style].join str.extend Console::InstanceMethods str end def self.color(col, str) str = [style(COLOURS[col]), str, default_style].join str.extend Console::InstanceMethods str end def self.att(name, str) str = [style(ATTRIBUTES[name]), str, default_style].join str.extend Console::InstanceMethods str end def self.bgcolor(col, str) str = [style(ATTRIBUTES[col]), str, default_style].join str.extend Console::InstanceMethods str end private def self.style(*att) # => \e[8;34;42m "\e[%sm" % att.join(';') end def self.default_style style(ATTRIBUTES[:default], ATTRIBUTES[:COLOURS], ATTRIBUTES[:BGCOLOURS]) end end end