# # = 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 =<