From 30e76a6af3f1ea21552a566680a19254e1c005fb Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 10 Oct 2008 01:17:21 +0000 Subject: [PATCH] Restored test/unit git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@19739 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- lib/test/unit/assertionfailederror.rb | 14 + lib/test/unit/assertions.rb | 622 ++++++++++++++++++++++++ lib/test/unit/autorunner.rb | 217 +++++++++ lib/test/unit/collector.rb | 43 ++ lib/test/unit/collector/dir.rb | 113 +++++ lib/test/unit/collector/objectspace.rb | 40 ++ lib/test/unit/error.rb | 56 +++ lib/test/unit/failure.rb | 51 ++ lib/test/unit/testcase.rb | 163 +++++++ lib/test/unit/testresult.rb | 81 +++ lib/test/unit/testsuite.rb | 76 +++ lib/test/unit/ui/console/testrunner.rb | 130 +++++ lib/test/unit/ui/fox/testrunner.rb | 268 ++++++++++ lib/test/unit/ui/gtk/testrunner.rb | 416 ++++++++++++++++ lib/test/unit/ui/gtk2/testrunner.rb | 465 ++++++++++++++++++ lib/test/unit/ui/testrunnermediator.rb | 68 +++ lib/test/unit/ui/testrunnerutilities.rb | 46 ++ lib/test/unit/ui/tk/testrunner.rb | 260 ++++++++++ lib/test/unit/util/backtracefilter.rb | 40 ++ lib/test/unit/util/observable.rb | 90 ++++ lib/test/unit/util/procwrapper.rb | 48 ++ 21 files changed, 3307 insertions(+) create mode 100644 lib/test/unit/assertionfailederror.rb create mode 100644 lib/test/unit/assertions.rb create mode 100644 lib/test/unit/autorunner.rb create mode 100644 lib/test/unit/collector.rb create mode 100644 lib/test/unit/collector/dir.rb create mode 100644 lib/test/unit/collector/objectspace.rb create mode 100644 lib/test/unit/error.rb create mode 100644 lib/test/unit/failure.rb create mode 100644 lib/test/unit/testcase.rb create mode 100644 lib/test/unit/testresult.rb create mode 100644 lib/test/unit/testsuite.rb create mode 100644 lib/test/unit/ui/console/testrunner.rb create mode 100644 lib/test/unit/ui/fox/testrunner.rb create mode 100644 lib/test/unit/ui/gtk/testrunner.rb create mode 100644 lib/test/unit/ui/gtk2/testrunner.rb create mode 100644 lib/test/unit/ui/testrunnermediator.rb create mode 100644 lib/test/unit/ui/testrunnerutilities.rb create mode 100644 lib/test/unit/ui/tk/testrunner.rb create mode 100644 lib/test/unit/util/backtracefilter.rb create mode 100644 lib/test/unit/util/observable.rb create mode 100644 lib/test/unit/util/procwrapper.rb diff --git a/lib/test/unit/assertionfailederror.rb b/lib/test/unit/assertionfailederror.rb new file mode 100644 index 0000000000..a21e4b5870 --- /dev/null +++ b/lib/test/unit/assertionfailederror.rb @@ -0,0 +1,14 @@ +#-- +# +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +module Test + module Unit + + # Thrown by Test::Unit::Assertions when an assertion fails. + class AssertionFailedError < StandardError + end + end +end diff --git a/lib/test/unit/assertions.rb b/lib/test/unit/assertions.rb new file mode 100644 index 0000000000..d9c9e096ba --- /dev/null +++ b/lib/test/unit/assertions.rb @@ -0,0 +1,622 @@ +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2003 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +require 'test/unit/assertionfailederror' +require 'test/unit/util/backtracefilter' + +module Test # :nodoc: + module Unit # :nodoc: + + ## + # Test::Unit::Assertions contains the standard Test::Unit assertions. + # Assertions is included in Test::Unit::TestCase. + # + # To include it in your own code and use its functionality, you simply + # need to rescue Test::Unit::AssertionFailedError. Additionally you may + # override add_assertion to get notified whenever an assertion is made. + # + # Notes: + # * The message to each assertion, if given, will be propagated with the + # failure. + # * It is easy to add your own assertions based on assert_block(). + # + # = Example Custom Assertion + # + # def deny(boolean, message = nil) + # message = build_message message, ' is not false or nil.', boolean + # assert_block message do + # not boolean + # end + # end + + module Assertions + + ## + # The assertion upon which all other assertions are based. Passes if the + # block yields true. + # + # Example: + # assert_block "Couldn't do the thing" do + # do_the_thing + # end + + public + def assert_block(message="assert_block failed.") # :yields: + _wrap_assertion do + if (! yield) + raise AssertionFailedError.new(message.to_s) + end + end + end + + ## + # Asserts that +boolean+ is not false or nil. + # + # Example: + # assert [1, 2].include?(5) + + public + def assert(boolean, message=nil) + _wrap_assertion do + assert_block("assert should not be called with a block.") { !block_given? } + assert_block(build_message(message, " is not true.", boolean)) { boolean } + end + end + + ## + # Passes if +expected+ == +actual. + # + # Note that the ordering of arguments is important, since a helpful + # error message is generated when this one fails that tells you the + # values of expected and actual. + # + # Example: + # assert_equal 'MY STRING', 'my string'.upcase + + public + def assert_equal(expected, actual, message=nil) + full_message = build_message(message, < expected but was +. +EOT + assert_block(full_message) { expected == actual } + end + + private + def _check_exception_class(args) # :nodoc: + args.partition do |klass| + next if klass.instance_of?(Module) + assert(Exception >= klass, "Should expect a class of exception, #{klass}") + true + end + end + + private + def _expected_exception?(actual_exception, exceptions, modules) # :nodoc: + exceptions.include?(actual_exception.class) or + modules.any? {|mod| actual_exception.is_a?(mod)} + end + + ## + # Passes if the block raises one of the given exceptions. + # + # Example: + # assert_raise RuntimeError, LoadError do + # raise 'Boom!!!' + # end + + public + def assert_raise(*args) + _wrap_assertion do + if Module === args.last + message = "" + else + message = args.pop + end + exceptions, modules = _check_exception_class(args) + expected = args.size == 1 ? args.first : args + actual_exception = nil + full_message = build_message(message, " exception expected but none was thrown.", expected) + assert_block(full_message) do + begin + yield + rescue Exception => actual_exception + break + end + false + end + full_message = build_message(message, " exception expected but was\n?", expected, actual_exception) + assert_block(full_message) {_expected_exception?(actual_exception, exceptions, modules)} + actual_exception + end + end + + ## + # Alias of assert_raise. + # + # Will be deprecated in 1.9, and removed in 2.0. + + public + def assert_raises(*args, &block) + assert_raise(*args, &block) + end + + ## + # Passes if +object+ .instance_of? +klass+ + # + # Example: + # assert_instance_of String, 'foo' + + public + def assert_instance_of(klass, object, message="") + _wrap_assertion do + assert_equal(Class, klass.class, "assert_instance_of takes a Class as its first argument") + full_message = build_message(message, < expected to be an instance of + but was +. +EOT + assert_block(full_message){object.instance_of?(klass)} + end + end + + ## + # Passes if +object+ is nil. + # + # Example: + # assert_nil [1, 2].uniq! + + public + def assert_nil(object, message="") + assert_equal(nil, object, message) + end + + ## + # Passes if +object+ .kind_of? +klass+ + # + # Example: + # assert_kind_of Object, 'foo' + + public + def assert_kind_of(klass, object, message="") + _wrap_assertion do + assert(klass.kind_of?(Module), "The first parameter to assert_kind_of should be a kind_of Module.") + full_message = build_message(message, "\nexpected to be kind_of\\?\n but was\n.", object, klass, object.class) + assert_block(full_message){object.kind_of?(klass)} + end + end + + ## + # Passes if +object+ .respond_to? +method+ + # + # Example: + # assert_respond_to 'bugbear', :slice + + public + def assert_respond_to(object, method, message="") + _wrap_assertion do + full_message = build_message(nil, "\ngiven as the method name argument to #assert_respond_to must be a Symbol or #respond_to\\?(:to_str).", method) + + assert_block(full_message) do + method.kind_of?(Symbol) || method.respond_to?(:to_str) + end + full_message = build_message(message, < +of type +expected to respond_to\\?. +EOT + assert_block(full_message) { object.respond_to?(method) } + end + end + + ## + # Passes if +string+ =~ +pattern+. + # + # Example: + # assert_match(/\d+/, 'five, 6, seven') + + public + def assert_match(pattern, string, message="") + _wrap_assertion do + pattern = case(pattern) + when String + Regexp.new(Regexp.escape(pattern)) + else + pattern + end + full_message = build_message(message, " expected to be =~\n.", string, pattern) + assert_block(full_message) { string =~ pattern } + end + end + + ## + # Passes if +actual+ .equal? +expected+ (i.e. they are the same + # instance). + # + # Example: + # o = Object.new + # assert_same o, o + + public + def assert_same(expected, actual, message="") + full_message = build_message(message, < +with id expected to be equal\\? to + +with id . +EOT + assert_block(full_message) { actual.equal?(expected) } + end + + ## + # Compares the +object1+ with +object2+ using +operator+. + # + # Passes if object1.__send__(operator, object2) is true. + # + # Example: + # assert_operator 5, :>=, 4 + + public + def assert_operator(object1, operator, object2, message="") + _wrap_assertion do + full_message = build_message(nil, "\ngiven as the operator for #assert_operator must be a Symbol or #respond_to\\?(:to_str).", operator) + assert_block(full_message){operator.kind_of?(Symbol) || operator.respond_to?(:to_str)} + full_message = build_message(message, < expected to be +? +. +EOT + assert_block(full_message) { object1.__send__(operator, object2) } + end + end + + ## + # Passes if block does not raise an exception. + # + # Example: + # assert_nothing_raised do + # [1, 2].uniq + # end + + public + def assert_nothing_raised(*args) + _wrap_assertion do + if Module === args.last + message = "" + else + message = args.pop + end + exceptions, modules = _check_exception_class(args) + begin + yield + rescue Exception => e + if ((args.empty? && !e.instance_of?(AssertionFailedError)) || + _expected_exception?(e, exceptions, modules)) + assert_block(build_message(message, "Exception raised:\n?", e)){false} + else + raise + end + end + nil + end + end + + ## + # Flunk always fails. + # + # Example: + # flunk 'Not done testing yet.' + + public + def flunk(message="Flunked") + assert_block(build_message(message)){false} + end + + ## + # Passes if ! +actual+ .equal? +expected+ + # + # Example: + # assert_not_same Object.new, Object.new + + public + def assert_not_same(expected, actual, message="") + full_message = build_message(message, < +with id expected to not be equal\\? to + +with id . +EOT + assert_block(full_message) { !actual.equal?(expected) } + end + + ## + # Passes if +expected+ != +actual+ + # + # Example: + # assert_not_equal 'some string', 5 + + public + def assert_not_equal(expected, actual, message="") + full_message = build_message(message, " expected to be != to\n.", expected, actual) + assert_block(full_message) { expected != actual } + end + + ## + # Passes if ! +object+ .nil? + # + # Example: + # assert_not_nil '1 two 3'.sub!(/two/, '2') + + public + def assert_not_nil(object, message="") + full_message = build_message(message, " expected to not be nil.", object) + assert_block(full_message){!object.nil?} + end + + ## + # Passes if +regexp+ !~ +string+ + # + # Example: + # assert_no_match(/two/, 'one 2 three') + + public + def assert_no_match(regexp, string, message="") + _wrap_assertion do + assert_instance_of(Regexp, regexp, "The first argument to assert_no_match should be a Regexp.") + full_message = build_message(message, " expected to not match\n.", regexp, string) + assert_block(full_message) { regexp !~ string } + end + end + + UncaughtThrow = { + ArgumentError => /^uncaught throw (.+)$/, + } #` + + ## + # Passes if the block throws +expected_object+ + # + # Example: + # assert_throws :done do + # throw :done + # end + + public + def assert_throws(expected_object, message="", &proc) + _wrap_assertion do + assert_block("Should have passed a block to assert_throws."){block_given?} + caught = true + begin + catch(expected_object) do + proc.call + caught = false + end + full_message = build_message(message, " should have been thrown.", expected_object) + assert_block(full_message){caught} + rescue ArgumentError => error + if UncaughtThrow[error.class] !~ error.message + raise error + end + full_message = build_message(message, " expected to be thrown but\n<#$1> was thrown.", expected_object) + flunk(full_message) + end + end + end + + ## + # Passes if block does not throw anything. + # + # Example: + # assert_nothing_thrown do + # [1, 2].uniq + # end + + public + def assert_nothing_thrown(message="", &proc) + _wrap_assertion do + assert(block_given?, "Should have passed a block to assert_nothing_thrown") + begin + proc.call + rescue ArgumentError => error + if UncaughtThrow[error.class] !~ error.message + raise error + end + full_message = build_message(message, "<#$1> was thrown when nothing was expected") + flunk(full_message) + end + assert(true, "Expected nothing to be thrown") + end + end + + ## + # Passes if +expected_float+ and +actual_float+ are equal + # within +delta+ tolerance. + # + # Example: + # assert_in_delta 0.05, (50000.0 / 10**6), 0.00001 + + public + def assert_in_delta(expected_float, actual_float, delta, message="") + _wrap_assertion do + {expected_float => "first float", actual_float => "second float", delta => "delta"}.each do |float, name| + assert_respond_to(float, :to_f, "The arguments must respond to to_f; the #{name} did not") + end + assert_operator(delta, :>=, 0.0, "The delta should not be negative") + full_message = build_message(message, < and + expected to be within + of each other. +EOT + assert_block(full_message) { (expected_float.to_f - actual_float.to_f).abs <= delta.to_f } + end + end + + ## + # Passes if the method send returns a true value. + # + # +send_array+ is composed of: + # * A receiver + # * A method + # * Arguments to the method + # + # Example: + # assert_send [[1, 2], :include?, 4] + + public + def assert_send(send_array, message="") + _wrap_assertion do + assert_instance_of(Array, send_array, "assert_send requires an array of send information") + assert(send_array.size >= 2, "assert_send requires at least a receiver and a message name") + full_message = build_message(message, < expected to respond to + with a true value. +EOT + assert_block(full_message) { send_array[0].__send__(send_array[1], *send_array[2..-1]) } + end + end + + ## + # Builds a failure message. +head+ is added before the +template+ and + # +arguments+ replaces the '?'s positionally in the template. + + public + def build_message(head, template=nil, *arguments) # :nodoc: + template &&= template.chomp + return AssertionMessage.new(head, template, arguments) + end + + private + def _wrap_assertion # :nodoc: + @_assertion_wrapped ||= false + unless (@_assertion_wrapped) + @_assertion_wrapped = true + begin + add_assertion + return yield + ensure + @_assertion_wrapped = false + end + else + return yield + end + end + + ## + # Called whenever an assertion is made. Define this in classes that + # include Test::Unit::Assertions to record assertion counts. + + private + def add_assertion + end + + ## + # Select whether or not to use the pretty-printer. If this option is set + # to false before any assertions are made, pp.rb will not be required. + + public + def self.use_pp=(value) + AssertionMessage.use_pp = value + end + + # :stopdoc: + + class AssertionMessage + @use_pp = true + class << self + attr_accessor :use_pp + end + + class Literal + def initialize(value) + @value = value + end + + def inspect + @value.to_s + end + end + + class Template + def self.create(string) + parts = (string ? string.scan(/(?=[^\\])\?|(?:\\\?|[^\?])+/m) : []) + self.new(parts) + end + + attr_reader :count + + def initialize(parts) + @parts = parts + @count = parts.find_all{|e| e == '?'}.size + end + + def result(parameters) + raise "The number of parameters does not match the number of substitutions." if(parameters.size != count) + params = parameters.dup + @parts.collect{|e| e == '?' ? params.shift : e.gsub(/\\\?/m, '?')}.join('') + end + end + + def self.literal(value) + Literal.new(value) + end + + include Util::BacktraceFilter + + def initialize(head, template_string, parameters) + @head = head + @template_string = template_string + @parameters = parameters + end + + def convert(object) + case object + when Exception + < +Message: <#{convert(object.message)}> +---Backtrace--- +#{filter_backtrace(object.backtrace).join("\n")} +--------------- +EOM + else + if(self.class.use_pp) + begin + require 'pp' + rescue LoadError + self.class.use_pp = false + return object.inspect + end unless(defined?(PP)) + PP.pp(object, '').chomp + else + object.inspect + end + end + end + + def template + @template ||= Template.create(@template_string) + end + + def add_period(string) + (string =~ /\.\Z/ ? string : string + '.') + end + + def to_s + message_parts = [] + if (@head) + head = @head.to_s + unless(head.empty?) + message_parts << add_period(head) + end + end + tail = template.result(@parameters.collect{|e| convert(e)}) + message_parts << tail unless(tail.empty?) + message_parts.join("\n") + end + end + + # :startdoc: + + end + end +end diff --git a/lib/test/unit/autorunner.rb b/lib/test/unit/autorunner.rb new file mode 100644 index 0000000000..c252e2e7cb --- /dev/null +++ b/lib/test/unit/autorunner.rb @@ -0,0 +1,217 @@ +require 'test/unit' +require 'test/unit/ui/testrunnerutilities' +require 'optparse' + +module Test + module Unit + class AutoRunner + def self.run(force_standalone=false, default_dir=nil, argv=ARGV, &block) + r = new(force_standalone || standalone?, &block) + r.base = default_dir + r.process_args(argv) + r.run + end + + def self.standalone? + return false unless("-e" == $0) + TestCase::DECENDANT_CLASSES.empty? + end + + RUNNERS = { + :console => proc do |r| + require 'test/unit/ui/console/testrunner' + Test::Unit::UI::Console::TestRunner + end, + :gtk => proc do |r| + require 'test/unit/ui/gtk/testrunner' + Test::Unit::UI::GTK::TestRunner + end, + :gtk2 => proc do |r| + require 'test/unit/ui/gtk2/testrunner' + Test::Unit::UI::GTK2::TestRunner + end, + :fox => proc do |r| + require 'test/unit/ui/fox/testrunner' + Test::Unit::UI::Fox::TestRunner + end, + :tk => proc do |r| + require 'test/unit/ui/tk/testrunner' + Test::Unit::UI::Tk::TestRunner + end, + } + + OUTPUT_LEVELS = [ + [:silent, UI::SILENT], + [:progress, UI::PROGRESS_ONLY], + [:normal, UI::NORMAL], + [:verbose, UI::VERBOSE], + ] + + COLLECTORS = { + :objectspace => proc do |r| + require 'test/unit/collector/objectspace' + c = Collector::ObjectSpace.new + c.filter = r.filters + c.collect($0.sub(/\.rb\Z/, '')) + end, + :dir => proc do |r| + require 'test/unit/collector/dir' + c = Collector::Dir.new + c.filter = r.filters + c.pattern.concat(r.pattern) if(r.pattern) + c.exclude.concat(r.exclude) if(r.exclude) + c.base = r.base + $:.push(r.base) if r.base + c.collect(*(r.to_run.empty? ? ['.'] : r.to_run)) + end, + } + + attr_reader :suite + attr_accessor :output_level, :filters, :to_run, :pattern, :exclude, :base, :workdir + attr_writer :runner, :collector + + def initialize(standalone) + Unit.run = true + @standalone = standalone + @runner = RUNNERS[:console] + @collector = COLLECTORS[(standalone ? :dir : :objectspace)] + @filters = [] + @to_run = [] + @output_level = UI::NORMAL + @workdir = false + yield(self) if(block_given?) + end + + def process_args(args = ARGV) + begin + options.order!(args) {|arg| @to_run << arg} + rescue OptionParser::ParseError => e + puts e + puts options + $! = nil + abort + else + @filters << proc{false} unless(@filters.empty?) + end + not @to_run.empty? + end + + def options + @options ||= OptionParser.new do |o| + o.banner = "Test::Unit automatic runner." + o.banner << "\nUsage: #{$0} [options] [-- untouched arguments]" + + o.on + o.on('-r', '--runner=RUNNER', RUNNERS, + "Use the given RUNNER.", + "(" + keyword_display(RUNNERS) + ")") do |r| + @runner = r + end + + if(@standalone) + o.on('-b', '--basedir=DIR', "Base directory of test suites.") do |b| + @base = b + end + + o.on('-w', '--workdir=DIR', "Working directory to run tests.") do |w| + @workdir = w + end + + o.on('-a', '--add=TORUN', Array, + "Add TORUN to the list of things to run;", + "can be a file or a directory.") do |a| + @to_run.concat(a) + end + + @pattern = [] + o.on('-p', '--pattern=PATTERN', Regexp, + "Match files to collect against PATTERN.") do |e| + @pattern << e + end + + @exclude = [] + o.on('-x', '--exclude=PATTERN', Regexp, + "Ignore files to collect against PATTERN.") do |e| + @exclude << e + end + end + + o.on('-n', '--name=NAME', String, + "Runs tests matching NAME.", + "(patterns may be used).") do |n| + n = (%r{\A/(.*)/\Z} =~ n ? Regexp.new($1) : n) + case n + when Regexp + @filters << proc{|t| n =~ t.method_name ? true : nil} + else + @filters << proc{|t| n == t.method_name ? true : nil} + end + end + + o.on('-t', '--testcase=TESTCASE', String, + "Runs tests in TestCases matching TESTCASE.", + "(patterns may be used).") do |n| + n = (%r{\A/(.*)/\Z} =~ n ? Regexp.new($1) : n) + case n + when Regexp + @filters << proc{|t| n =~ t.class.name ? true : nil} + else + @filters << proc{|t| n == t.class.name ? true : nil} + end + end + + o.on('-I', "--load-path=DIR[#{File::PATH_SEPARATOR}DIR...]", + "Appends directory list to $LOAD_PATH.") do |dirs| + $LOAD_PATH.concat(dirs.split(File::PATH_SEPARATOR)) + end + + o.on('-v', '--verbose=[LEVEL]', OUTPUT_LEVELS, + "Set the output level (default is verbose).", + "(" + keyword_display(OUTPUT_LEVELS) + ")") do |l| + @output_level = l || UI::VERBOSE + end + + o.on('--', + "Stop processing options so that the", + "remaining options will be passed to the", + "test."){o.terminate} + + o.on('-h', '--help', 'Display this help.'){puts o; exit} + + o.on_tail + o.on_tail('Deprecated options:') + + o.on_tail('--console', 'Console runner (use --runner).') do + warn("Deprecated option (--console).") + @runner = RUNNERS[:console] + end + + o.on_tail('--gtk', 'GTK runner (use --runner).') do + warn("Deprecated option (--gtk).") + @runner = RUNNERS[:gtk] + end + + o.on_tail('--fox', 'Fox runner (use --runner).') do + warn("Deprecated option (--fox).") + @runner = RUNNERS[:fox] + end + + o.on_tail + end + end + + def keyword_display(array) + list = array.collect {|e, *| e.to_s} + Array === array or list.sort! + list.collect {|e| e.sub(/^(.)([A-Za-z]+)(?=\w*$)/, '\\1[\\2]')}.join(", ") + end + + def run + @suite = @collector[self] + result = @runner[self] or return false + Dir.chdir(@workdir) if @workdir + result.run(@suite, @output_level).passed? + end + end + end +end diff --git a/lib/test/unit/collector.rb b/lib/test/unit/collector.rb new file mode 100644 index 0000000000..9e9e654147 --- /dev/null +++ b/lib/test/unit/collector.rb @@ -0,0 +1,43 @@ +module Test + module Unit + module Collector + def initialize + @filters = [] + end + + def filter=(filters) + @filters = case(filters) + when Proc + [filters] + when Array + filters + end + end + + def add_suite(destination, suite) + to_delete = suite.tests.find_all{|t| !include?(t)} + to_delete.each{|t| suite.delete(t)} + destination << suite unless(suite.size == 0) + end + + def include?(test) + return true if(@filters.empty?) + @filters.each do |filter| + result = filter[test] + if(result.nil?) + next + elsif(!result) + return false + else + return true + end + end + true + end + + def sort(suites) + suites.sort_by{|s| s.name} + end + end + end +end diff --git a/lib/test/unit/collector/dir.rb b/lib/test/unit/collector/dir.rb new file mode 100644 index 0000000000..9c311b72ce --- /dev/null +++ b/lib/test/unit/collector/dir.rb @@ -0,0 +1,113 @@ +require 'test/unit/testsuite' +require 'test/unit/collector' + +module Test + module Unit + module Collector + class Dir + include Collector + + attr_reader :pattern, :exclude + attr_accessor :base + + def initialize(dir=::Dir, file=::File, object_space=nil, req=nil) + super() + @dir = dir + @file = file + @object_space = object_space + @req = req + @pattern = [/\btest_.*\.rb\Z/m] + @exclude = [] + end + + def collect(*from) + basedir = @base + $:.push(basedir) if basedir + if(from.empty?) + recursive_collect('.', find_test_cases) + elsif(from.size == 1) + recursive_collect(from.first, find_test_cases) + else + suites = [] + from.each do |f| + suite = recursive_collect(f, find_test_cases) + suites << suite unless(suite.tests.empty?) + end + suite = TestSuite.new("[#{from.join(', ')}]") + sort(suites).each{|s| suite << s} + suite + end + ensure + $:.delete_at($:.rindex(basedir)) if basedir + end + + def find_test_cases(ignore=[]) + cases = [] + if @object_space + @object_space.each_object(Class) do |c| + cases << c if(c < TestCase && !ignore.include?(c)) + end + else + TestCase::DECENDANT_CLASSES.each do |c| + cases << c if !ignore.include?(c) + end + end + ignore.concat(cases) + cases + end + + def recursive_collect(name, already_gathered) + sub_suites = [] + path = realdir(name) + if @file.directory?(path) + dir_name = name unless name == '.' + @dir.entries(path).each do |e| + next if(e == '.' || e == '..') + e_name = dir_name ? @file.join(dir_name, e) : e + if @file.directory?(realdir(e_name)) + next if /\A(?:CVS|\.svn)\z/ =~ e + sub_suite = recursive_collect(e_name, already_gathered) + sub_suites << sub_suite unless(sub_suite.empty?) + else + next if /~\z/ =~ e_name or /\A\.\#/ =~ e + if @pattern and !@pattern.empty? + next unless @pattern.any? {|pat| pat =~ e_name} + end + if @exclude and !@exclude.empty? + next if @exclude.any? {|pat| pat =~ e_name} + end + collect_file(e_name, sub_suites, already_gathered) + end + end + else + collect_file(name, sub_suites, already_gathered) + end + suite = TestSuite.new(@file.basename(name)) + sort(sub_suites).each{|s| suite << s} + suite + end + + def collect_file(name, suites, already_gathered) + dir = @file.dirname(@file.expand_path(name, @base)) + $:.unshift(dir) + if(@req) + @req.require(name) + else + require(name) + end + find_test_cases(already_gathered).each{|t| add_suite(suites, t.suite)} + ensure + $:.delete_at($:.index(dir)) if dir + end + + def realdir(path) + if @base + @file.join(@base, path) + else + path + end + end + end + end + end +end diff --git a/lib/test/unit/collector/objectspace.rb b/lib/test/unit/collector/objectspace.rb new file mode 100644 index 0000000000..4a768c721e --- /dev/null +++ b/lib/test/unit/collector/objectspace.rb @@ -0,0 +1,40 @@ +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2003 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +require 'test/unit/collector' + +module Test + module Unit + module Collector + class ObjectSpace + include Test::Unit::Collector + + NAME = 'collected from the subclasses of TestCase' + + def initialize(source=nil) + super() + @source = source + end + + def collect(name=NAME) + suite = TestSuite.new(name) + sub_suites = [] + if @source + @source.each_object(Class) do |klass| + if(Test::Unit::TestCase > klass) + add_suite(sub_suites, klass.suite) + end + end + else + TestCase::DECENDANT_CLASSES.each do |klass| + add_suite(sub_suites, klass.suite) + end + end + sort(sub_suites).each{|s| suite << s} + suite + end + end + end + end +end diff --git a/lib/test/unit/error.rb b/lib/test/unit/error.rb new file mode 100644 index 0000000000..43a813f7d1 --- /dev/null +++ b/lib/test/unit/error.rb @@ -0,0 +1,56 @@ +#-- +# +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +require 'test/unit/util/backtracefilter' + +module Test + module Unit + + # Encapsulates an error in a test. Created by + # Test::Unit::TestCase when it rescues an exception thrown + # during the processing of a test. + class Error + include Util::BacktraceFilter + + attr_reader(:test_name, :exception) + + SINGLE_CHARACTER = 'E' + + # Creates a new Error with the given test_name and + # exception. + def initialize(test_name, exception) + @test_name = test_name + @exception = exception + end + + # Returns a single character representation of an error. + def single_character_display + SINGLE_CHARACTER + end + + # Returns the message associated with the error. + def message + "#{@exception.class.name}: #{@exception.message}" + end + + # Returns a brief version of the error description. + def short_display + "#@test_name: #{message.split("\n")[0]}" + end + + # Returns a verbose version of the error description. + def long_display + backtrace = filter_backtrace(@exception.backtrace).join("\n ") + "Error:\n#@test_name:\n#{message}\n #{backtrace}" + end + + # Overridden to return long_display. + def to_s + long_display + end + end + end +end diff --git a/lib/test/unit/failure.rb b/lib/test/unit/failure.rb new file mode 100644 index 0000000000..832c99857c --- /dev/null +++ b/lib/test/unit/failure.rb @@ -0,0 +1,51 @@ +#-- +# +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +module Test + module Unit + + # Encapsulates a test failure. Created by Test::Unit::TestCase + # when an assertion fails. + class Failure + attr_reader :test_name, :location, :message + + SINGLE_CHARACTER = 'F' + + # Creates a new Failure with the given location and + # message. + def initialize(test_name, location, message) + @test_name = test_name + @location = location + @message = message + end + + # Returns a single character representation of a failure. + def single_character_display + SINGLE_CHARACTER + end + + # Returns a brief version of the error description. + def short_display + "#@test_name: #{@message.split("\n")[0]}" + end + + # Returns a verbose version of the error description. + def long_display + location_display = if(location.size == 1) + location[0].sub(/\A(.+:\d+).*/, ' [\\1]') + else + "\n [#{location.join("\n ")}]" + end + "Failure:\n#@test_name#{location_display}:\n#@message" + end + + # Overridden to return long_display. + def to_s + long_display + end + end + end +end diff --git a/lib/test/unit/testcase.rb b/lib/test/unit/testcase.rb new file mode 100644 index 0000000000..3cf80d2256 --- /dev/null +++ b/lib/test/unit/testcase.rb @@ -0,0 +1,163 @@ +#-- +# +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2003 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +require 'test/unit/assertions' +require 'test/unit/failure' +require 'test/unit/error' +require 'test/unit/testsuite' +require 'test/unit/assertionfailederror' +require 'test/unit/util/backtracefilter' + +module Test + module Unit + + # Ties everything together. If you subclass and add your own + # test methods, it takes care of making them into tests and + # wrapping those tests into a suite. It also does the + # nitty-gritty of actually running an individual test and + # collecting its results into a Test::Unit::TestResult object. + class TestCase + include Assertions + include Util::BacktraceFilter + + attr_reader :method_name + + STARTED = name + "::STARTED" + FINISHED = name + "::FINISHED" + + ## + # These exceptions are not caught by #run. + + PASSTHROUGH_EXCEPTIONS = [NoMemoryError, SignalException, Interrupt, + SystemExit] + + DECENDANT_CLASSES = [] + def self.inherited(decendant) + DECENDANT_CLASSES << decendant + end + + # Creates a new instance of the fixture for running the + # test represented by test_method_name. + def initialize(test_method_name) + unless(respond_to?(test_method_name) && method(test_method_name).arity == 0) + throw :invalid_test + end + @method_name = test_method_name + @test_passed = true + end + + # Rolls up all of the test* methods in the fixture into + # one suite, creating a new instance of the fixture for + # each method. + def self.suite + method_names = public_instance_methods(true).map { |m| m.to_s } + tests = method_names.delete_if {|method_name| method_name !~ /^test./} + suite = TestSuite.new(name) + tests.sort.each do + |test| + catch(:invalid_test) do + suite << new(test) + end + end + if (suite.empty?) + catch(:invalid_test) do + suite << new(:default_test) + end + end + return suite + end + + # Runs the individual test method represented by this + # instance of the fixture, collecting statistics, failures + # and errors in result. + def run(result) + yield(STARTED, name) + @_result = result + begin + setup + __send__(@method_name) + rescue AssertionFailedError => e + add_failure(e.message, e.backtrace) + rescue Exception + raise if PASSTHROUGH_EXCEPTIONS.include? $!.class + add_error($!) + ensure + begin + teardown + rescue AssertionFailedError => e + add_failure(e.message, e.backtrace) + rescue Exception + raise if PASSTHROUGH_EXCEPTIONS.include? $!.class + add_error($!) + end + end + result.add_run + yield(FINISHED, name) + end + + # Called before every test method runs. Can be used + # to set up fixture information. + def setup + end + + # Called after every test method runs. Can be used to tear + # down fixture information. + def teardown + end + + def default_test + flunk("No tests were specified") + end + + # Returns whether this individual test passed or + # not. Primarily for use in teardown so that artifacts + # can be left behind if the test fails. + def passed? + return @test_passed + end + private :passed? + + def size # :nodoc: + 1 + end + + def add_assertion # :nodoc: + @_result.add_assertion + end + private :add_assertion + + def add_failure(message, all_locations=caller()) # :nodoc: + @test_passed = false + @_result.add_failure(Failure.new(name, filter_backtrace(all_locations), message)) + end + private :add_failure + + def add_error(exception) # :nodoc: + @test_passed = false + @_result.add_error(Error.new(name, exception)) + end + private :add_error + + # Returns a human-readable name for the specific test that + # this instance of TestCase represents. + def name + "#{@method_name}(#{self.class.name})" + end + + # Overridden to return #name. + def to_s + name + end + + # It's handy to be able to compare TestCase instances. + def ==(other) + return false unless(other.kind_of?(self.class)) + return false unless(@method_name == other.method_name) + self.class == other.class + end + end + end +end diff --git a/lib/test/unit/testresult.rb b/lib/test/unit/testresult.rb new file mode 100644 index 0000000000..3fe9b7c57f --- /dev/null +++ b/lib/test/unit/testresult.rb @@ -0,0 +1,81 @@ +#-- +# +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +require 'test/unit/util/observable' + +module Test + module Unit + + # Collects Test::Unit::Failure and Test::Unit::Error so that + # they can be displayed to the user. To this end, observers + # can be added to it, allowing the dynamic updating of, say, a + # UI. + class TestResult + include Util::Observable + + CHANGED = "CHANGED" + FAULT = "FAULT" + + attr_reader(:run_count, :assertion_count) + + # Constructs a new, empty TestResult. + def initialize + @run_count, @assertion_count = 0, 0 + @failures, @errors = Array.new, Array.new + end + + # Records a test run. + def add_run + @run_count += 1 + notify_listeners(CHANGED, self) + end + + # Records a Test::Unit::Failure. + def add_failure(failure) + @failures << failure + notify_listeners(FAULT, failure) + notify_listeners(CHANGED, self) + end + + # Records a Test::Unit::Error. + def add_error(error) + @errors << error + notify_listeners(FAULT, error) + notify_listeners(CHANGED, self) + end + + # Records an individual assertion. + def add_assertion + @assertion_count += 1 + notify_listeners(CHANGED, self) + end + + # Returns a string contain the recorded runs, assertions, + # failures and errors in this TestResult. + def to_s + "#{run_count} tests, #{assertion_count} assertions, #{failure_count} failures, #{error_count} errors" + end + + # Returns whether or not this TestResult represents + # successful completion. + def passed? + return @failures.empty? && @errors.empty? + end + + # Returns the number of failures this TestResult has + # recorded. + def failure_count + return @failures.size + end + + # Returns the number of errors this TestResult has + # recorded. + def error_count + return @errors.size + end + end + end +end diff --git a/lib/test/unit/testsuite.rb b/lib/test/unit/testsuite.rb new file mode 100644 index 0000000000..6fea976c50 --- /dev/null +++ b/lib/test/unit/testsuite.rb @@ -0,0 +1,76 @@ +#-- +# +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2003 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +module Test + module Unit + + # A collection of tests which can be #run. + # + # Note: It is easy to confuse a TestSuite instance with + # something that has a static suite method; I know because _I_ + # have trouble keeping them straight. Think of something that + # has a suite method as simply providing a way to get a + # meaningful TestSuite instance. + class TestSuite + attr_reader :name, :tests + + STARTED = name + "::STARTED" + FINISHED = name + "::FINISHED" + + # Creates a new TestSuite with the given name. + def initialize(name="Unnamed TestSuite") + @name = name + @tests = [] + end + + # Runs the tests and/or suites contained in this + # TestSuite. + def run(result, &progress_block) + yield(STARTED, name) + @tests.each do |test| + test.run(result, &progress_block) + end + yield(FINISHED, name) + end + + # Adds the test to the suite. + def <<(test) + @tests << test + self + end + + def delete(test) + @tests.delete(test) + end + + # Retuns the rolled up number of tests in this suite; + # i.e. if the suite contains other suites, it counts the + # tests within those suites, not the suites themselves. + def size + total_size = 0 + @tests.each { |test| total_size += test.size } + total_size + end + + def empty? + tests.empty? + end + + # Overridden to return the name given the suite at + # creation. + def to_s + @name + end + + # It's handy to be able to compare TestSuite instances. + def ==(other) + return false unless(other.kind_of?(self.class)) + return false unless(@name == other.name) + @tests == other.tests + end + end + end +end diff --git a/lib/test/unit/ui/console/testrunner.rb b/lib/test/unit/ui/console/testrunner.rb new file mode 100644 index 0000000000..b6475c0037 --- /dev/null +++ b/lib/test/unit/ui/console/testrunner.rb @@ -0,0 +1,130 @@ +#-- +# +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2003 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +require 'test/unit/ui/testrunnermediator' +require 'test/unit/ui/testrunnerutilities' + +module Test + module Unit + module UI + module Console # :nodoc: + + # Runs a Test::Unit::TestSuite on the console. + class TestRunner + extend TestRunnerUtilities + + # Creates a new TestRunner for running the passed + # suite. If quiet_mode is true, the output while + # running is limited to progress dots, errors and + # failures, and the final result. io specifies + # where runner output should go to; defaults to + # STDOUT. + def initialize(suite, output_level=NORMAL, io=STDOUT) + if (suite.respond_to?(:suite)) + @suite = suite.suite + else + @suite = suite + end + @output_level = output_level + @io = io + @already_outputted = false + @faults = [] + end + + # Begins the test run. + def start + setup_mediator + attach_to_mediator + return start_mediator + end + + private + def setup_mediator # :nodoc: + @mediator = create_mediator(@suite) + suite_name = @suite.to_s + if ( @suite.kind_of?(Module) ) + suite_name = @suite.name + end + output("Loaded suite #{suite_name}") + end + + def create_mediator(suite) # :nodoc: + return TestRunnerMediator.new(suite) + end + + def attach_to_mediator # :nodoc: + @mediator.add_listener(TestResult::FAULT, &method(:add_fault)) + @mediator.add_listener(TestRunnerMediator::STARTED, &method(:started)) + @mediator.add_listener(TestRunnerMediator::FINISHED, &method(:finished)) + @mediator.add_listener(TestCase::STARTED, &method(:test_started)) + @mediator.add_listener(TestCase::FINISHED, &method(:test_finished)) + end + + def start_mediator # :nodoc: + return @mediator.run_suite + end + + def add_fault(fault) # :nodoc: + @faults << fault + output_single(fault.single_character_display, PROGRESS_ONLY) + @already_outputted = true + end + + def started(result) + @result = result + output("Started") + end + + def finished(elapsed_time) + nl + output("Finished in #{elapsed_time} seconds.") + @faults.each_with_index do |fault, index| + nl + output("%3d) %s" % [index + 1, fault.long_display]) + end + nl + output(@result) + end + + def test_started(name) + $program_name = $0 + alias $0 $program_name + $PROGRAM_NAME += "\0#{name}" + output_single(name + ": ", VERBOSE) + end + + def test_finished(name) + output_single(".", PROGRESS_ONLY) unless (@already_outputted) + nl(VERBOSE) + @already_outputted = false + end + + def nl(level=NORMAL) + output("", level) + end + + def output(something, level=NORMAL) + @io.puts(something) if (output?(level)) + @io.flush + end + + def output_single(something, level=NORMAL) + @io.write(something) if (output?(level)) + @io.flush + end + + def output?(level) + level <= @output_level + end + end + end + end + end +end + +if __FILE__ == $0 + Test::Unit::UI::Console::TestRunner.start_command_line_test +end diff --git a/lib/test/unit/ui/fox/testrunner.rb b/lib/test/unit/ui/fox/testrunner.rb new file mode 100644 index 0000000000..34a8ff1288 --- /dev/null +++ b/lib/test/unit/ui/fox/testrunner.rb @@ -0,0 +1,268 @@ +#-- +# +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +require 'fox' +require 'test/unit/ui/testrunnermediator' +require 'test/unit/ui/testrunnerutilities' + +include Fox + +module Test + module Unit + module UI + module Fox # :nodoc: + + # Runs a Test::Unit::TestSuite in a Fox UI. Obviously, + # this one requires you to have Fox + # (http://www.fox-toolkit.org/fox.html) and the Ruby + # Fox extension (http://fxruby.sourceforge.net/) + # installed. + class TestRunner + + extend TestRunnerUtilities + + RED_STYLE = FXRGBA(0xFF,0,0,0xFF) #0xFF000000 + GREEN_STYLE = FXRGBA(0,0xFF,0,0xFF) #0x00FF0000 + + # Creates a new TestRunner for running the passed + # suite. + def initialize(suite, output_level = NORMAL) + if (suite.respond_to?(:suite)) + @suite = suite.suite + else + @suite = suite + end + + @result = nil + @red = false + end + + # Begins the test run. + def start + setup_ui + setup_mediator + attach_to_mediator + start_ui + @result + end + + def setup_mediator # :nodoc: + @mediator = TestRunnerMediator.new(@suite) + suite_name = @suite.to_s + if ( @suite.kind_of?(Module) ) + suite_name = @suite.name + end + @suite_name_entry.text = suite_name + end + + def attach_to_mediator # :nodoc: + @mediator.add_listener(TestRunnerMediator::RESET, &method(:reset_ui)) + @mediator.add_listener(TestResult::FAULT, &method(:add_fault)) + @mediator.add_listener(TestResult::CHANGED, &method(:result_changed)) + @mediator.add_listener(TestRunnerMediator::STARTED, &method(:started)) + @mediator.add_listener(TestCase::STARTED, &method(:test_started)) + @mediator.add_listener(TestRunnerMediator::FINISHED, &method(:finished)) + end + + def start_ui # :nodoc: + @application.create + @window.show(PLACEMENT_SCREEN) + @application.addTimeout(1) do + @mediator.run_suite + end + @application.run + end + + def stop # :nodoc: + @application.exit(0) + end + + def reset_ui(count) # :nodoc: + @test_progress_bar.barColor = GREEN_STYLE + @test_progress_bar.total = count + @test_progress_bar.progress = 0 + @red = false + + @test_count_label.text = "0" + @assertion_count_label.text = "0" + @failure_count_label.text = "0" + @error_count_label.text = "0" + + @fault_list.clearItems + end + + def add_fault(fault) # :nodoc: + if ( ! @red ) + @test_progress_bar.barColor = RED_STYLE + @red = true + end + item = FaultListItem.new(fault) + @fault_list.appendItem(item) + end + + def show_fault(fault) # :nodoc: + raw_show_fault(fault.long_display) + end + + def raw_show_fault(string) # :nodoc: + @detail_text.setText(string) + end + + def clear_fault # :nodoc: + raw_show_fault("") + end + + def result_changed(result) # :nodoc: + @test_progress_bar.progress = result.run_count + + @test_count_label.text = result.run_count.to_s + @assertion_count_label.text = result.assertion_count.to_s + @failure_count_label.text = result.failure_count.to_s + @error_count_label.text = result.error_count.to_s + + # repaint now! + @info_panel.repaint + @application.flush + end + + def started(result) # :nodoc: + @result = result + output_status("Started...") + end + + def test_started(test_name) + output_status("Running #{test_name}...") + end + + def finished(elapsed_time) + output_status("Finished in #{elapsed_time} seconds") + end + + def output_status(string) + @status_entry.text = string + @status_entry.repaint + end + + def setup_ui # :nodoc: + @application = create_application + create_tooltip(@application) + + @window = create_window(@application) + + @status_entry = create_entry(@window) + + main_panel = create_main_panel(@window) + + suite_panel = create_suite_panel(main_panel) + create_label(suite_panel, "Suite:") + @suite_name_entry = create_entry(suite_panel) + create_button(suite_panel, "&Run\tRun the current suite", proc { @mediator.run_suite }) + + @test_progress_bar = create_progress_bar(main_panel) + + @info_panel = create_info_panel(main_panel) + create_label(@info_panel, "Tests:") + @test_count_label = create_label(@info_panel, "0") + create_label(@info_panel, "Assertions:") + @assertion_count_label = create_label(@info_panel, "0") + create_label(@info_panel, "Failures:") + @failure_count_label = create_label(@info_panel, "0") + create_label(@info_panel, "Errors:") + @error_count_label = create_label(@info_panel, "0") + + list_panel = create_list_panel(main_panel) + @fault_list = create_fault_list(list_panel) + + detail_panel = create_detail_panel(main_panel) + @detail_text = create_text(detail_panel) + end + + def create_application # :nodoc: + app = FXApp.new("TestRunner", "Test::Unit") + app.init([]) + app + end + + def create_window(app) + FXMainWindow.new(app, "Test::Unit TestRunner", nil, nil, DECOR_ALL, 0, 0, 450) + end + + def create_tooltip(app) + FXTooltip.new(app) + end + + def create_main_panel(parent) # :nodoc: + panel = FXVerticalFrame.new(parent, LAYOUT_FILL_X | LAYOUT_FILL_Y) + panel.vSpacing = 10 + panel + end + + def create_suite_panel(parent) # :nodoc: + FXHorizontalFrame.new(parent, LAYOUT_SIDE_LEFT | LAYOUT_FILL_X) + end + + def create_button(parent, text, action) # :nodoc: + FXButton.new(parent, text).connect(SEL_COMMAND, &action) + end + + def create_progress_bar(parent) # :nodoc: + FXProgressBar.new(parent, nil, 0, PROGRESSBAR_NORMAL | LAYOUT_FILL_X) + end + + def create_info_panel(parent) # :nodoc: + FXMatrix.new(parent, 1, MATRIX_BY_ROWS | LAYOUT_FILL_X) + end + + def create_label(parent, text) + FXLabel.new(parent, text, nil, JUSTIFY_CENTER_X | LAYOUT_FILL_COLUMN) + end + + def create_list_panel(parent) # :nodoc: + FXHorizontalFrame.new(parent, LAYOUT_FILL_X | FRAME_SUNKEN | FRAME_THICK) + end + + def create_fault_list(parent) # :nodoc: + list = FXList.new(parent, 10, nil, 0, LIST_SINGLESELECT | LAYOUT_FILL_X) #, 0, 0, 0, 150) + list.connect(SEL_COMMAND) do |sender, sel, ptr| + if sender.retrieveItem(sender.currentItem).selected? + show_fault(sender.retrieveItem(sender.currentItem).fault) + else + clear_fault + end + end + list + end + + def create_detail_panel(parent) # :nodoc: + FXHorizontalFrame.new(parent, LAYOUT_FILL_X | LAYOUT_FILL_Y | FRAME_SUNKEN | FRAME_THICK) + end + + def create_text(parent) # :nodoc: + FXText.new(parent, nil, 0, TEXT_READONLY | LAYOUT_FILL_X | LAYOUT_FILL_Y) + end + + def create_entry(parent) # :nodoc: + entry = FXTextField.new(parent, 30, nil, 0, TEXTFIELD_NORMAL | LAYOUT_SIDE_BOTTOM | LAYOUT_FILL_X) + entry.disable + entry + end + end + + class FaultListItem < FXListItem # :nodoc: all + attr_reader(:fault) + def initialize(fault) + super(fault.short_display) + @fault = fault + end + end + end + end + end +end + +if __FILE__ == $0 + Test::Unit::UI::Fox::TestRunner.start_command_line_test +end diff --git a/lib/test/unit/ui/gtk/testrunner.rb b/lib/test/unit/ui/gtk/testrunner.rb new file mode 100644 index 0000000000..c63cc6a39b --- /dev/null +++ b/lib/test/unit/ui/gtk/testrunner.rb @@ -0,0 +1,416 @@ +#-- +# +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +require 'gtk' +require 'test/unit/ui/testrunnermediator' +require 'test/unit/ui/testrunnerutilities' + +module Test + module Unit + module UI + module GTK # :nodoc: + + # Runs a Test::Unit::TestSuite in a Gtk UI. Obviously, + # this one requires you to have Gtk + # (http://www.gtk.org/) and the Ruby Gtk extension + # (http://ruby-gnome.sourceforge.net/) installed. + class TestRunner + extend TestRunnerUtilities + + # Creates a new TestRunner for running the passed + # suite. + def initialize(suite, output_level = NORMAL) + if (suite.respond_to?(:suite)) + @suite = suite.suite + else + @suite = suite + end + @result = nil + + @runner = Thread.current + @restart_signal = Class.new(Exception) + @viewer = Thread.start do + @runner.join rescue @runner.run + Gtk.main + end + @viewer.join rescue nil # wait deadlock to handshake + end + + # Begins the test run. + def start + setup_mediator + setup_ui + attach_to_mediator + start_ui + @result + end + + private + def setup_mediator # :nodoc: + @mediator = TestRunnerMediator.new(@suite) + suite_name = @suite.to_s + if ( @suite.kind_of?(Module) ) + suite_name = @suite.name + end + suite_name_entry.set_text(suite_name) + end + + def attach_to_mediator # :nodoc: + run_button.signal_connect("clicked", nil, &method(:run_test)) + @mediator.add_listener(TestRunnerMediator::RESET, &method(:reset_ui)) + @mediator.add_listener(TestResult::FAULT, &method(:add_fault)) + @mediator.add_listener(TestResult::CHANGED, &method(:result_changed)) + @mediator.add_listener(TestRunnerMediator::STARTED, &method(:started)) + @mediator.add_listener(TestCase::STARTED, &method(:test_started)) + @mediator.add_listener(TestCase::FINISHED, &method(:test_finished)) + @mediator.add_listener(TestRunnerMediator::FINISHED, &method(:finished)) + end + + def run_test(*) + @runner.raise(@restart_signal) + end + + def start_ui # :nodoc: + @viewer.run + running = false + begin + loop do + if (running ^= true) + run_button.child.text = "Stop" + @mediator.run_suite + else + run_button.child.text = "Run" + @viewer.join + break + end + end + rescue @restart_signal + retry + rescue + end + end + + def stop(*) # :nodoc: + Gtk.main_quit + end + + def reset_ui(count) # :nodoc: + test_progress_bar.set_style(green_style) + test_progress_bar.configure(0, 0, count) + @red = false + + run_count_label.set_text("0") + assertion_count_label.set_text("0") + failure_count_label.set_text("0") + error_count_label.set_text("0") + + fault_list.remove_items(fault_list.children) + end + + def add_fault(fault) # :nodoc: + if ( ! @red ) + test_progress_bar.set_style(red_style) + @red = true + end + item = FaultListItem.new(fault) + item.show + fault_list.append_items([item]) + end + + def show_fault(fault) # :nodoc: + raw_show_fault(fault.long_display) + end + + def raw_show_fault(string) # :nodoc: + fault_detail_label.set_text(string) + outer_detail_sub_panel.queue_resize + end + + def clear_fault # :nodoc: + raw_show_fault("") + end + + def result_changed(result) # :nodoc: + run_count_label.set_text(result.run_count.to_s) + assertion_count_label.set_text(result.assertion_count.to_s) + failure_count_label.set_text(result.failure_count.to_s) + error_count_label.set_text(result.error_count.to_s) + end + + def started(result) # :nodoc: + @result = result + output_status("Started...") + end + + def test_started(test_name) + output_status("Running #{test_name}...") + end + + def test_finished(test_name) + test_progress_bar.set_value(test_progress_bar.get_value + 1) + end + + def finished(elapsed_time) + output_status("Finished in #{elapsed_time} seconds") + end + + def output_status(string) # :nodoc: + status_entry.set_text(string) + end + + def setup_ui # :nodoc: + main_window.signal_connect("destroy", nil, &method(:stop)) + main_window.show_all + fault_list.signal_connect("select-child", nil) { + | list, item, data | + show_fault(item.fault) + } + fault_list.signal_connect("unselect-child", nil) { + clear_fault + } + @red = false + end + + def main_window # :nodoc: + lazy_initialize(:main_window) { + @main_window = Gtk::Window.new(Gtk::WINDOW_TOPLEVEL) + @main_window.set_title("Test::Unit TestRunner") + @main_window.set_usize(800, 600) + @main_window.set_uposition(20, 20) + @main_window.set_policy(true, true, false) + @main_window.add(main_panel) + } + end + + def main_panel # :nodoc: + lazy_initialize(:main_panel) { + @main_panel = Gtk::VBox.new(false, 0) + @main_panel.pack_start(suite_panel, false, false, 0) + @main_panel.pack_start(progress_panel, false, false, 0) + @main_panel.pack_start(info_panel, false, false, 0) + @main_panel.pack_start(list_panel, false, false, 0) + @main_panel.pack_start(detail_panel, true, true, 0) + @main_panel.pack_start(status_panel, false, false, 0) + } + end + + def suite_panel # :nodoc: + lazy_initialize(:suite_panel) { + @suite_panel = Gtk::HBox.new(false, 10) + @suite_panel.border_width(10) + @suite_panel.pack_start(Gtk::Label.new("Suite:"), false, false, 0) + @suite_panel.pack_start(suite_name_entry, true, true, 0) + @suite_panel.pack_start(run_button, false, false, 0) + } + end + + def suite_name_entry # :nodoc: + lazy_initialize(:suite_name_entry) { + @suite_name_entry = Gtk::Entry.new + @suite_name_entry.set_editable(false) + } + end + + def run_button # :nodoc: + lazy_initialize(:run_button) { + @run_button = Gtk::Button.new("Run") + } + end + + def progress_panel # :nodoc: + lazy_initialize(:progress_panel) { + @progress_panel = Gtk::HBox.new(false, 10) + @progress_panel.border_width(10) + @progress_panel.pack_start(test_progress_bar, true, true, 0) + } + end + + def test_progress_bar # :nodoc: + lazy_initialize(:test_progress_bar) { + @test_progress_bar = EnhancedProgressBar.new + @test_progress_bar.set_usize(@test_progress_bar.allocation.width, + info_panel.size_request.height) + @test_progress_bar.set_style(green_style) + } + end + + def green_style # :nodoc: + lazy_initialize(:green_style) { + @green_style = Gtk::Style.new + @green_style.set_bg(Gtk::STATE_PRELIGHT, 0x0000, 0xFFFF, 0x0000) + } + end + + def red_style # :nodoc: + lazy_initialize(:red_style) { + @red_style = Gtk::Style.new + @red_style.set_bg(Gtk::STATE_PRELIGHT, 0xFFFF, 0x0000, 0x0000) + } + end + + def info_panel # :nodoc: + lazy_initialize(:info_panel) { + @info_panel = Gtk::HBox.new(false, 0) + @info_panel.border_width(10) + @info_panel.pack_start(Gtk::Label.new("Runs:"), false, false, 0) + @info_panel.pack_start(run_count_label, true, false, 0) + @info_panel.pack_start(Gtk::Label.new("Assertions:"), false, false, 0) + @info_panel.pack_start(assertion_count_label, true, false, 0) + @info_panel.pack_start(Gtk::Label.new("Failures:"), false, false, 0) + @info_panel.pack_start(failure_count_label, true, false, 0) + @info_panel.pack_start(Gtk::Label.new("Errors:"), false, false, 0) + @info_panel.pack_start(error_count_label, true, false, 0) + } + end + + def run_count_label # :nodoc: + lazy_initialize(:run_count_label) { + @run_count_label = Gtk::Label.new("0") + @run_count_label.set_justify(Gtk::JUSTIFY_LEFT) + } + end + + def assertion_count_label # :nodoc: + lazy_initialize(:assertion_count_label) { + @assertion_count_label = Gtk::Label.new("0") + @assertion_count_label.set_justify(Gtk::JUSTIFY_LEFT) + } + end + + def failure_count_label # :nodoc: + lazy_initialize(:failure_count_label) { + @failure_count_label = Gtk::Label.new("0") + @failure_count_label.set_justify(Gtk::JUSTIFY_LEFT) + } + end + + def error_count_label # :nodoc: + lazy_initialize(:error_count_label) { + @error_count_label = Gtk::Label.new("0") + @error_count_label.set_justify(Gtk::JUSTIFY_LEFT) + } + end + + def list_panel # :nodoc: + lazy_initialize(:list_panel) { + @list_panel = Gtk::HBox.new + @list_panel.border_width(10) + @list_panel.pack_start(list_scrolled_window, true, true, 0) + } + end + + def list_scrolled_window # :nodoc: + lazy_initialize(:list_scrolled_window) { + @list_scrolled_window = Gtk::ScrolledWindow.new + @list_scrolled_window.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC) + @list_scrolled_window.set_usize(@list_scrolled_window.allocation.width, 150) + @list_scrolled_window.add_with_viewport(fault_list) + } + end + + def fault_list # :nodoc: + lazy_initialize(:fault_list) { + @fault_list = Gtk::List.new + } + end + + def detail_panel # :nodoc: + lazy_initialize(:detail_panel) { + @detail_panel = Gtk::HBox.new + @detail_panel.border_width(10) + @detail_panel.pack_start(detail_scrolled_window, true, true, 0) + } + end + + def detail_scrolled_window # :nodoc: + lazy_initialize(:detail_scrolled_window) { + @detail_scrolled_window = Gtk::ScrolledWindow.new + @detail_scrolled_window.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC) + @detail_scrolled_window.set_usize(400, @detail_scrolled_window.allocation.height) + @detail_scrolled_window.add_with_viewport(outer_detail_sub_panel) + } + end + + def outer_detail_sub_panel # :nodoc: + lazy_initialize(:outer_detail_sub_panel) { + @outer_detail_sub_panel = Gtk::VBox.new + @outer_detail_sub_panel.pack_start(inner_detail_sub_panel, false, false, 0) + } + end + + def inner_detail_sub_panel # :nodoc: + lazy_initialize(:inner_detail_sub_panel) { + @inner_detail_sub_panel = Gtk::HBox.new + @inner_detail_sub_panel.pack_start(fault_detail_label, false, false, 0) + } + end + + def fault_detail_label # :nodoc: + lazy_initialize(:fault_detail_label) { + @fault_detail_label = EnhancedLabel.new("") + style = Gtk::Style.new + font = Gdk::Font.font_load("-*-Courier New-medium-r-normal--*-120-*-*-*-*-*-*") + begin + style.set_font(font) + rescue ArgumentError; end + @fault_detail_label.set_style(style) + @fault_detail_label.set_justify(Gtk::JUSTIFY_LEFT) + @fault_detail_label.set_line_wrap(false) + } + end + + def status_panel # :nodoc: + lazy_initialize(:status_panel) { + @status_panel = Gtk::HBox.new + @status_panel.border_width(10) + @status_panel.pack_start(status_entry, true, true, 0) + } + end + + def status_entry # :nodoc: + lazy_initialize(:status_entry) { + @status_entry = Gtk::Entry.new + @status_entry.set_editable(false) + } + end + + def lazy_initialize(symbol) # :nodoc: + if (!instance_eval("defined?(@#{symbol.to_s})")) + yield + end + return instance_eval("@" + symbol.to_s) + end + end + + class EnhancedProgressBar < Gtk::ProgressBar # :nodoc: all + def set_style(style) + super + hide + show + end + end + + class EnhancedLabel < Gtk::Label # :nodoc: all + def set_text(text) + super(text.gsub(/\n\t/, "\n" + (" " * 4))) + end + end + + class FaultListItem < Gtk::ListItem # :nodoc: all + attr_reader(:fault) + def initialize(fault) + super(fault.short_display) + @fault = fault + end + end + end + end + end +end + +if __FILE__ == $0 + Test::Unit::UI::GTK::TestRunner.start_command_line_test +end diff --git a/lib/test/unit/ui/gtk2/testrunner.rb b/lib/test/unit/ui/gtk2/testrunner.rb new file mode 100644 index 0000000000..128424cf34 --- /dev/null +++ b/lib/test/unit/ui/gtk2/testrunner.rb @@ -0,0 +1,465 @@ +#-- +# +# Author:: Kenta MURATA. +# Copyright:: Copyright (c) 2000-2002 Kenta MURATA. All rights reserved. +# License:: Ruby license. + +require "gtk2" +require "test/unit/ui/testrunnermediator" +require "test/unit/ui/testrunnerutilities" + +module Test + module Unit + module UI + module GTK2 # :nodoc: all + + Gtk.init + + class EnhancedLabel < Gtk::Label # :nodoc: all + def set_text(text) + super(text.gsub(/\n\t/, "\n ")) + end + end + + class FaultList < Gtk::TreeView # :nodoc: all + def initialize + @faults = [] + @model = Gtk::ListStore.new(String, String) + super(@model) + column = Gtk::TreeViewColumn.new + column.visible = false + append_column(column) + renderer = Gtk::CellRendererText.new + column = Gtk::TreeViewColumn.new("Failures", renderer, {:text => 1}) + append_column(column) + selection.mode = Gtk::SELECTION_SINGLE + set_rules_hint(true) + set_headers_visible(false) + end # def initialize + + def add_fault(fault) + @faults.push(fault) + iter = @model.append + iter.set_value(0, (@faults.length - 1).to_s) + iter.set_value(1, fault.short_display) + end # def add_fault(fault) + + def get_fault(iter) + @faults[iter.get_value(0).to_i] + end # def get_fault + + def clear + model.clear + end # def clear + end + + class TestRunner + extend TestRunnerUtilities + + def lazy_initialize(symbol) # :nodoc: + if !instance_eval("defined?(@#{symbol})") then + yield + end + return instance_eval("@#{symbol}") + end + private :lazy_initialize + + def status_entry # :nodoc: + lazy_initialize(:status_entry) do + @status_entry = Gtk::Entry.new + @status_entry.editable = false + end + end + private :status_entry + + def status_panel # :nodoc: + lazy_initialize(:status_panel) do + @status_panel = Gtk::HBox.new + @status_panel.border_width = 10 + @status_panel.pack_start(status_entry, true, true, 0) + end + end + private :status_panel + + def fault_detail_label # :nodoc: + lazy_initialize(:fault_detail_label) do + @fault_detail_label = EnhancedLabel.new("") +# style = Gtk::Style.new +# font = Gdk::Font. +# font_load("-*-Courier 10 Pitch-medium-r-normal--*-120-*-*-*-*-*-*") +# style.set_font(font) +# @fault_detail_label.style = style + @fault_detail_label.justify = Gtk::JUSTIFY_LEFT + @fault_detail_label.wrap = false + end + end + private :fault_detail_label + + def inner_detail_sub_panel # :nodoc: + lazy_initialize(:inner_detail_sub_panel) do + @inner_detail_sub_panel = Gtk::HBox.new + @inner_detail_sub_panel.pack_start(fault_detail_label, false, false, 0) + end + end + private :inner_detail_sub_panel + + def outer_detail_sub_panel # :nodoc: + lazy_initialize(:outer_detail_sub_panel) do + @outer_detail_sub_panel = Gtk::VBox.new + @outer_detail_sub_panel.pack_start(inner_detail_sub_panel, false, false, 0) + end + end + private :outer_detail_sub_panel + + def detail_scrolled_window # :nodoc: + lazy_initialize(:detail_scrolled_window) do + @detail_scrolled_window = Gtk::ScrolledWindow.new + @detail_scrolled_window.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC) + @detail_scrolled_window. + set_size_request(400, @detail_scrolled_window.allocation.height) + @detail_scrolled_window.add_with_viewport(outer_detail_sub_panel) + end + end + private :detail_scrolled_window + + def detail_panel # :nodoc: + lazy_initialize(:detail_panel) do + @detail_panel = Gtk::HBox.new + @detail_panel.border_width = 10 + @detail_panel.pack_start(detail_scrolled_window, true, true, 0) + end + end + private :detail_panel + + def fault_list # :nodoc: + lazy_initialize(:fault_list) do + @fault_list = FaultList.new + end + end + private :fault_list + + def list_scrolled_window # :nodoc: + lazy_initialize(:list_scrolled_window) do + @list_scrolled_window = Gtk::ScrolledWindow.new + @list_scrolled_window.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC) + @list_scrolled_window. + set_size_request(@list_scrolled_window.allocation.width, 150) + @list_scrolled_window.add_with_viewport(fault_list) + end + end + private :list_scrolled_window + + def list_panel # :nodoc: + lazy_initialize(:list_panel) do + @list_panel = Gtk::HBox.new + @list_panel.border_width = 10 + @list_panel.pack_start(list_scrolled_window, true, true, 0) + end + end + private :list_panel + + def error_count_label # :nodoc: + lazy_initialize(:error_count_label) do + @error_count_label = Gtk::Label.new("0") + @error_count_label.justify = Gtk::JUSTIFY_LEFT + end + end + private :error_count_label + + def failure_count_label # :nodoc: + lazy_initialize(:failure_count_label) do + @failure_count_label = Gtk::Label.new("0") + @failure_count_label.justify = Gtk::JUSTIFY_LEFT + end + end + private :failure_count_label + + def assertion_count_label # :nodoc: + lazy_initialize(:assertion_count_label) do + @assertion_count_label = Gtk::Label.new("0") + @assertion_count_label.justify = Gtk::JUSTIFY_LEFT + end + end + private :assertion_count_label + + def run_count_label # :nodoc: + lazy_initialize(:run_count_label) do + @run_count_label = Gtk::Label.new("0") + @run_count_label.justify = Gtk::JUSTIFY_LEFT + end + end + private :run_count_label + + def info_panel # :nodoc: + lazy_initialize(:info_panel) do + @info_panel = Gtk::HBox.new(false, 0) + @info_panel.border_width = 10 + @info_panel.pack_start(Gtk::Label.new("Runs:"), false, false, 0) + @info_panel.pack_start(run_count_label, true, false, 0) + @info_panel.pack_start(Gtk::Label.new("Assertions:"), false, false, 0) + @info_panel.pack_start(assertion_count_label, true, false, 0) + @info_panel.pack_start(Gtk::Label.new("Failures:"), false, false, 0) + @info_panel.pack_start(failure_count_label, true, false, 0) + @info_panel.pack_start(Gtk::Label.new("Errors:"), false, false, 0) + @info_panel.pack_start(error_count_label, true, false, 0) + end + end # def info_panel + private :info_panel + + def green_style # :nodoc: + lazy_initialize(:green_style) do + @green_style = Gtk::Style.new + @green_style.set_bg(Gtk::STATE_PRELIGHT, 0x0000, 0xFFFF, 0x0000) + end + end # def green_style + private :green_style + + def red_style # :nodoc: + lazy_initialize(:red_style) do + @red_style = Gtk::Style.new + @red_style.set_bg(Gtk::STATE_PRELIGHT, 0xFFFF, 0x0000, 0x0000) + end + end # def red_style + private :red_style + + def test_progress_bar # :nodoc: + lazy_initialize(:test_progress_bar) { + @test_progress_bar = Gtk::ProgressBar.new + @test_progress_bar.fraction = 0.0 + @test_progress_bar. + set_size_request(@test_progress_bar.allocation.width, + info_panel.size_request[1]) + @test_progress_bar.style = green_style + } + end # def test_progress_bar + private :test_progress_bar + + def progress_panel # :nodoc: + lazy_initialize(:progress_panel) do + @progress_panel = Gtk::HBox.new(false, 10) + @progress_panel.border_width = 10 + @progress_panel.pack_start(test_progress_bar, true, true, 0) + end + end # def progress_panel + + def run_button # :nodoc: + lazy_initialize(:run_button) do + @run_button = Gtk::Button.new("Run") + end + end # def run_button + + def suite_name_entry # :nodoc: + lazy_initialize(:suite_name_entry) do + @suite_name_entry = Gtk::Entry.new + @suite_name_entry.editable = false + end + end # def suite_name_entry + private :suite_name_entry + + def suite_panel # :nodoc: + lazy_initialize(:suite_panel) do + @suite_panel = Gtk::HBox.new(false, 10) + @suite_panel.border_width = 10 + @suite_panel.pack_start(Gtk::Label.new("Suite:"), false, false, 0) + @suite_panel.pack_start(suite_name_entry, true, true, 0) + @suite_panel.pack_start(run_button, false, false, 0) + end + end # def suite_panel + private :suite_panel + + def main_panel # :nodoc: + lazy_initialize(:main_panel) do + @main_panel = Gtk::VBox.new(false, 0) + @main_panel.pack_start(suite_panel, false, false, 0) + @main_panel.pack_start(progress_panel, false, false, 0) + @main_panel.pack_start(info_panel, false, false, 0) + @main_panel.pack_start(list_panel, false, false, 0) + @main_panel.pack_start(detail_panel, true, true, 0) + @main_panel.pack_start(status_panel, false, false, 0) + end + end # def main_panel + private :main_panel + + def main_window # :nodoc: + lazy_initialize(:main_window) do + @main_window = Gtk::Window.new(Gtk::Window::TOPLEVEL) + @main_window.set_title("Test::Unit TestRunner") + @main_window.set_default_size(800, 600) + @main_window.set_resizable(true) + @main_window.add(main_panel) + end + end # def main_window + private :main_window + + def setup_ui # :nodoc: + main_window.signal_connect("destroy", nil) { stop } + main_window.show_all + fault_list.selection.signal_connect("changed", nil) do + |selection, data| + if selection.selected then + show_fault(fault_list.get_fault(selection.selected)) + else + clear_fault + end + end + end # def setup_ui + private :setup_ui + + def output_status(string) # :nodoc: + status_entry.set_text(string) + end # def output_status(string) + private :output_status + + def finished(elapsed_time) # :nodoc: + test_progress_bar.fraction = 1.0 + output_status("Finished in #{elapsed_time} seconds") + end # def finished(elapsed_time) + private :finished + + def test_started(test_name) # :nodoc: + output_status("Running #{test_name}...") + end # def test_started(test_name) + private :test_started + + def started(result) # :nodoc: + @result = result + output_status("Started...") + end # def started(result) + private :started + + def test_finished(result) # :nodoc: + test_progress_bar.fraction += 1.0 / @count + end # def test_finished(result) + + def result_changed(result) # :nodoc: + run_count_label.label = result.run_count.to_s + assertion_count_label.label = result.assertion_count.to_s + failure_count_label.label = result.failure_count.to_s + error_count_label.label = result.error_count.to_s + end # def result_changed(result) + private :result_changed + + def clear_fault # :nodoc: + raw_show_fault("") + end # def clear_fault + private :clear_fault + + def raw_show_fault(string) # :nodoc: + fault_detail_label.set_text(string) + outer_detail_sub_panel.queue_resize + end # def raw_show_fault(string) + private :raw_show_fault + + def show_fault(fault) # :nodoc: + raw_show_fault(fault.long_display) + end # def show_fault(fault) + private :show_fault + + def add_fault(fault) # :nodoc: + if not @red then + test_progress_bar.style = red_style + @red = true + end + fault_list.add_fault(fault) + end # def add_fault(fault) + private :add_fault + + def reset_ui(count) # :nodoc: + test_progress_bar.style = green_style + test_progress_bar.fraction = 0.0 + @count = count + 1 + @red = false + + run_count_label.set_text("0") + assertion_count_label.set_text("0") + failure_count_label.set_text("0") + error_count_label.set_text("0") + + fault_list.clear + end # def reset_ui(count) + private :reset_ui + + def stop # :nodoc: + Gtk.main_quit + end # def stop + private :stop + + def run_test + @runner.raise(@restart_signal) + end + private :run_test + + def start_ui # :nodoc + @viewer.run + running = false + begin + loop do + if (running ^= true) + run_button.child.text = "Stop" + @mediator.run_suite + else + run_button.child.text = "Run" + @viewer.join + break + end + end + rescue @restart_signal + retry + rescue + end + end # def start_ui + private :start_ui + + def attach_to_mediator + run_button.signal_connect("clicked", nil) { run_test } + @mediator.add_listener(TestRunnerMediator::RESET, &method(:reset_ui)) + @mediator.add_listener(TestRunnerMediator::STARTED, &method(:started)) + @mediator.add_listener(TestRunnerMediator::FINISHED, &method(:finished)) + @mediator.add_listener(TestResult::FAULT, &method(:add_fault)) + @mediator.add_listener(TestResult::CHANGED, &method(:result_changed)) + @mediator.add_listener(TestCase::STARTED, &method(:test_started)) + @mediator.add_listener(TestCase::FINISHED, &method(:test_finished)) + end # def attach_to_mediator + private :attach_to_mediator + + def setup_mediator + @mediator = TestRunnerMediator.new(@suite) + suite_name = @suite.to_s + if @suite.kind_of?(Module) then + suite_name = @suite.name + end + suite_name_entry.set_text(suite_name) + end # def setup_mediator + private :setup_mediator + + def start + setup_mediator + setup_ui + attach_to_mediator + start_ui + @result + end # def start + + def initialize(suite, output_level = NORMAL) + if suite.respond_to?(:suite) then + @suite = suite.suite + else + @suite = suite + end + @result = nil + + @runner = Thread.current + @restart_signal = Class.new(Exception) + @viewer = Thread.start do + @runner.join rescue @runner.run + Gtk.main + end + @viewer.join rescue nil # wait deadlock to handshake + end # def initialize(suite) + + end # class TestRunner + + end # module GTK2 + end # module UI + end # module Unit +end # module Test diff --git a/lib/test/unit/ui/testrunnermediator.rb b/lib/test/unit/ui/testrunnermediator.rb new file mode 100644 index 0000000000..07bb462cc0 --- /dev/null +++ b/lib/test/unit/ui/testrunnermediator.rb @@ -0,0 +1,68 @@ +#-- +# +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +require 'test/unit' +require 'test/unit/util/observable' +require 'test/unit/testresult' + +module Test + module Unit + module UI # :nodoc: + + # Provides an interface to write any given UI against, + # hopefully making it easy to write new UIs. + class TestRunnerMediator + RESET = name + "::RESET" + STARTED = name + "::STARTED" + FINISHED = name + "::FINISHED" + + include Util::Observable + + # Creates a new TestRunnerMediator initialized to run + # the passed suite. + def initialize(suite) + @suite = suite + end + + # Runs the suite the TestRunnerMediator was created + # with. + def run_suite + Unit.run = true + begin_time = Time.now + notify_listeners(RESET, @suite.size) + result = create_result + notify_listeners(STARTED, result) + result_listener = result.add_listener(TestResult::CHANGED) do |updated_result| + notify_listeners(TestResult::CHANGED, updated_result) + end + + fault_listener = result.add_listener(TestResult::FAULT) do |fault| + notify_listeners(TestResult::FAULT, fault) + end + + @suite.run(result) do |channel, value| + notify_listeners(channel, value) + end + + result.remove_listener(TestResult::FAULT, fault_listener) + result.remove_listener(TestResult::CHANGED, result_listener) + end_time = Time.now + elapsed_time = end_time - begin_time + notify_listeners(FINISHED, elapsed_time) #"Finished in #{elapsed_time} seconds.") + return result + end + + private + # A factory method to create the result the mediator + # should run with. Can be overridden by subclasses if + # one wants to use a different result. + def create_result + return TestResult.new + end + end + end + end +end diff --git a/lib/test/unit/ui/testrunnerutilities.rb b/lib/test/unit/ui/testrunnerutilities.rb new file mode 100644 index 0000000000..70b885bd6c --- /dev/null +++ b/lib/test/unit/ui/testrunnerutilities.rb @@ -0,0 +1,46 @@ +#-- +# +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +module Test + module Unit + module UI + + SILENT = 0 + PROGRESS_ONLY = 1 + NORMAL = 2 + VERBOSE = 3 + + # Provides some utilities common to most, if not all, + # TestRunners. + # + #-- + # + # Perhaps there ought to be a TestRunner superclass? There + # seems to be a decent amount of shared code between test + # runners. + + module TestRunnerUtilities + + # Creates a new TestRunner and runs the suite. + def run(suite, output_level=NORMAL) + return new(suite, output_level).start + end + + # Takes care of the ARGV parsing and suite + # determination necessary for running one of the + # TestRunners from the command line. + def start_command_line_test + if ARGV.empty? + puts "You should supply the name of a test suite file to the runner" + exit + end + require ARGV[0].gsub(/.+::/, '') + new(eval(ARGV[0])).start + end + end + end + end +end diff --git a/lib/test/unit/ui/tk/testrunner.rb b/lib/test/unit/ui/tk/testrunner.rb new file mode 100644 index 0000000000..4521b8e258 --- /dev/null +++ b/lib/test/unit/ui/tk/testrunner.rb @@ -0,0 +1,260 @@ +#-- +# +# Original Author:: Nathaniel Talbott. +# Author:: Kazuhiro NISHIYAMA. +# Copyright:: Copyright (c) 2000-2002 Nathaniel Talbott. All rights reserved. +# Copyright:: Copyright (c) 2003 Kazuhiro NISHIYAMA. All rights reserved. +# License:: Ruby license. + +require 'tk' +require 'test/unit/ui/testrunnermediator' +require 'test/unit/ui/testrunnerutilities' + +module Test + module Unit + module UI + module Tk # :nodoc: + + # Runs a Test::Unit::TestSuite in a Tk UI. Obviously, + # this one requires you to have Tk + # and the Ruby Tk extension installed. + class TestRunner + extend TestRunnerUtilities + + # Creates a new TestRunner for running the passed + # suite. + def initialize(suite, output_level = NORMAL) + if (suite.respond_to?(:suite)) + @suite = suite.suite + else + @suite = suite + end + @result = nil + + @red = false + @fault_detail_list = [] + @runner = Thread.current + @restart_signal = Class.new(Exception) + @viewer = Thread.start do + @runner.join rescue @runner.run + ::Tk.mainloop + end + @viewer.join rescue nil # wait deadlock to handshake + end + + # Begins the test run. + def start + setup_ui + setup_mediator + attach_to_mediator + start_ui + @result + end + + private + def setup_mediator # :nodoc: + @mediator = TestRunnerMediator.new(@suite) + suite_name = @suite.to_s + if ( @suite.kind_of?(Module) ) + suite_name = @suite.name + end + @suite_name_entry.value = suite_name + end + + def attach_to_mediator # :nodoc: + @run_button.command(method(:run_test)) + @fault_list.bind('ButtonPress-1', proc{|y| + fault = @fault_detail_list[@fault_list.nearest(y)] + if fault + show_fault(fault) + end + }, '%y') + @mediator.add_listener(TestRunnerMediator::RESET, &method(:reset_ui)) + @mediator.add_listener(TestResult::FAULT, &method(:add_fault)) + @mediator.add_listener(TestResult::CHANGED, &method(:result_changed)) + @mediator.add_listener(TestRunnerMediator::STARTED, &method(:started)) + @mediator.add_listener(TestCase::STARTED, &method(:test_started)) + @mediator.add_listener(TestRunnerMediator::FINISHED, &method(:finished)) + end + + def run_test + @runner.raise(@restart_signal) + end + + def start_ui # :nodoc: + @viewer.run + running = false + begin + loop do + if (running ^= true) + @run_button.configure('text'=>'Stop') + @mediator.run_suite + else + @run_button.configure('text'=>'Run') + @viewer.join + break + end + end + rescue @restart_signal + retry + rescue + end + end + + def stop # :nodoc: + ::Tk.exit + end + + def reset_ui(count) # :nodoc: + @test_total_count = count.to_f + @test_progress_bar.configure('background'=>'green') + @test_progress_bar.place('relwidth'=>(count.zero? ? 0 : 0/count)) + @red = false + + @test_count_label.value = 0 + @assertion_count_label.value = 0 + @failure_count_label.value = 0 + @error_count_label.value = 0 + + @fault_list.delete(0, 'end') + @fault_detail_list = [] + clear_fault + end + + def add_fault(fault) # :nodoc: + if ( ! @red ) + @test_progress_bar.configure('background'=>'red') + @red = true + end + @fault_detail_list.push fault + @fault_list.insert('end', fault.short_display) + end + + def show_fault(fault) # :nodoc: + raw_show_fault(fault.long_display) + end + + def raw_show_fault(string) # :nodoc: + @detail_text.value = string + end + + def clear_fault # :nodoc: + raw_show_fault("") + end + + def result_changed(result) # :nodoc: + @test_count_label.value = result.run_count + @test_progress_bar.place('relwidth'=>result.run_count/@test_total_count) + @assertion_count_label.value = result.assertion_count + @failure_count_label.value = result.failure_count + @error_count_label.value = result.error_count + end + + def started(result) # :nodoc: + @result = result + output_status("Started...") + end + + def test_started(test_name) + output_status("Running #{test_name}...") + end + + def finished(elapsed_time) + output_status("Finished in #{elapsed_time} seconds") + end + + def output_status(string) # :nodoc: + @status_entry.value = string + end + + def setup_ui # :nodoc: + @status_entry = TkVariable.new + l = TkLabel.new(nil, 'textvariable'=>@status_entry, 'relief'=>'sunken') + l.pack('side'=>'bottom', 'fill'=>'x') + + suite_frame = TkFrame.new.pack('fill'=>'x') + + @run_button = TkButton.new(suite_frame, 'text'=>'Run') + @run_button.pack('side'=>'right') + + TkLabel.new(suite_frame, 'text'=>'Suite:').pack('side'=>'left') + @suite_name_entry = TkVariable.new + l = TkLabel.new(suite_frame, 'textvariable'=>@suite_name_entry, 'relief'=>'sunken') + l.pack('side'=>'left', 'fill'=>'x', 'expand'=>true) + + f = TkFrame.new(nil, 'relief'=>'sunken', 'borderwidth'=>3, 'height'=>20).pack('fill'=>'x', 'padx'=>1) + @test_progress_bar = TkFrame.new(f, 'background'=>'green').place('anchor'=>'nw', 'relwidth'=>0.0, 'relheight'=>1.0) + + info_frame = TkFrame.new.pack('fill'=>'x') + @test_count_label = create_count_label(info_frame, 'Tests:') + @assertion_count_label = create_count_label(info_frame, 'Assertions:') + @failure_count_label = create_count_label(info_frame, 'Failures:') + @error_count_label = create_count_label(info_frame, 'Errors:') + + if (::Tk.info('command', TkPanedWindow::TkCommandNames[0]) != "") + # use panedwindow + paned_frame = TkPanedWindow.new("orient"=>"vertical").pack('fill'=>'both', 'expand'=>true) + + fault_list_frame = TkFrame.new(paned_frame) + detail_frame = TkFrame.new(paned_frame) + + paned_frame.add(fault_list_frame, detail_frame) + else + # no panedwindow + paned_frame = nil + fault_list_frame = TkFrame.new.pack('fill'=>'both', 'expand'=>true) + detail_frame = TkFrame.new.pack('fill'=>'both', 'expand'=>true) + end + + TkGrid.rowconfigure(fault_list_frame, 0, 'weight'=>1, 'minsize'=>0) + TkGrid.columnconfigure(fault_list_frame, 0, 'weight'=>1, 'minsize'=>0) + + fault_scrollbar_y = TkScrollbar.new(fault_list_frame) + fault_scrollbar_x = TkScrollbar.new(fault_list_frame) + @fault_list = TkListbox.new(fault_list_frame) + @fault_list.yscrollbar(fault_scrollbar_y) + @fault_list.xscrollbar(fault_scrollbar_x) + + TkGrid.rowconfigure(detail_frame, 0, 'weight'=>1, 'minsize'=>0) + TkGrid.columnconfigure(detail_frame, 0, 'weight'=>1, 'minsize'=>0) + + ::Tk.grid(@fault_list, fault_scrollbar_y, 'sticky'=>'news') + ::Tk.grid(fault_scrollbar_x, 'sticky'=>'news') + + detail_scrollbar_y = TkScrollbar.new(detail_frame) + detail_scrollbar_x = TkScrollbar.new(detail_frame) + @detail_text = TkText.new(detail_frame, 'height'=>10, 'wrap'=>'none') { + bindtags(bindtags - [TkText]) + } + @detail_text.yscrollbar(detail_scrollbar_y) + @detail_text.xscrollbar(detail_scrollbar_x) + + ::Tk.grid(@detail_text, detail_scrollbar_y, 'sticky'=>'news') + ::Tk.grid(detail_scrollbar_x, 'sticky'=>'news') + + # rubber-style pane + if paned_frame + ::Tk.update + @height = paned_frame.winfo_height + paned_frame.bind('Configure', proc{|h| + paned_frame.sash_place(0, 0, paned_frame.sash_coord(0)[1] * h / @height) + @height = h + }, '%h') + end + end + + def create_count_label(parent, label) # :nodoc: + TkLabel.new(parent, 'text'=>label).pack('side'=>'left', 'expand'=>true) + v = TkVariable.new(0) + TkLabel.new(parent, 'textvariable'=>v).pack('side'=>'left', 'expand'=>true) + v + end + end + end + end + end +end + +if __FILE__ == $0 + Test::Unit::UI::Tk::TestRunner.start_command_line_test +end diff --git a/lib/test/unit/util/backtracefilter.rb b/lib/test/unit/util/backtracefilter.rb new file mode 100644 index 0000000000..7ebec2dfef --- /dev/null +++ b/lib/test/unit/util/backtracefilter.rb @@ -0,0 +1,40 @@ +module Test + module Unit + module Util + module BacktraceFilter + TESTUNIT_FILE_SEPARATORS = %r{[\\/:]} + TESTUNIT_PREFIX = __FILE__.split(TESTUNIT_FILE_SEPARATORS)[0..-3] + TESTUNIT_RB_FILE = /\.rb\Z/ + + def filter_backtrace(backtrace, prefix=nil) + return ["No backtrace"] unless(backtrace) + split_p = if(prefix) + prefix.split(TESTUNIT_FILE_SEPARATORS) + else + TESTUNIT_PREFIX + end + match = proc do |e| + split_e = e.split(TESTUNIT_FILE_SEPARATORS)[0, split_p.size] + next false unless(split_e[0..-2] == split_p[0..-2]) + split_e[-1].sub(TESTUNIT_RB_FILE, '') == split_p[-1] + end + return backtrace unless(backtrace.detect(&match)) + found_prefix = false + new_backtrace = backtrace.reverse.reject do |e| + if(match[e]) + found_prefix = true + true + elsif(found_prefix) + false + else + true + end + end.reverse + new_backtrace = (new_backtrace.empty? ? backtrace : new_backtrace) + new_backtrace = new_backtrace.reject(&match) + new_backtrace.empty? ? backtrace : new_backtrace + end + end + end + end +end diff --git a/lib/test/unit/util/observable.rb b/lib/test/unit/util/observable.rb new file mode 100644 index 0000000000..924c10f24c --- /dev/null +++ b/lib/test/unit/util/observable.rb @@ -0,0 +1,90 @@ +#-- +# +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +require 'test/unit/util/procwrapper' + +module Test + module Unit + module Util # :nodoc: + + # This is a utility class that allows anything mixing + # it in to notify a set of listeners about interesting + # events. + module Observable + # We use this for defaults since nil might mean something + NOTHING = "NOTHING/#{__id__}" + + # Adds the passed proc as a listener on the + # channel indicated by channel_name. listener_key + # is used to remove the listener later; if none is + # specified, the proc itself is used. + # + # Whatever is used as the listener_key is + # returned, making it very easy to use the proc + # itself as the listener_key: + # + # listener = add_listener("Channel") { ... } + # remove_listener("Channel", listener) + def add_listener(channel_name, listener_key=NOTHING, &listener) # :yields: value + unless(block_given?) + raise ArgumentError.new("No callback was passed as a listener") + end + + key = listener_key + if (listener_key == NOTHING) + listener_key = listener + key = ProcWrapper.new(listener) + end + + channels[channel_name] ||= {} + channels[channel_name][key] = listener + return listener_key + end + + # Removes the listener indicated by listener_key + # from the channel indicated by + # channel_name. Returns the registered proc, or + # nil if none was found. + def remove_listener(channel_name, listener_key) + channel = channels[channel_name] + return nil unless (channel) + key = listener_key + if (listener_key.instance_of?(Proc)) + key = ProcWrapper.new(listener_key) + end + if (channel.has_key?(key)) + return channel.delete(key) + end + return nil + end + + # Calls all the procs registered on the channel + # indicated by channel_name. If value is + # specified, it is passed in to the procs, + # otherwise they are called with no arguments. + # + #-- + # + # Perhaps this should be private? Would it ever + # make sense for an external class to call this + # method directly? + def notify_listeners(channel_name, *arguments) + channel = channels[channel_name] + return 0 unless (channel) + listeners = channel.values + listeners.each { |listener| listener.call(*arguments) } + return listeners.size + end + + private + def channels # :nodoc: + @channels ||= {} + return @channels + end + end + end + end +end diff --git a/lib/test/unit/util/procwrapper.rb b/lib/test/unit/util/procwrapper.rb new file mode 100644 index 0000000000..ad3b4d8a6f --- /dev/null +++ b/lib/test/unit/util/procwrapper.rb @@ -0,0 +1,48 @@ +#-- +# +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +module Test + module Unit + module Util + + # Allows the storage of a Proc passed through '&' in a + # hash. + # + # Note: this may be inefficient, since the hash being + # used is not necessarily very good. In Observable, + # efficiency is not too important, since the hash is + # only accessed when adding and removing listeners, + # not when notifying. + + class ProcWrapper + + # Creates a new wrapper for a_proc. + def initialize(a_proc) + @a_proc = a_proc + @hash = a_proc.inspect.sub(/^(#<#{a_proc.class}:)/, '').sub(/(>)$/, '').hex + end + + def hash # :nodoc: + return @hash + end + + def ==(other) # :nodoc: + case(other) + when ProcWrapper + return @a_proc == other.to_proc + else + return super + end + end + alias :eql? :== + + def to_proc # :nodoc: + return @a_proc + end + end + end + end +end