add the code

This commit is contained in:
Johan Holmberg 2009-03-24 08:41:36 +00:00 committed by holmberg556
parent 6b3ed73ae2
commit 8245c819bd
11 changed files with 1686 additions and 0 deletions

330
bin/cmdtest.rb Executable file

@ -0,0 +1,330 @@
#!/usr/bin/ruby
#----------------------------------------------------------------------
# cmdtest.rb
#----------------------------------------------------------------------
# Copyright 2002-2009 Johan Holmberg.
#----------------------------------------------------------------------
# This file is part of "cmdtest".
#
# "cmdtest" is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# "cmdtest" is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with "cmdtest". If not, see <http://www.gnu.org/licenses/>.
#----------------------------------------------------------------------
# This is the main program of "cmdtest".
# It reads a number of "CMDTEST_*.rb" files and executes the testcases
# found in the files. The result can be reported in different ways.
# Most of the testing logic is found in the library files "cmdtest/*.rb".
top_dir = File.dirname(File.dirname(__FILE__))
lib_dir = File.join(File.expand_path(top_dir), "lib")
$:.unshift(lib_dir) if File.directory?(File.join(lib_dir, "cmdtest"))
require "cmdtest/baselogger"
require "cmdtest/consolelogger"
require "cmdtest/junitlogger"
require "cmdtest/testcase"
require "cmdtest/workdir"
require "cmdtest/util"
require "set"
require "stringio"
require "fileutils"
require "find"
require "rbconfig"
module Cmdtest
#----------------------------------------------------------------------
class TestMethod
def initialize(test_method, test_class, runner)
@test_method, @test_class, @runner = test_method, test_class, runner
end
def run
@runner.notify("testmethod", @test_method) do
obj = @test_class.testcase_class.new(self, @runner)
obj._work_dir.chdir do
obj.setup
begin
obj.send(@test_method)
@runner.assert_success
rescue Cmdtest::AssertFailed => e
@runner.assert_failure(e.message)
rescue => e
io = StringIO.new
io.puts "CAUGHT EXCEPTION:"
io.puts " " + e.message + " (#{e.class})"
io.puts "BACKTRACE:"
io.puts e.backtrace.map {|line| " " + line }
@runner.assert_error(io.string)
end
obj.teardown
end
end
end
end
#----------------------------------------------------------------------
class TestClass
attr_reader :testcase_class
def initialize(testcase_class, file, runner)
@testcase_class, @file, @runner = testcase_class, file, runner
end
def run
@runner.notify("testclass", @testcase_class) do
get_test_methods.each do |method|
TestMethod.new(method, self, @runner).run
end
end
end
def get_test_methods
@testcase_class.public_instance_methods(false).sort.select do |method|
in_list = @runner.opts.tests.empty? || @runner.opts.tests.include?(method)
method =~ /^test_/ && in_list
end
end
end
#----------------------------------------------------------------------
class TestFile
def initialize(file)
@file = file
end
def run(runner)
@runner = runner
@runner.notify("testfile", @file) do
testcase_classes = Cmdtest::Testcase.new_subclasses do
Kernel.load(@file, true)
end
for testcase_class in testcase_classes
test_class = TestClass.new(testcase_class, self, @runner)
if ! test_class.get_test_methods.empty?
test_class.run
end
end
end
end
end
#----------------------------------------------------------------------
class Runner
attr_reader :opts
def initialize(project_dir, opts)
@project_dir = project_dir
@listeners = []
@opts = opts
end
def add_listener(listener)
@listeners << listener
end
def notify_once(method, *args)
for listener in @listeners
listener.send(method, *args)
end
end
def notify(method, *args)
if block_given?
notify_once(method + "_begin", *args)
yield
notify_once(method + "_end", *args)
else
notify_once(method, *args)
end
end
def run
ENV["PATH"] = Dir.pwd + Config::CONFIG["PATH_SEPARATOR"] + ENV["PATH"]
@n_assert_failures = 0
@n_assert_errors = 0
@n_assert_successes = 0
notify("testsuite") do
for test_file in @project_dir.test_files
test_file.run(self)
end
end
end
def assert_success
@n_assert_successes += 1
end
def assert_failure(str)
@n_assert_failures += 1
notify("assert_failure", str)
end
def assert_error(str)
@n_assert_errors += 1
notify("assert_error", str)
end
end
#----------------------------------------------------------------------
class ProjectDir
def initialize(argv)
@argv = argv
end
def test_files
if ! @argv.empty?
files = _expand_files_or_dirs(@argv)
if files.empty?
puts "ERROR: no files given"
exit 1
end
return files
end
try = Dir.glob("t/CMDTEST_*.rb")
return _test_files(try) if ! try.empty?
try = Dir.glob("CMDTEST_*.rb")
return _test_files(try) if ! try.empty?
puts "ERROR: no CMDTEST_*.rb files found"
exit 1
end
private
def _test_files(files)
files.map {|file| TestFile.new(file) }
end
def _expand_files_or_dirs(argv)
files = []
for arg in @argv
if File.file?(arg)
if File.basename(arg) =~ /^.*\.rb$/
files << TestFile.new(arg)
else
puts "ERROR: illegal file: #{arg}"
exit(1)
end
elsif File.directory?(arg)
Dir.foreach(arg) do |entry|
path = File.join(arg,entry)
next unless File.file?(path)
next unless entry =~ /^CMDTEST_.*\.rb$/
files << TestFile.new(path)
end
else
puts "ERROR: unknown file: #{arg}"
end
end
return files
end
end
#----------------------------------------------------------------------
class Main
attr_reader :tests, :quiet, :verbose, :fast, :ruby_s
def initialize
@tests = []
@quiet = false
@verbose = false
@fast = false
@xml = nil
@ruby_s = false
_update_cmdtest_level
end
def run
while ! ARGV.empty? && ARGV[0] =~ /^-/
opt = ARGV.shift
case opt
when /^--test=(.*)/
@tests << $1
when /^--quiet$/
@quiet = true
when /^--verbose$/
@verbose = true
when /^--fast$/
@fast = true
when /^--xml=(.+)$/
@xml = $1
when /^--ruby_s$/
@ruby_s = true
when /^--help$/, /^-h$/
puts
_show_options
puts
exit 0
else
puts "ERROR: unknown option: #{opt}"
puts
_show_options
puts
exit 1
end
end
Util.opts = self
@project_dir = ProjectDir.new(ARGV)
@runner = Runner.new(@project_dir, self)
logger = ConsoleLogger.new(self)
@runner.add_listener(logger)
if @xml
@runner.add_listener(JunitLogger.new(self, @xml))
end
@runner.run
end
private
def _update_cmdtest_level
$cmdtest_level = (ENV["CMDTEST_LEVEL"] || "0").to_i + 1
ENV["CMDTEST_LEVEL"] = $cmdtest_level.to_s
end
def _show_options
puts " --help show this help"
puts " --quiet be more quiet"
puts " --verbose be more verbose"
puts " --fast run fast without waiting for unique mtime:s"
puts " --test=NAME only run named test"
end
end
end
#----------------------------------------------------------------------
Cmdtest::Main.new.run
#----------------------------------------------------------------------

94
lib/cmdtest/baselogger.rb Normal file

@ -0,0 +1,94 @@
#----------------------------------------------------------------------
# baselogger.rb
#----------------------------------------------------------------------
# Copyright 2002-2009 Johan Holmberg.
#----------------------------------------------------------------------
# This file is part of "cmdtest".
#
# "cmdtest" is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# "cmdtest" is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with "cmdtest". If not, see <http://www.gnu.org/licenses/>.
#----------------------------------------------------------------------
module Cmdtest
class BaseLogger
@@debug = false
attr_reader :opts
attr_reader :n_suites, :n_files, :n_classes
attr_reader :n_methods, :n_commands, :n_failures, :n_errors
def initialize(opts)
@opts = opts
@n_suites = 0
@n_files = 0
@n_classes = 0
@n_methods = 0
@n_commands = 0
@n_failures = 0
@n_errors = 0
end
def testsuite_begin
p :testsuite_begin if @@debug
@n_suites += 1
end
def testsuite_end
p :testsuite_end if @@debug
end
def testfile_begin(file)
p [:testfile_begin, file] if @@debug
@n_files += 1
end
def testfile_end(file)
p :testfile_end if @@debug
end
def testclass_begin(testcase_class)
p [:testclass_begin, testcase_class] if @@debug
@n_classes += 1
end
def testclass_end(testcase_class)
p :testclass_end if @@debug
end
def testmethod_begin(method)
p [:testmethod_begin, method] if @@debug
@n_methods += 1
end
def testmethod_end(method)
p :testmethod_end if @@debug
end
def cmdline(method, comment)
p :testmethod_end if @@debug
@n_commands += 1
end
def assert_failure
@n_failures += 1
end
def assert_error
@n_errors += 1
end
end
end

83
lib/cmdtest/cmdeffects.rb Normal file

@ -0,0 +1,83 @@
#----------------------------------------------------------------------
# cmdeffects.rb
#----------------------------------------------------------------------
# Copyright 2002-2009 Johan Holmberg.
#----------------------------------------------------------------------
# This file is part of "cmdtest".
#
# "cmdtest" is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# "cmdtest" is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with "cmdtest". If not, see <http://www.gnu.org/licenses/>.
#----------------------------------------------------------------------
require "set"
module Cmdtest
class CmdEffects
attr_reader :stdout, :stderr
def initialize(process_status, stdout, stderr, snapshot_before, snapshot_after)
@process_status = process_status
@stdout = stdout
@stderr = stderr
@before = snapshot_before
@after = snapshot_after
end
def exit_status
@process_status.exitstatus
end
def _select_files
files = @before.files.to_set + @after.files.to_set
files.sort.select do |file|
before = @before.fileinfo(file)
after = @after.fileinfo(file)
yield before, after
end
end
def affected_files
_select_files do |before,after|
((!! before ^ !! after) ||
(before && after && before != after))
end
end
def written_files
_select_files do |before,after|
((! before && after) ||
(before && after && before != after))
end
end
def created_files
_select_files do |before,after|
(! before && after)
end
end
def modified_files
_select_files do |before,after|
(before && after && before != after)
end
end
def removed_files
_select_files do |before,after|
(before && ! after)
end
end
end
end

@ -0,0 +1,82 @@
#----------------------------------------------------------------------
# consolelogger.rb
#----------------------------------------------------------------------
# Copyright 2002-2009 Johan Holmberg.
#----------------------------------------------------------------------
# This file is part of "cmdtest".
#
# "cmdtest" is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# "cmdtest" is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with "cmdtest". If not, see <http://www.gnu.org/licenses/>.
#----------------------------------------------------------------------
require "cmdtest/baselogger"
module Cmdtest
class ConsoleLogger < BaseLogger
def _banner(ch, str)
puts "### " + ch * 40 + " " + str
end
def testfile_begin(file)
super
_banner "=", file if ! opts.quiet
end
def testclass_begin(testcase_class)
super
_banner "-", testcase_class.display_name if ! opts.quiet
end
def testmethod_begin(method)
super
_banner ".", method.to_s if ! opts.quiet
end
def cmdline(cmdline_arg, comment)
super
if opts.verbose
first = comment || "..."
puts "### %s" % [first]
puts "### %s" % [cmdline_arg]
else
first = comment || cmdline_arg
puts "### %s" % [first]
end
end
def assert_failure(str)
super()
puts str.gsub(/^/, "--- ")
end
def assert_error(str)
super()
puts str.gsub(/^/, "--- ")
end
def testsuite_end
super
if ! opts.quiet
puts
puts "%s %d test classes, %d test methods, %d commands, %d errors, %d fatals." % [
n_failures == 0 && n_errors == 0 ? "###" : "---",
n_classes, n_methods, n_commands, n_failures, n_errors
]
puts
end
end
end
end

67
lib/cmdtest/fileinfo.rb Normal file

@ -0,0 +1,67 @@
#----------------------------------------------------------------------
# fileinfo.rb
#----------------------------------------------------------------------
# Copyright 2002-2009 Johan Holmberg.
#----------------------------------------------------------------------
# This file is part of "cmdtest".
#
# "cmdtest" is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# "cmdtest" is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with "cmdtest". If not, see <http://www.gnu.org/licenses/>.
#----------------------------------------------------------------------
require "digest/md5"
module Cmdtest
class FileInfo
attr_reader :stat, :digest
def initialize(path)
@path = path
@stat = File.lstat(path)
if @stat.file?
md5 = Digest::MD5.new
File.open(path) {|f| f.binmode; md5.update(f.read) }
@digest = md5.hexdigest
else
@digest = "a-directory"
end
end
FILE_SUFFIXES = {
"file" => "",
"directory" => "/",
"link" => "@",
}
def display_path
@path + (FILE_SUFFIXES[@stat.ftype] || "?")
end
def ==(other)
stat = other.stat
case
when @stat.file? && stat.file?
(@stat.mtime == stat.mtime &&
@stat.ino == stat.ino &&
@digest == other.digest)
when @stat.directory? && stat.directory?
true
else
false
end
end
end
end

62
lib/cmdtest/fssnapshot.rb Normal file

@ -0,0 +1,62 @@
#----------------------------------------------------------------------
# fssnapshot.rb
#----------------------------------------------------------------------
# Copyright 2002-2009 Johan Holmberg.
#----------------------------------------------------------------------
# This file is part of "cmdtest".
#
# "cmdtest" is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# "cmdtest" is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with "cmdtest". If not, see <http://www.gnu.org/licenses/>.
#----------------------------------------------------------------------
require "cmdtest/fileinfo"
require "cmdtest/util"
require "find"
module Cmdtest
class FsSnapshot
def initialize(dir, ignored_files)
@dir = dir
@ignored_files = ignored_files
@fileinfo_by_path = {}
Util.chdir(@dir) do
Find.find(".") do |path|
next if path == "."
path.sub!(/^\.\//, "")
file_info = FileInfo.new(path)
display_path = file_info.display_path
Find.prune if _ignore_file?(display_path)
@fileinfo_by_path[display_path] = file_info
end
end
end
def _ignore_file?(path)
@ignored_files.any? {|ignored| ignored === path }
end
def files
@fileinfo_by_path.keys.sort.select do |path|
stat = @fileinfo_by_path[path].stat
stat.file? || stat.directory?
end
end
def fileinfo(path)
@fileinfo_by_path[path]
end
end
end

173
lib/cmdtest/junitfile.rb Normal file

@ -0,0 +1,173 @@
#----------------------------------------------------------------------
# junitfile.rb
#----------------------------------------------------------------------
# Copyright 2009 Johan Holmberg.
#----------------------------------------------------------------------
# This file is part of "cmdtest".
#
# "cmdtest" is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# "cmdtest" is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with "cmdtest". If not, see <http://www.gnu.org/licenses/>.
#----------------------------------------------------------------------
module Cmdtest
class JunitFile
#----------
class XmlFile
def initialize(file)
@file = file
@f = File.open(file, "w")
end
def put(str, args=[])
@f.puts str % args.map {|arg| String === arg ? _quote(arg) : arg}
end
def _quote(arg)
arg.gsub(/&/, "&amp;").gsub(/</, "&lt;").gsub(/>/, "&gt;")
end
def close
@f.close
end
end
#----------
class Testcase
def write(f)
f.put ' <testcase classname="%s" name="%s"/>', [
@classname,
@name,
]
end
end
#----------
class OkTestcase < Testcase
def initialize(classname, name)
@classname = classname
@name = name
@message = @type = @text = nil
end
end
#----------
class ErrTestcase < Testcase
def initialize(classname, name, message, type, text)
@classname = classname
@name = name
@message = message
@type = type
@text = text
end
def write(f)
f.put ' <testcase classname="%s" name="%s">', [
@classname,
@name,
]
f.put ' <failure message="%s" type="%s">%s</failure>', [
@message,
@type,
@text,
]
f.put ' </testcase>'
end
end
#----------
class Testsuite
def initialize(package, name)
@package = package
@name = name
@testcases = []
end
def ok_testcase(classname, name)
testcase = OkTestcase.new(classname, name)
@testcases << testcase
testcase
end
def err_testcase(classname, name, message, type, text)
testcase = ErrTestcase.new(classname, name, message, type, text)
@testcases << testcase
testcase
end
def write(f)
f.put ' <testsuite errors="%d" failures="%d" name="%s" package="%s">', [
0,
@testcases.grep(ErrTestcase).size,
@name,
@package,
]
for testcase in @testcases
testcase.write(f)
end
f.put ' </testsuite>'
end
end
#----------
def initialize(file)
@file = file
@testsuites = []
end
def new_testsuite(*args)
testsuite = Testsuite.new(*args)
@testsuites << testsuite
testsuite
end
def write
@f = XmlFile.new(@file)
@f.put '<?xml version="1.0" encoding="UTF-8" ?>'
@f.put '<testsuites>'
for testsuite in @testsuites
testsuite.write(@f)
end
@f.put '</testsuites>'
@f.close
end
end
end
if $0 == __FILE__
jf = Cmdtest::JunitFile.new("jh.xml")
ts = jf.new_testsuite("foo")
ts.ok_testcase("jh.Foo", "test_a")
ts.ok_testcase("jh.Foo", "test_b")
ts.err_testcase("jh.Foo", "test_c", "2 > 1", "assert", "111\n222\n333\n")
ts = jf.new_testsuite("bar")
ts.ok_testcase("jh.Bar", "test_x")
jf.write
end

@ -0,0 +1,93 @@
#----------------------------------------------------------------------
# junitlogger.rb
#----------------------------------------------------------------------
# Copyright 2002-2009 Johan Holmberg.
#----------------------------------------------------------------------
# This file is part of "cmdtest".
#
# "cmdtest" is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# "cmdtest" is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with "cmdtest". If not, see <http://www.gnu.org/licenses/>.
#----------------------------------------------------------------------
require "cmdtest/baselogger"
require "cmdtest/junitfile"
module Cmdtest
class JunitLogger < BaseLogger
def initialize(opts, file)
super(opts)
@file = file
end
def testsuite_begin
@jf = JunitFile.new(@file)
end
def testfile_begin(file)
super
end
def testclass_begin(testcase_class)
super
@testcase_class = testcase_class
@ts = @jf.new_testsuite("CMDTEST", testcase_class.display_name)
end
def testclass_end(testcase_class)
super
end
def testmethod_begin(method)
super
@err_assertions = []
end
def testmethod_end(method)
super
if @err_assertions.size > 0
message = @err_assertions[0].split(/\n/)[0]
type = "assert"
text = @err_assertions.join
@ts.err_testcase(_xml_class, method, message, type, text)
else
@ts.ok_testcase(_xml_class, method)
end
end
def _xml_class
"CMDTEST." + @testcase_class.display_name
end
def cmdline(cmdline_arg, comment)
super
end
def assert_failure(str)
super()
@err_assertions << str
end
def assert_error(str)
super()
@err_assertions << str
end
def testsuite_end
super
@jf.write
end
end
end

515
lib/cmdtest/testcase.rb Normal file

@ -0,0 +1,515 @@
#----------------------------------------------------------------------
# testcase.rb
#----------------------------------------------------------------------
# Copyright 2002-2009 Johan Holmberg.
#----------------------------------------------------------------------
# This file is part of "cmdtest".
#
# "cmdtest" is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# "cmdtest" is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with "cmdtest". If not, see <http://www.gnu.org/licenses/>.
#----------------------------------------------------------------------
require "set"
require "stringio"
module Cmdtest
class AssertFailed < RuntimeError ; end
# Base class for testcases.
# Some attributes and methods are prefixed with an "_" to avoid
# name collisions with user declared variables/methods.
class Testcase
@@_loaded_classes = []
def self.inherited(klass)
@@_loaded_classes << klass
end
def self.new_subclasses
@@_loaded_classes = []
yield
@@_loaded_classes
end
#------------------------------
def self.display_name
to_s.sub(/^.*?::/, "")
end
#------------------------------
def setup
end
def teardown
end
#------------------------------
attr_reader :_work_dir
def initialize(test_method, runner)
@_test_method = test_method
@_runner = runner
@_work_dir = Workdir.new(runner)
@_in_cmd = false
@_comment_str = nil
end
#------------------------------
# Import file into the "workdir" from the outside world.
# The source is found relative to the current directory when "cmdtest"
# was invoked. The target is created inside the "workdir" relative to
# the current directory at the time of the call.
def import_file(src, tgt)
src_path = File.expand_path(src, Workdir::ORIG_CWD)
tgt_path = tgt # rely on CWD
FileUtils.mkdir_p(File.dirname(tgt_path))
FileUtils.cp(src_path, tgt_path)
end
#------------------------------
# Create a file inside the "workdir".
# The content can be specified either as an Array of lines or as
# a string with the content of the whole file.
# The filename is evaluated relative to the current directory at the
# time of the call.
def create_file(filename, lines)
Util.wait_for_new_second
FileUtils.mkdir_p( File.dirname(filename) )
File.open(filename, "w") do |f|
case lines
when Array
f.puts lines
else
f.write lines
end
end
end
#------------------------------
# "touch" a file inside the "workdir".
# The filename is evaluated relative to the current directory at the
# time of the call.
def touch_file(filename)
Util.wait_for_new_second
FileUtils.touch(filename)
end
#------------------------------
# Dont count the specified file when calculating the "side effects"
# of a command.
def ignore_file(file)
@_work_dir.ignore_file(file)
end
#------------------------------
# Dont count the specified file when calculating the "side effects"
# of a command.
def ignore_files(*files)
for file in files.flatten
@_work_dir.ignore_file(file)
end
end
#==============================
# Used in methods invoked from the "cmd" do-block, in methods that
# should be executed *before* the actual command.
def _process_before
yield
end
# Used in methods invoked from the "cmd" do-block, in methods that
# should be executed *after* the actual command.
def _process_after
_delayed_run_cmd
yield
end
def comment(str)
_process_before do
@_comment_str = str
end
end
#------------------------------
def assert(flag, msg=nil)
_process_after do
_assert flag do
msg ? "assertion: #{msg}" : "assertion failed"
end
end
end
#------------------------------
def exit_zero
_process_after do
@_checked_status = true
status = @_effects.exit_status
_assert status == 0 do
"expected zero exit status, got #{status}"
end
end
end
#------------------------------
def exit_nonzero
_process_after do
@_checked_status = true
status = @_effects.exit_status
_assert status != 0 do
"expected nonzero exit status"
end
end
end
#------------------------------
def exit_status(expected_status)
_process_after do
@_checked_status = true
status = @_effects.exit_status
_assert status == expected_status do
"expected #{expected_status} exit status, got #{status}"
end
end
end
#------------------------------
#------------------------------
def _xxx_files(xxx, files)
actual = @_effects.send(xxx)
expected = files.flatten.sort
#p [:xxx_files, xxx, actual, expected]
_assert0 actual == expected do
_format_output(xxx.to_s.gsub(/_/, " ").gsub(/modified/, "changed"),
actual.inspect + "\n",
expected.inspect + "\n")
end
end
#------------------------------
def created_files(*files)
_process_after do
_xxx_files(:created_files, files)
@_checked_files_set << :created
end
end
#------------------------------
def modified_files(*files)
_process_after do
_xxx_files(:modified_files, files)
@_checked_files_set << :modified
end
end
alias :changed_files :modified_files
#------------------------------
def removed_files(*files)
_process_after do
_xxx_files(:removed_files, files)
@_checked_files_set << :removed
end
end
#------------------------------
def written_files(*files)
_process_after do
_xxx_files(:written_files, files)
@_checked_files_set << :created << :modified
end
end
#------------------------------
def affected_files(*files)
_process_after do
_xxx_files(:affected_files, files)
@_checked_files_set << :created << :modified << :removed
end
end
#------------------------------
def _read_file(file)
if File.directory?(file) && RUBY_PLATFORM =~ /mswin32/
:is_directory
else
File.read(file)
end
rescue Errno::ENOENT
:no_such_file
rescue Errno::EISDIR
:is_directory
rescue
:other_error
end
# Assert file equal to specific value.
def file_equal(file, expected)
_file_equal_aux(true, file, expected)
end
def file_not_equal(file, expected)
_file_equal_aux(false, file, expected)
end
def _file_equal_aux(positive, file, expected)
_process_after do
actual = _read_file(file)
case actual
when :no_such_file
_assert false do
"no such file: '#{file}'"
end
when :is_directory
_assert false do
"is a directory: '#{file}'"
end
when :other_error
_assert false do
"error reading file: '#{file}'"
end
else
_xxx_equal("file '#{file}'", positive, actual, expected)
end
end
end
# Assert stdout equal to specific value.
def stdout_equal(expected)
_stdxxx_equal_aux("stdout", true, expected)
end
# Assert stdout not equal to specific value.
def stdout_not_equal(expected)
_stdxxx_equal_aux("stdout", false, expected)
end
# Assert stderr equal to specific value.
def stderr_equal(expected)
_stdxxx_equal_aux("stderr", true, expected)
end
# Assert stderr not equal to specific value.
def stderr_not_equal(expected)
_stdxxx_equal_aux("stderr", false, expected)
end
# helper methods
def _stdxxx_equal_aux(stdxxx, positive, expected)
_process_after do
@_checked[stdxxx] = true
actual = @_effects.send(stdxxx)
_xxx_equal(stdxxx, positive, actual, expected)
end
end
def _xxx_equal(xxx, positive, actual, expected)
_assert0 _output_match(positive, actual, expected) do
_format_output "wrong #{xxx}", actual, expected
end
end
def _output_match(positive, actual, expected)
! positive ^ _output_match_positive(actual, expected)
end
def _output_match_positive(actual, expected)
case expected
when String
expected == actual
when Regexp
expected =~ actual
when Array
actual_lines = _str_as_lines(actual)
expected_lines = _str_or_arr_as_lines(expected)
if actual_lines.size != expected_lines.size
return false
end
actual_lines.each_index do |i|
return false if ! (expected_lines[i] === actual_lines[i])
end
return true
else
raise "error"
end
end
def _str_as_lines(str)
lines = str.split(/\n/, -1)
if lines[-1] == ""
lines.pop
elsif ! lines.empty?
lines[-1] << "<<missing newline>>"
end
return lines
end
def _str_or_arr_as_lines(arg)
case arg
when Array
arg
when String
_str_as_lines(arg)
else
raise "unknown arg: #{arg.inspect}"
end
end
def _indented_lines(prefix, output)
case output
when Array
lines = output
when String
lines = output.split(/\n/, -1)
if lines[-1] == ""
lines.pop
elsif ! lines.empty?
lines[-1] << "<<missing newline>>"
end
when Regexp
lines = [output]
else
raise "unexpected arg: #{output}"
end
if lines == []
lines = ["[[empty]]"]
end
first = true
lines.map do |line|
if first
first = false
prefix + line.to_s + "\n"
else
" " * prefix.size + line.to_s + "\n"
end
end.join("")
end
def _format_output(error, actual, expected)
res = ""
res << "ERROR: #{error}\n"
res << _indented_lines(" actual: ", actual)
res << _indented_lines(" expect: ", expected)
return res
end
def _assert0(flag)
if ! flag
msg = yield
@_io.puts msg
@_nerrors += 1
end
end
def _assert(flag)
if ! flag
msg = yield
@_io.puts "ERROR: " + msg
@_nerrors += 1
end
end
#------------------------------
def _update_hardlinks
return if ! @_runner.opts.fast
@_work_dir.chdir do
FileUtils.mkdir_p("../hardlinks")
Find.find(".") do |path|
st = File.lstat(path)
if st.file?
inode_path = "../hardlinks/%d" % [st.ino]
if ! File.file?(inode_path)
FileUtils.ln(path,inode_path)
end
end
end
end
end
#------------------------------
def _delayed_run_cmd
return if @_cmd_done
@_cmd_done = true
@_runner.notify("cmdline", @_cmdline, @_comment_str)
@_comment_str = nil
@_effects = @_work_dir.run_cmd(@_cmdline)
@_checked_status = false
@_checked = {}
@_checked["stdout"] = false
@_checked["stderr"] = false
@_checked_files_set = Set.new
@_nerrors = 0
@_io = StringIO.new
end
#------------------------------
def cmd(cmdline)
Util.wait_for_new_second
_update_hardlinks
@_cmdline = cmdline
@_cmd_done = false
yield
_delayed_run_cmd
exit_zero if ! @_checked_status
stdout_equal "" if ! @_checked["stdout"]
stderr_equal "" if ! @_checked["stderr"]
created_files [] if ! @_checked_files_set.include?( :created )
modified_files [] if ! @_checked_files_set.include?( :modified )
removed_files [] if ! @_checked_files_set.include?( :removed )
if @_nerrors > 0
str = @_io.string
str = str.gsub(/actual: \S+\/tmp-command\.sh/, "actual: COMMAND.sh")
raise AssertFailed, str
end
end
end
end

67
lib/cmdtest/util.rb Normal file

@ -0,0 +1,67 @@
#----------------------------------------------------------------------
# util.rb
#----------------------------------------------------------------------
# Copyright 2002-2009 Johan Holmberg.
#----------------------------------------------------------------------
# This file is part of "cmdtest".
#
# "cmdtest" is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# "cmdtest" is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with "cmdtest". If not, see <http://www.gnu.org/licenses/>.
#----------------------------------------------------------------------
module Cmdtest
class Util
TRUST_MTIME = (ENV["TRUST_MTIME"] || "1").to_i != 0
@@opts = nil
def self.opts=(opts)
@@opts = opts
end
def self._timestamp_file
File.join(Workdir.tmp_cmdtest_dir, "TIMESTAMP")
end
def self.wait_for_new_second
return if ! TRUST_MTIME || @@opts.fast
loop do
File.open(_timestamp_file, "w") {|f| f.puts Time.now }
break if File.mtime(_timestamp_file) != _newest_file_time
sleep 0.2
end
end
def self._newest_file_time
tnew = Time.at(0)
Find.find(Workdir.tmp_work_dir) do |path|
next if ! File.file?(path)
t = File.mtime(path)
tnew = t > tnew ? t : tnew
end
return tnew
end
def self.chdir(dir)
old_cwd = Dir.pwd
Dir.chdir(dir)
yield
ensure
if Dir.pwd != old_cwd
Dir.chdir(old_cwd)
end
end
end
end

120
lib/cmdtest/workdir.rb Normal file

@ -0,0 +1,120 @@
#----------------------------------------------------------------------
# workdir.rb
#----------------------------------------------------------------------
# Copyright 2002-2009 Johan Holmberg.
#----------------------------------------------------------------------
# This file is part of "cmdtest".
#
# "cmdtest" is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# "cmdtest" is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with "cmdtest". If not, see <http://www.gnu.org/licenses/>.
#----------------------------------------------------------------------
require "cmdtest/fssnapshot"
require "cmdtest/cmdeffects"
require "fileutils"
module Cmdtest
class Workdir
ORIG_CWD = Dir.pwd
def self.tmp_cmdtest_dir
File.join(ORIG_CWD, "tmp-cmdtest-%d" % [$cmdtest_level])
end
def self.tmp_work_dir
File.join(tmp_cmdtest_dir, "workdir")
end
def initialize(runner)
@runner = runner
@dir = Workdir.tmp_work_dir
hardlinkdir = File.join(Workdir.tmp_cmdtest_dir, "hardlinks")
FileUtils.rm_rf(@dir)
FileUtils.rm_rf(hardlinkdir)
FileUtils.mkdir_p(@dir)
@ignored_files = []
end
#--------------------
# called by user (indirectly)
def ignore_file(file)
@ignored_files << file
end
#--------------------
def chdir(&block)
Util.chdir(@dir, &block)
end
def _take_snapshot
FsSnapshot.new(@dir, @ignored_files)
end
def _windows
RUBY_PLATFORM =~ /mswin32/
end
def _shell
_windows ? "cmd /Q /c" : "sh"
end
def _tmp_command_sh
File.join(Workdir.tmp_cmdtest_dir,
_windows ? "tmp-command.bat" : "tmp-command.sh")
end
def _tmp_stdout_log
File.join(Workdir.tmp_cmdtest_dir, "tmp-stdout.log")
end
def _tmp_stderr_log
File.join(Workdir.tmp_cmdtest_dir, "tmp-stderr.log")
end
def _ruby_S(cmdline)
if @runner.opts.ruby_s
if cmdline =~ /ruby/
cmdline
else
cmdline.gsub(/\b(\w+\.rb)\b/, 'ruby -S \1')
end
else
cmdline
end
end
def run_cmd(cmdline)
File.open(_tmp_command_sh, "w") do |f|
f.puts _ruby_S(cmdline)
end
str = "%s %s > %s 2> %s" % [
_shell,
_tmp_command_sh,
_tmp_stdout_log,
_tmp_stderr_log,
]
before = _take_snapshot
ok = system(str)
after = _take_snapshot
CmdEffects.new($?,
File.read(_tmp_stdout_log),
File.read(_tmp_stderr_log),
before, after)
end
end
end