#
# = dev-utils/test.rb
#
# _Planned_ functionality to support unit testing. Nothing here at the moment.
#
=begin
#
# = dev-utils/test.rb
#
# Testing utilities DevUtils::Test.{load_tests,load_data} and various project directory access
# methods are implemented here. See DevUtils::Test for documentation.
#
require 'test/unit'
require 'test/unit/ui/console/testrunner'
require 'optparse'
require 'ostruct'
module DevUtils
#
# ...
#
class Test
#
# Loads all unit test files matching the arguments given in the sense described below.
# A typical use for this is:
#
# DevUtils::Test.load_tests(__FILE__, *ARGV)
#
# That will load all 'tc_*.rb' files in or below the directory in which the calling
# program is located. If any arguments are provides, they are substrings used to
# restrict the tests that are run, giving the user an easy way to select a unit test.
#
# For example, if the arguments "str" and "num" are given, then for any test file to be
# loaded, its filename must contain either or both of those substrings. It's a hassle
# for the user to type the whole filename, so this makes test selection more practical.
#
# I'll experiment with accepting other arguments like "-q" and "-v" to mean "quiet" and
# "verbose" respectively. Verbose is default, meaning each test filename is relayed to
# STDERR to inform the user what's going on. Other arguments: "-h" for help (to explain
# this whole thing), and "-s" for separate running of each test, instead of running all
# tests in one big suite.
#
# The ultimate purpose of this is to implement a unit test suite runner in a
# reusable fashion, rather than coding one up for each project. Therefore, the file
# test/TEST.rb, for instance, would consist of:
#
# require 'rubygems'
# require 'dev-utils/test'
#
# DevUtils::Test.load_tests(__FILE__, ARGV)
#
# The user can then simply run:
#
# ruby test/TEST.rb -h # get help on options
# ruby test/TEST.rb -q # quiet operation
# ruby test/TEST.rb -s # separate suite per test file
# ruby test/TEST.rb str io # only tests that have "str" or "io" in the filename
#
# The convention of naming test files "tc_XYZ.rb" must be followed for this to work.
#
def self.load_tests(path, args)
if self.project_dir.nil?
# This is the usual case. We derive the project directory from the path. The fact
# that @project_dir is nil means that all defaults have been accepted. Therefore we
# must be in the +test+ directory.
path = File.expand_path(path)
unless FileTest.exist?(path)
raise RuntimeError, "Internal state problem: path '#{path}' doesn't exist"
end
test_dir = File.dirname(path)
project_dir = File.dirname(test_dir)
self.project_dir = project_dir
_assert_path self.lib_dir
_assert_path self.test_dir
# We don't assert the existence of the test data directory because it's not
# mandatory.
else
# In this case, the project directory has been specified prior to calling this
# method. We will simply use the values provided or derived.
end
# The main things to do in this method: modify the load path; parse the arguments
# given, decide which test files to load, and load them.
# 1. Handle the load path.
$:.unshift self.lib_dir
# 2. Parse the arguments given to this method.
opts = _parse_options(args)
Dir.chdir(self.test_dir) do
# 3. Decide which test files to load.
test_files = Dir['**/tc_*.rb']
unless opts.patterns.empty?
test_files = test_files.select { |fn| opts.patterns.any? { |str| fn.index(str) } }
end
# 4. Load them, using separate test suites if requested.
test_files.each do |test_file|
STDERR.puts "Loading test file: #{test_file}" if opts.mode == :verbose
require test_file
# TODO: take care of separate test suites, and respect --very-quiet.
end
if opts.separate
test_cases = []
ObjectSpace.each_object(Class) do |klass|
test_cases << klass if klass < ::Test::Unit::TestCase
end
test_cases.sort_by { |c| c.name }.each do |klass|
STDERR.puts "Running test class #{klass}" if opts.mode == :verbose
output_level =
case opts.mode
when :veryquiet then ::Test::Unit::UI::SILENT
when :quiet then ::Test::Unit::UI::PROGRESS_ONLY
when :verbose then ::Test::Unit::UI::NORMAL
end
runner = ::Test::Unit::UI::Console::TestRunner.new(klass.suite, output_level)
runner.start
end
else
# We let all test cases run in a single suite on exit.
end
end
end # def load_tests
#
# This method is aimed at allowing unit tests to access data resources easily. Those
# resources are files buried in a test data directory, so they do not clutter the
# unit tests themselves.
#
# You access a test data resource by its path relative to the test data directory. The
# unit test runs blissfully unaware of where that directory is. See ... for a
# demonstration. (TODO: write a high-level document that ties this all together.)
#
# Once the test data resource is located, the default is to load the file and return its
# contents as a string. You can get different behaviour by specifying the XXX
#
# The default is to return a String. The +flags+ provided affect this:
# * :string means return a String
# * :file means return a File
# * :pathname means return a Pathname
# * :copy means return a _copy_ of the file (:file and
# :pathname only)
# * :write means resource is intended to be _written_, therefore don't complain
# if it doesn't exist (:file and :pathname only)
#
# An error of some sort is raised if the file doesn't exist, unless :write is
# specified.
#
def self.load_data(resource_path, *flags)
if self.data_directory.nil?
raise RuntimeError,
"Test data directory hasn't been defined. Use #{self}.data_directory="
end
end
def self.set_project_dir_from(path, project_name)
path = File.expand_path(path)
_assert_path path
# The project dir is everything in the path up to and including the first instance of the
# project name.
if path =~ /\A(.*?#{project_name}).*\Z/
self.project_dir = $1
else
raise RuntimeError, "Project name '#{project_name}' not found in path '#{path}'"
end
end
def self.project_dir=(path)
path = File.expand_path(path)
_assert_path path
@project_dir = path
end
def self.project_dir
@project_dir
end
def self.lib_dir=(path)
raise RuntimeError, "Project directory not set" unless project_dir
_assert_path path
@lib_dir = File.join(project_dir, path)
end
def self.lib_dir
return @lib_dir if @lib_dir
File.join(project_dir, 'lib')
end
def self.test_dir=(path)
raise RuntimeError, "Project directory not set" unless project_dir
_assert_path path
@test_dir = File.join(project_dir, path)
end
def self.test_dir
return @test_dir if @test_dir
File.join(project_dir, 'test')
end
def self.data_dir=(path)
raise RuntimeError, "Project directory not set" unless project_dir
_assert_path path
@data_dir = File.join(project_dir, path)
end
def self.data_dir
return @data_dir if @data_dir
File.join(project_dir, 'data')
end
private
def self._assert_path(path)
unless FileTest.exist?(path)
raise RuntimeError, "Path doesn't exist: #{path}"
end
end
#
# Parse the given arguments for the test loader. Return an OpenStruct with 'mode',
# 'separate', and 'patterns' attributes.
#
def self._parse_options(args)
opts = OpenStruct.new
opts.mode = :verbose
opts.separate = false
opts.patterns = []
parser = OptionParser.new do |op|
op.banner =<