summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorntalbott <ntalbott@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>2003-02-12 03:12:14 +0000
committerntalbott <ntalbott@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>2003-02-12 03:12:14 +0000
commit04f2b8f7bf16ad37d3c28f0e06eb02d63ab6a731 (patch)
tree4354dc87a540b777a7eb6a2276f44be51ec8bebb
parent4a44a6d474cc793d75344b3a0e4c9b0031802689 (diff)
Initial revision
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@3477 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
-rw-r--r--lib/rubyunit.rb8
-rw-r--r--lib/runit/assert.rb71
-rw-r--r--lib/runit/cui/testrunner.rb51
-rw-r--r--lib/runit/error.rb9
-rw-r--r--lib/runit/testcase.rb45
-rw-r--r--lib/runit/testresult.rb44
-rw-r--r--lib/runit/testsuite.rb26
-rw-r--r--lib/runit/topublic.rb8
-rw-r--r--lib/test/unit.rb214
-rw-r--r--lib/test/unit/assertionfailederror.rb14
-rw-r--r--lib/test/unit/assertions.rb394
-rw-r--r--lib/test/unit/error.rb68
-rw-r--r--lib/test/unit/failure.rb45
-rw-r--r--lib/test/unit/testcase.rb152
-rw-r--r--lib/test/unit/testresult.rb81
-rw-r--r--lib/test/unit/testsuite.rb64
-rw-r--r--lib/test/unit/ui/console/testrunner.rb135
-rw-r--r--lib/test/unit/ui/fox/testrunner.rb270
-rw-r--r--lib/test/unit/ui/gtk/testrunner.rb389
-rw-r--r--lib/test/unit/ui/testrunnermediator.rb75
-rw-r--r--lib/test/unit/ui/testrunnerutilities.rb36
-rw-r--r--lib/test/unit/util/observable.rb90
-rw-r--r--lib/test/unit/util/procwrapper.rb48
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