diff options
author | ntalbott <ntalbott@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2003-02-12 03:12:14 +0000 |
---|---|---|
committer | ntalbott <ntalbott@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2003-02-12 03:12:14 +0000 |
commit | 04f2b8f7bf16ad37d3c28f0e06eb02d63ab6a731 (patch) | |
tree | 4354dc87a540b777a7eb6a2276f44be51ec8bebb /lib | |
parent | 4a44a6d474cc793d75344b3a0e4c9b0031802689 (diff) |
Initial revision
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@3477 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Diffstat (limited to 'lib')
-rw-r--r-- | lib/rubyunit.rb | 8 | ||||
-rw-r--r-- | lib/runit/assert.rb | 71 | ||||
-rw-r--r-- | lib/runit/cui/testrunner.rb | 51 | ||||
-rw-r--r-- | lib/runit/error.rb | 9 | ||||
-rw-r--r-- | lib/runit/testcase.rb | 45 | ||||
-rw-r--r-- | lib/runit/testresult.rb | 44 | ||||
-rw-r--r-- | lib/runit/testsuite.rb | 26 | ||||
-rw-r--r-- | lib/runit/topublic.rb | 8 | ||||
-rw-r--r-- | lib/test/unit.rb | 214 | ||||
-rw-r--r-- | lib/test/unit/assertionfailederror.rb | 14 | ||||
-rw-r--r-- | lib/test/unit/assertions.rb | 394 | ||||
-rw-r--r-- | lib/test/unit/error.rb | 68 | ||||
-rw-r--r-- | lib/test/unit/failure.rb | 45 | ||||
-rw-r--r-- | lib/test/unit/testcase.rb | 152 | ||||
-rw-r--r-- | lib/test/unit/testresult.rb | 81 | ||||
-rw-r--r-- | lib/test/unit/testsuite.rb | 64 | ||||
-rw-r--r-- | lib/test/unit/ui/console/testrunner.rb | 135 | ||||
-rw-r--r-- | lib/test/unit/ui/fox/testrunner.rb | 270 | ||||
-rw-r--r-- | lib/test/unit/ui/gtk/testrunner.rb | 389 | ||||
-rw-r--r-- | lib/test/unit/ui/testrunnermediator.rb | 75 | ||||
-rw-r--r-- | lib/test/unit/ui/testrunnerutilities.rb | 36 | ||||
-rw-r--r-- | lib/test/unit/util/observable.rb | 90 | ||||
-rw-r--r-- | lib/test/unit/util/procwrapper.rb | 48 |
23 files changed, 2337 insertions, 0 deletions
diff --git a/lib/rubyunit.rb b/lib/rubyunit.rb new file mode 100644 index 0000000000..8f1086c81d --- /dev/null +++ b/lib/rubyunit.rb @@ -0,0 +1,8 @@ +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +require 'runit/testcase' +require 'test/unit' + +TestCase = RUNIT::TestCase diff --git a/lib/runit/assert.rb b/lib/runit/assert.rb new file mode 100644 index 0000000000..ede7df1c92 --- /dev/null +++ b/lib/runit/assert.rb @@ -0,0 +1,71 @@ +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +require 'test/unit/assertions' +require 'runit/error' + +module RUNIT + module Assert + include Test::Unit::Assertions + + def setup_assert + end + + def assert_no_exception(*args, &block) + assert_nothing_raised(*args, &block) + end + + # To deal with the fact that RubyUnit does not check that the regular expression + # is, indeed, a regular expression, if it is not, we do our own assertion using + # the same semantics as RubyUnit + def assert_match(actual_string, expected_re, message="") + _wrap_assertion { + full_message = build_message(message, actual_string, expected_re) { + | arg1, arg2 | + "Expected <#{arg1}> to match <#{arg2}>" + } + assert_block(full_message) { + expected_re =~ actual_string + } + Regexp.last_match + } + end + + def assert_not_match(actual_string, expected_re, message="") + assert_no_match(expected_re, actual_string, message) + end + + def assert_matches(*args) + assert_match(*args) + end + + def assert_fail(message="") + flunk(message) + end + + def assert_equal_float(expected, actual, delta, message="") + assert_in_delta(expected, actual, delta, message) + end + + def assert_send(object, method, *args) + super([object, method, *args]) + end + + def assert_exception(exception, message="", &block) + assert_raises(exception, message, &block) + end + + def assert_respond_to(method, object, message="") + if (called_internally?) + super + else + super(object, method, message) + end + end + + def called_internally? + /assertions\.rb/.match(caller[1]) + end + end +end diff --git a/lib/runit/cui/testrunner.rb b/lib/runit/cui/testrunner.rb new file mode 100644 index 0000000000..0106b6c859 --- /dev/null +++ b/lib/runit/cui/testrunner.rb @@ -0,0 +1,51 @@ +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +require 'test/unit/ui/console/testrunner' +require 'runit/testresult' + +module RUNIT + module CUI + class TestRunner < Test::Unit::UI::Console::TestRunner + @@quiet_mode = false + + def self.run(suite) + self.new().run(suite) + end + + def initialize + super nil + end + + def run(suite, quiet_mode=@@quiet_mode) + @suite = suite + def @suite.suite + self + end + @output_level = (quiet_mode ? PROGRESS_ONLY : NORMAL) + start + end + + def create_mediator(suite) + mediator = Test::Unit::UI::TestRunnerMediator.new(suite) + class << mediator + attr_writer :result_delegate + def create_result + return @result_delegate.create_result + end + end + mediator.result_delegate = self + return mediator + end + + def create_result + return RUNIT::TestResult.new + end + + def self.quiet_mode=(boolean) + @@quiet_mode = boolean + end + end + end +end diff --git a/lib/runit/error.rb b/lib/runit/error.rb new file mode 100644 index 0000000000..4a727fb02b --- /dev/null +++ b/lib/runit/error.rb @@ -0,0 +1,9 @@ +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +require 'test/unit/assertionfailederror.rb' + +module RUNIT + AssertionFailedError = Test::Unit::AssertionFailedError +end diff --git a/lib/runit/testcase.rb b/lib/runit/testcase.rb new file mode 100644 index 0000000000..4576cb8644 --- /dev/null +++ b/lib/runit/testcase.rb @@ -0,0 +1,45 @@ +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +require 'runit/testresult' +require 'runit/testsuite' +require 'runit/assert' +require 'runit/error' +require 'test/unit/testcase' + +module RUNIT + class TestCase < Test::Unit::TestCase + include RUNIT::Assert + + def self.suite + method_names = instance_methods(true) + tests = method_names.delete_if { |method_name| method_name !~ /^test/ } + suite = TestSuite.new(name) + tests.each { + |test| + catch(:invalid_test) { + suite << new(test, name) + } + } + return suite + end + + def initialize(test_name, suite_name=self.class.name) + super(test_name) + end + + def assert_equals(*args) + assert_equal(*args) + end + + def name + super.sub(/^(.*?)\((.*)\)$/, '\2#\1') + end + + def run(result, &progress_block) + progress_block = proc {} unless (block_given?) + super(result, &progress_block) + end + end +end diff --git a/lib/runit/testresult.rb b/lib/runit/testresult.rb new file mode 100644 index 0000000000..7f70778171 --- /dev/null +++ b/lib/runit/testresult.rb @@ -0,0 +1,44 @@ +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +require 'test/unit/testresult' + +module RUNIT + class TestResult < Test::Unit::TestResult + attr_reader(:errors, :failures) + def succeed? + return passed? + end + def failure_size + return failure_count + end + def run_asserts + return assertion_count + end + def error_size + return error_count + end + def run_tests + return run_count + end + def add_failure(failure) + def failure.at + return location + end + def failure.err + return message + end + super(failure) + end + def add_error(error) + def error.at + return location + end + def error.err + return exception + end + super(error) + end + end +end diff --git a/lib/runit/testsuite.rb b/lib/runit/testsuite.rb new file mode 100644 index 0000000000..63baf65707 --- /dev/null +++ b/lib/runit/testsuite.rb @@ -0,0 +1,26 @@ +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +require 'test/unit/testsuite' + +module RUNIT + class TestSuite < Test::Unit::TestSuite + def add_test(*args) + add(*args) + end + + def add(*args) + self.<<(*args) + end + + def count_test_cases + return size + end + + def run(result, &progress_block) + progress_block = proc {} unless (block_given?) + super(result, &progress_block) + end + end +end diff --git a/lib/runit/topublic.rb b/lib/runit/topublic.rb new file mode 100644 index 0000000000..566f0dd35c --- /dev/null +++ b/lib/runit/topublic.rb @@ -0,0 +1,8 @@ +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +module RUNIT + module ToPublic + end +end diff --git a/lib/test/unit.rb b/lib/test/unit.rb new file mode 100644 index 0000000000..f4202c5294 --- /dev/null +++ b/lib/test/unit.rb @@ -0,0 +1,214 @@ +# :include: ../../../../README +# +# ---- +# +# = Usage +# +# The general idea behind unit testing is that you write a _test_ +# _method_ that makes certain _assertions_ about your code, working +# against a _test_ _fixture_. A bunch of these _test_ _methods_ are +# bundled up into a _test_ _suite_ and can be run any time the +# developer wants. The results of a run are gathered in a _test_ +# _result_ and displayed to the user through some UI. So, lets break +# this down and see how Test::Unit provides each of these necessary +# pieces. +# +# +# == Assertions +# +# These are the heart of the framework. Think of an assertion as a +# statement of expected outcome, i.e. "I assert that x should be equal +# to y". If, when the assertion is executed, it turns out to be +# correct, nothing happens, and life is good. If, on the other hand, +# your assertion turns out to be false, an error is propagated with +# pertinent information so that you can go back and make your +# assertion succeed, and, once again, life is good. For an explanation +# of the current assertions, see Test::Unit::Assertions. +# +# +# == Test Method & Test Fixture +# +# Obviously, these assertions have to be called within a context that +# knows about them and can do something meaningful with their +# pass/fail value. Also, it's handy to collect a bunch of related +# tests, each test represented by a method, into a common test class +# that knows how to run them. The tests will be in a separate class +# from the code they're testing for a couple of reasons. First of all, +# it allows your code to stay uncluttered with test code, making it +# easier to maintain. Second, it allows the tests to be stripped out +# for deployment, since they're really there for you, the developer, +# and your users don't need them. Third, and most importantly, it +# allows you to set up a common test fixture for your tests to run +# against. +# +# What's a test fixture? Well, tests do not live in a vacuum; rather, +# they're run against the code they are testing. Often, a collection +# of tests will run against a common set of data, also called a +# fixture. If they're all bundled into the same test class, they can +# all share the setting up and tearing down of that data, eliminating +# unnecessary duplication and making it much easier to add related +# tests. +# +# Test::Unit::TestCase wraps up a collection of test methods together +# and allows you to easily set up and tear down the same test fixture +# for each test. This is done by overriding #setup and/or #teardown, +# which will be called before and after each test method that is +# run. The TestCase also knows how to collect the results of your +# assertions into a Test::Unit::TestResult, which can then be reported +# back to you... but I'm getting ahead of myself. To write a test, +# follow these steps: +# +# * Make sure Test::Unit is in your library path. +# * require 'test/unit' in your test script. +# * Create a class that subclasses Test::Unit::TestCase. +# * Add a method that begins with "test" to your class. +# * Make assertions in your test method. +# * Optionally define #setup and/or #teardown to set up and/or tear +# down your common test fixture. +# * You can now run your test as you would any other Ruby +# script... try it and see! +# +# A really simple test might look like this (#setup and #teardown are +# commented out to indicate that they are completely optional): +# +# require 'test/unit' +# +# class TC_MyTest < Test::Unit::TestCase +# # def setup +# # end +# +# # def teardown +# # end +# +# def test_fail +# assert(false, 'Assertion was false.') +# end +# end +# +# +# == Test Runners +# +# So, now you have this great test class, but you still need a way to +# run it and view any failures that occur during the run. This is +# where Test::Unit::UI::Console::TestRunner (and others, such as +# Test::Unit::UI::GTK::TestRunner) comes into play. The console test +# runner is automatically invoked for you if you require 'test/unit' +# and simply run the file. To use another runner, or to manually +# invoke a runner, simply call its run class method and pass in an +# object that responds to the suite message with a +# Test::Unit::TestSuite. This can be as simple as passing in your +# TestCase class (which has a class suite method). It might look +# something like this: +# +# require 'test/unit/ui/console/testrunner' +# Test::Unit::UI::Console::TestRunner.run(TC_MyTest) +# +# +# == Test Suite +# +# As more and more unit tests accumulate for a given project, it +# becomes a real drag running them one at a time, and it also +# introduces the potential to overlook a failing test because you +# forget to run it. Suddenly it becomes very handy that the +# TestRunners can take any object that returns a Test::Unit::TestSuite +# in response to a suite method. The TestSuite can, in turn, contain +# other TestSuites or individual tests (typically created by a +# TestCase). In other words, you can easily wrap up a group of +# TestCases and TestSuites like this: +# +# require 'test/unit/testsuite' +# require 'tc_myfirsttests' +# require 'tc_moretestsbyme' +# require 'ts_anothersetoftests' +# +# class TS_MyTests +# def self.suite +# suite = Test::Unit::TestSuite.new +# suite << TC_MyFirstTests.suite +# suite << TC_MoreTestsByMe.suite +# suite << TS_AnotherSetOfTests.suite +# return suite +# end +# end +# Test::Unit::UI::Console::TestRunner.run(TS_MyTests) +# +# Now, this is a bit cumbersome, so Test::Unit does a little bit more +# for you, by wrapping these up automatically when you require +# 'test/unit'. What does this mean? It means you could write the above +# test case like this instead: +# +# require 'test/unit' +# require 'tc_myfirsttests' +# require 'tc_moretestsbyme' +# require 'ts_anothersetoftests' +# +# Test::Unit is smart enough to find all the test cases existing in +# the ObjectSpace and wrap them up into a suite for you. It then runs +# the dynamic suite using the console TestRunner. +# +# +# == Questions? +# +# I'd really like to get feedback from all levels of Ruby +# practitioners about typos, grammatical errors, unclear statements, +# missing points, etc., in this document (or any other). + + + + +require 'test/unit/testcase' +require 'test/unit/ui/testrunnermediator' + +at_exit { + # We can't debug tests run with at_exit unless we add the following: + set_trace_func DEBUGGER__.context.method(:trace_func).to_proc if (defined? DEBUGGER__) + + if (!Test::Unit::UI::TestRunnerMediator.run?) + suite_name = $0.sub(/\.rb$/, '') + suite = Test::Unit::TestSuite.new(suite_name) + test_classes = [] + ObjectSpace.each_object(Class) { + | klass | + test_classes << klass if (Test::Unit::TestCase > klass) + } + + runners = { + '--console' => proc do |suite| + require 'test/unit/ui/console/testrunner' + Test::Unit::UI::Console::TestRunner.run(suite) + end, + '--gtk' => proc do |suite| + require 'test/unit/ui/gtk/testrunner' + Test::Unit::UI::GTK::TestRunner.run(suite) + end, + '--fox' => proc do |suite| + require 'test/unit/ui/fox/testrunner' + Test::Unit::UI::Fox::TestRunner.run(suite) + end, + } + + unless (ARGV.empty?) + runner = runners[ARGV[0]] + ARGV.shift unless (runner.nil?) + end + runner = runners['--console'] if (runner.nil?) + + if ARGV.empty? + test_classes.each { |klass| suite << klass.suite } + else + tests = test_classes.map { |klass| klass.suite.tests }.flatten + criteria = ARGV.map { |arg| (arg =~ %r{^/(.*)/$}) ? Regexp.new($1) : arg } + criteria.each { + | criterion | + if (criterion.instance_of?(Regexp)) + tests.each { |test| suite << test if (criterion =~ test.name) } + elsif (/^A-Z/ =~ criterion) + tests.each { |test| suite << test if (criterion == test.class.name) } + else + tests.each { |test| suite << test if (criterion == test.method_name) } + end + } + end + runner.call(suite) + end +} diff --git a/lib/test/unit/assertionfailederror.rb b/lib/test/unit/assertionfailederror.rb new file mode 100644 index 0000000000..9b9e53f16c --- /dev/null +++ b/lib/test/unit/assertionfailederror.rb @@ -0,0 +1,14 @@ +# :nodoc: +# +# 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 < Exception + end + end +end diff --git a/lib/test/unit/assertions.rb b/lib/test/unit/assertions.rb new file mode 100644 index 0000000000..0af3468b9b --- /dev/null +++ b/lib/test/unit/assertions.rb @@ -0,0 +1,394 @@ +# :nodoc: +# +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +require 'test/unit/assertionfailederror' + +module Test # :nodoc: + module Unit # :nodoc: + + # Contains all of the standard Test::Unit assertions. Mixed in + # to Test::Unit::TestCase. To mix it in and use its + # functionality, you simply need to rescue + # Test::Unit::AssertionFailedError, and you can additionally + # override add_assertion to be notified whenever an assertion + # is made. + # + # Notes: + # * The message to each assertion, if given, will be + # propagated with the failure. + # * It's easy to add your own assertions based on assert_block(). + module Assertions + + # The assertion upon which all other assertions are + # based. Passes if the block yields true. + public + def assert_block(message="") # :yields: + _wrap_assertion do + if (! yield) + raise AssertionFailedError.new(message.to_s) + end + end + end + + # Passes if boolean is true. + public + def assert(boolean, message="") + _wrap_assertion do + assert_block("assert should not be called with a block.") { !block_given? } + assert_block(message) { 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. + public + def assert_equal(expected, actual, message=nil) + full_message = build_message(message, expected, actual) do |arg1, arg2| + "<#{arg1}> expected but was\n" + + "<#{arg2}>" + end + assert_block(full_message) { expected == actual } + end + + # Passes if block raises exception. + public + def assert_raises(expected_exception_klass, message="") + _wrap_assertion do + assert_instance_of(Class, expected_exception_klass, "Should expect a class of exception") + actual_exception = nil + full_message = build_message(message, expected_exception_klass) do |arg| + "<#{arg}> exception expected but none was thrown" + end + assert_block(full_message) do + thrown = false + begin + yield + rescue Exception => thrown_exception + actual_exception = thrown_exception + thrown = true + end + thrown + end + full_message = build_message(message, expected_exception_klass, actual_exception) do |arg1, arg2| + "<#{arg1}> exception expected but was\n" + + arg2 + end + assert_block(full_message) { expected_exception_klass == actual_exception.class } + actual_exception + end + end + + # Passes if object.class == klass. + 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, object, klass, object.class) do |arg1, arg2, arg3| + "<#{arg1}> expected to be an instance of\n" + + "<#{arg2}> but was\n" + + "<#{arg3}>" + end + assert_block(full_message) { klass == object.class } + end + end + + # Passes if object.nil?. + public + def assert_nil(object, message="") + assert_equal(nil, object, message) + end + + # Passes if object.kind_of?(klass). + 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, object, klass) do |arg1, arg2| + "<#{arg1}>\n" + + "expected to be kind_of?<#{arg2}>" + end + assert_block(full_message) { object.kind_of?(klass) } + end + end + + # Passes if object.respond_to?(method) is true. + public + def assert_respond_to(object, method, message="") + _wrap_assertion do + assert(method.kind_of?(Symbol) || method.kind_of?(String), "The method argument to #assert_respond_to should be specified as a Symbol or a String.") + full_message = build_message(message, object, object.class, method) do |arg1, arg2, arg3| + "<#{arg1}>\n" + + "of type <#{arg2}>\n" + + "expected to respond_to?<#{arg3}>" + end + assert_block(full_message) { object.respond_to?(method) } + end + end + + # Passes if string =~ pattern. + public + def assert_match(pattern, string, message="") + _wrap_assertion do + full_message = build_message(message, string, pattern) do |arg1, arg2| + "<#{arg1}> expected to be =~\n" + + "<#{arg2}>" + end + assert_block(full_message) { string =~ pattern } + end + end + + # Passes if actual.equal?(expected) (i.e. they are the + # same instance). + public + def assert_same(expected, actual, message="") + full_message = build_message(message, expected, expected.__id__, actual, actual.__id__) do |arg1, arg2, arg3, arg4| + "<#{arg1}:#{arg2}> expected to be equal? to\n" + + "<#{arg3}:#{arg4}>" + end + assert_block(full_message) { actual.equal?(expected) } + end + + # Compares the two objects based on the passed + # operator. Passes if object1.send(operator, object2) is + # true. + public + def assert_operator(object1, operator, object2, message="") + full_message = build_message(message, object1, operator, object2) do |arg1, arg2, arg3| + "<#{arg1}> expected to be\n" + + "#{arg2}\n" + + "<#{arg3}>" + end + assert_block(full_message) { object1.send(operator, object2) } + end + + # Passes if block does not raise an exception. + public + def assert_nothing_raised(*args) + _wrap_assertion do + message = "" + if (!args[-1].instance_of?(Class)) + message = args.pop + end + begin + yield + rescue Exception => thrown_exception + if (args.empty? || args.include?(thrown_exception.class)) + full_message = build_message(message, thrown_exception) do |arg1| + "Exception raised:\n" + + arg1 + end + flunk(full_message) + else + raise thrown_exception.class, thrown_exception.message, thrown_exception.backtrace + end + end + nil + end + end + + # Always fails. + public + def flunk(message="") + assert(false, message) + end + + # Passes if !actual.equal?(expected). + public + def assert_not_same(expected, actual, message="") + full_message = build_message(message, expected, expected.__id__, actual, actual.__id__) do |arg1, arg2, arg3, arg4| + "<#{arg1}:#{arg2}> expected to not be equal? to\n" + + "<#{arg3}:#{arg4}>" + end + assert_block(full_message) { !actual.equal?(expected) } + end + + # Passes if expected != actual. + public + def assert_not_equal(expected, actual, message="") + full_message = build_message(message, expected, actual) do |arg1, arg2| + "<#{arg1}> expected to be != to\n" + + "<#{arg2}>" + end + assert_block(full_message) { expected != actual } + end + + # Passes if !object.nil?. + public + def assert_not_nil(object, message="") + full_message = build_message(message, object) do |arg| + "<#{arg}> expected to not be nil" + end + assert_block(full_message) { !object.nil? } + end + + # Passes if string !~ regularExpression. + public + def assert_no_match(regexp, string, message="") + _wrap_assertion do + assert_instance_of(Regexp, regexp, "The first argument to assert_does_not_match should be a Regexp.") + full_message = build_message(message, regexp.source, string) do |arg1, arg2| + "</#{arg1}/> expected to not match\n" + + " <#{arg2}>" + end + assert_block(full_message) { regexp !~ string } + end + end + + # Passes if block throws symbol. + public + def assert_throws(expected_symbol, message="", &proc) + _wrap_assertion do + assert_instance_of(Symbol, expected_symbol, "assert_throws expects the symbol that should be thrown for its first argument") + assert(block_given?, "Should have passed a block to assert_throws") + caught = true + begin + catch(expected_symbol) do + proc.call + caught = false + end + full_message = build_message(message, expected_symbol) do |arg| + "<:#{arg}> should have been thrown" + end + assert(caught, full_message) + rescue NameError => name_error + if ( name_error.message !~ /^uncaught throw `(.+)'$/ ) #` + raise name_error + end + full_message = build_message(message, expected_symbol, $1) do |arg1, arg2| + "<:#{arg1}> expected to be thrown but\n" + + "<:#{arg2}> was thrown" + end + flunk(full_message) + end + end + end + + # Passes if block does not throw anything. + 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 NameError => name_error + if (name_error.message !~ /^uncaught throw `(.+)'$/ ) #` + raise name_error + end + full_message = build_message(message, $1) do |arg| + "<:#{arg}> was thrown when nothing was expected" + end + flunk(full_message) + end + full_message = build_message(message) { || "Expected nothing to be thrown" } + assert(true, full_message) + end + end + + # Passes if expected_float and actual_float are equal + # within delta tolerance. + 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, expected_float, actual_float, delta) do |arg1, arg2, arg3| + "<#{arg1}> and\n" + + "<#{arg2}> expected to be within\n" + + "<#{arg3}> of each other" + end + assert_block(full_message) { (expected_float.to_f - actual_float.to_f).abs <= delta.to_f } + end + end + + # Passes if the method sent returns a true value. + 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, send_array[0], send_array[1], send_array[2..-1]) do |arg1, arg2, arg3| + "<#{arg1}> expected to respond to\n" + + "<#{arg2}(#{arg3})> with true" + end + assert_block(full_message) { send_array[0].__send__(send_array[1], *send_array[2..-1]) } + end + end + + public + def build_message(message, *arguments, &block) # :nodoc: + return AssertionMessage.new(message.to_s, arguments, block) + 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. + private + def add_assertion + end + + class AssertionMessage # :nodoc: all + def self.convert(object) + case object + when String + return object + when Symbol + return object.to_s + when Regexp + return "/#{object.source}/" + when Exception + return "Class: <#{object.class}>\n" + + "Message: <#{object.message}>\n" + + "---Backtrace---\n" + + object.backtrace.join("\n") + "\n" + + "---------------" + else + return object.inspect + end + end + + def initialize(message, parameters, block) + @message = message + @parameters = parameters + @block = block + end + + def to_s + message_parts = [] + if (@message != nil && @message != "") + if (@message !~ /\.$/) + @message << "." + end + message_parts << @message + end + @parameters = @parameters.collect { + | parameter | + self.class.convert(parameter) + } + message_parts << @block.call(*@parameters) + return message_parts.join("\n") + 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..46c6663d86 --- /dev/null +++ b/lib/test/unit/error.rb @@ -0,0 +1,68 @@ +# :nodoc: +# +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +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 + attr_reader(:location, :exception) + + SINGLE_CHARACTER = 'E' + + # Creates a new Error with the given location and + # exception. + def initialize(location, exception) + @location = location + @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 + "#{@location}:\n#{message}" + end + + # Returns a verbose version of the error description. + def long_display + backtrace = self.class.filter(@exception.backtrace).join("\n ") + "Error!!!\n#{short_display}\n #{backtrace}" + end + + # Overridden to return long_display. + def to_s + long_display + end + + SEPARATOR_PATTERN = '[\\\/:]' + def self.filter(backtrace) # :nodoc: + @test_unit_patterns ||= $:.collect { + | path | + /^#{Regexp.escape(path)}#{SEPARATOR_PATTERN}test#{SEPARATOR_PATTERN}unit#{SEPARATOR_PATTERN}/ + }.push(/#{SEPARATOR_PATTERN}test#{SEPARATOR_PATTERN}unit\.rb/) + + return backtrace.delete_if { + | line | + @test_unit_patterns.detect { + | pattern | + line =~ pattern + } + } + end + end + end +end diff --git a/lib/test/unit/failure.rb b/lib/test/unit/failure.rb new file mode 100644 index 0000000000..6817d8f3bf --- /dev/null +++ b/lib/test/unit/failure.rb @@ -0,0 +1,45 @@ +# :nodoc: +# +# 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(:location, :message) + + SINGLE_CHARACTER = 'F' + + # Creates a new Failure with the given location and + # message. + def initialize(location, message) + @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 + "#{@location}:\n#{@message}" + end + + # Returns a verbose version of the error description. + def long_display + "Failure!!!\n#{short_display}" + 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..2ccf192906 --- /dev/null +++ b/lib/test/unit/testcase.rb @@ -0,0 +1,152 @@ +# :nodoc: +# +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 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' + +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 + + attr_reader :method_name + + STARTED = name + "::STARTED" + FINISHED = name + "::FINISHED" + + # Creates a new instance of the fixture for running the + # test represented by test_method_name. + def initialize(test_method_name) + if ((!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) + tests = method_names.delete_if { |method_name| method_name !~ /^test.+/ } + suite = TestSuite.new(name) + tests.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 StandardError, ScriptError + add_error($!) + ensure + begin + teardown + rescue AssertionFailedError => e + add_failure(e.message, e.backtrace) + rescue StandardError, ScriptError + 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 + assertions_pattern = /[^A-Za-z_]assertions\.rb:/ + if (all_locations.detect { |entry| entry =~ assertions_pattern }) + all_locations.shift + until (all_locations[0] =~ assertions_pattern || all_locations.empty?) + all_locations.shift + end + location = all_locations.detect { |entry| entry !~ assertions_pattern } + else + location = all_locations[0] + end + location = location[/^.+:\d+/] + @_result.add_failure(Failure.new("#{name} [#{location}]", 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 + + # Overriden to return #name. + def to_s + name + end + end + end +end diff --git a/lib/test/unit/testresult.rb b/lib/test/unit/testresult.rb new file mode 100644 index 0000000000..b1bfbf72e4 --- /dev/null +++ b/lib/test/unit/testresult.rb @@ -0,0 +1,81 @@ +# :nodoc: +# +# 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..e3a164cb6e --- /dev/null +++ b/lib/test/unit/testsuite.rb @@ -0,0 +1,64 @@ +# :nodoc: +# +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 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.sort { |test1, test2| test1.name <=> test2.name }.each do |test| + test.run(result, &progress_block) + end + yield(FINISHED, name) + end + + # Adds the test to the suite. + def <<(test) + @tests << 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 + + # Overriden to return the name given the suite at + # creation. + def to_s + @name + 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..0a5e264006 --- /dev/null +++ b/lib/test/unit/ui/console/testrunner.rb @@ -0,0 +1,135 @@ +# :nodoc: +# +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 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 + + SILENT = 0 + PROGRESS_ONLY = 1 + NORMAL = 2 + VERBOSE = 3 + + # Creates a new TestRunner and runs the suite. + def self.run(suite, output_level=NORMAL) + return new(suite, output_level).start + end + + # 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 + # STDERR. + 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) + 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)) + end + + def output_single(something, level=NORMAL) + @io.write(something) if (output?(level)) + 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..8b82ec634d --- /dev/null +++ b/lib/test/unit/ui/fox/testrunner.rb @@ -0,0 +1,270 @@ +# :nodoc: +# +# 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 and runs the suite. + def self.run(suite) + new(suite).start + end + + # Creates a new TestRunner for running the passed + # suite. + def initialize(suite) + if (suite.respond_to?(:suite)) + @suite = suite.suite + else + @suite = suite + end + + @red = false + end + + # Begins the test run. + def start + setup_ui + setup_mediator + attach_to_mediator + start_ui + 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: + 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..229df3e358 --- /dev/null +++ b/lib/test/unit/ui/gtk/testrunner.rb @@ -0,0 +1,389 @@ +# :nodoc: +# +# 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 and runs the suite. + def self.run(suite) + new(suite).start + + end + + # Creates a new TestRunner for running the passed + # suite. + def initialize(suite) + if (suite.respond_to?(:suite)) + @suite = suite.suite + else + @suite = suite + end + end + + # Begins the test run. + def start + setup_mediator + setup_ui + attach_to_mediator + start_ui + 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) { @mediator.run_suite } + @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: + timer = Gtk::timeout_add(0) { + Gtk::timeout_remove(timer) + @mediator.run_suite + } + Gtk.main + 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.longDisplay) + end + + def raw_show_fault(string) # :nodoc: + faultDetailLabel.set_text(string) + outerDetailSubPanel.queue_resize + end + + def clear_fault # :nodoc: + raw_show_fault("") + end + + def result_changed(result) # :nodoc: + test_progress_bar.set_value(test_progress_bar.get_value + 1) + + 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: + 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.set_text(string) + end + + def setup_ui # :nodoc: + main_window.signal_connect("destroy", nil) { 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, 50) + @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-*-*-*-*-*-*") + style.set_font(font) + @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/testrunnermediator.rb b/lib/test/unit/ui/testrunnermediator.rb new file mode 100644 index 0000000000..41c77dc7a9 --- /dev/null +++ b/lib/test/unit/ui/testrunnermediator.rb @@ -0,0 +1,75 @@ +# :nodoc: +# +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +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 + + @@run = false + + # Returns true if any TestRunnerMediator instances + # have been run. + def self.run? + return @@run + end + + # 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 + @@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..69811608c2 --- /dev/null +++ b/lib/test/unit/ui/testrunnerutilities.rb @@ -0,0 +1,36 @@ +# :nodoc: +# +# Author:: Nathaniel Talbott. +# Copyright:: Copyright (c) 2000-2002 Nathaniel Talbott. All rights reserved. +# License:: Ruby license. + +module Test + module Unit + module UI + + # 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 + + # 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/util/observable.rb b/lib/test/unit/util/observable.rb new file mode 100644 index 0000000000..f5066d3425 --- /dev/null +++ b/lib/test/unit/util/observable.rb @@ -0,0 +1,90 @@ +# :nodoc: +# +# 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..fea66d7671 --- /dev/null +++ b/lib/test/unit/util/procwrapper.rb @@ -0,0 +1,48 @@ +# :nodoc: +# +# 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 |