$TAPTAP_VERBOSE = 0 module TapTap VERSION = "0.2" class Processor def parse(io) current = 0 start while line = io.gets case line when /^1\.\.(\d+)/ plan $1.to_i when /^not ok\b(?:\s*(\d+))?(?:\s*[#-]?\s*(.*))?/ current += 1 test_failed(($1 || current).to_i, $2) when /^ok\b(?:\s*(\d+))?(?:\s*[#-]?\s*(.*))?/ current += 1 test_passed(current, $2) when /^Bail out!(.*)/ bail_out $1.strip when /^#.*/ comment line else ignore line end end done(current) end def start; end def plan(n); end def test_failed(n, detail); end def test_passed(n, detail); end def bail_out(reason); end def comment(line); end def ignore(line); end def done(n); end attr_reader :current attr_reader :failed def self.summarize(tests) all = tests.inject(0) { |a,e| a + e.current } failed = tests.inject(0) { |a,e| a + e.failed.size } if failed == 0 "All #{all} tests SUCCEEDED." else "Failed %d/%d tests, %02.2f%% okay.\n" % [failed, all, (all-failed.size.to_f)/all*100] end end end class Runner < Processor def initialize(output, name="*stdin*") @planned = nil @failed = {} @ran = 0 @bailed = nil @name = name @output = output end attr_reader :ran attr_reader :failed attr_reader :name def puts(str) @output.puts str end def quiet? $TAPTAP_VERBOSE < 0 end def plan(n) @planned = n end def start @began = Time.now end def test_failed(n, detail) puts "%s... %4d/%s FAILED: %s" % [@name.ljust(13, "."), n, @planned||"???", detail] unless quiet? @failed[n] = detail || true end def test_passed(n, detail) print "%s... %4d/%s PASS: %-45s\r" % [@name.ljust(13, "."), n, @planned||"???", detail[0..44]] unless quiet? end def bail_out(reason) puts "BAIL OUT: #{reason}" unless quiet? @bailed = reason end def comment(line) puts line.chomp if $TAPTAP_VERBOSE > 0 end def ignore(line) if $DEBUG warn "No idea what to do with `#{line.chomp}'" end end def done(n) @ran = n @finished = Time.now puts "" unless quiet? end def run_time @finished - @began end def summarize puts "\nTests took %.2f seconds." % run_time if @failed.empty? puts "All #{@ran} tests SUCCEEDED." else puts "FAILED tests #{@failed.keys.join(", ")}" puts @failed.sort.map { |k, v| "%4d) %s" % [k, v] }.join("\n") puts "" puts "Failed %d/%d tests, %02.2f%% okay." % [@failed.size, @ran, (@ran-@failed.size.to_f)/@ran*100] end if @planned && @planned != @ran puts "WEIRD: Planned #{@planned} tests, but ran #{@ran}." end if @bailed puts "BAILED OUT: #{@bailed}" end end end class Harness def initialize(output, runner) @output, @runner = output, runner end def run(tests) @tests = tests.map { |test| t = @runner.new(@output, test) begin redir = " 2>&1" if File.executable?(test) IO.popen(test+redir) { |io| t.parse io } else require 'rbconfig' ruby = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name']) # This ought to deal with shebang lines of other languages as well. IO.popen("#{ruby} #{test.dump} #{redir}") { |io| t.parse io } end rescue => e t.bail_out("error running test: #{e.message}") end t } end def summarize @tests.each { |t| @output << "\n" << t.name << ": " t.summarize } unless $TAPTAP_VERBOSE < -1 if @tests.size > 1 all = @tests.inject(0) { |a,e| a + e.ran } failed_suites = @tests.inject(0) { |a,e| a + (e.failed.empty? ? 0 : 1) } failed = @tests.inject(0) { |a,e| a + e.failed.size } run_time = @tests.inject(0) { |a,e| a + e.run_time } @output << "\n" unless $TAPTAP_VERBOSE < -1 @output << "TOTAL: " @output << "Tests took %.2f seconds.\n" % run_time if failed == 0 @output << "All #{all} tests SUCCEEDED." else @output << "Failed %d/%d tests, %d/%d subtests, %02.2f%% okay.\n" % [failed_suites, @tests.size, failed, all, (all-failed.size.to_f)/all*100] end end end end end