mirror of https://github.com/lukechilds/node.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
581 lines
21 KiB
581 lines
21 KiB
"""
|
|
TestCommon.py: a testing framework for commands and scripts
|
|
with commonly useful error handling
|
|
|
|
The TestCommon module provides a simple, high-level interface for writing
|
|
tests of executable commands and scripts, especially commands and scripts
|
|
that interact with the file system. All methods throw exceptions and
|
|
exit on failure, with useful error messages. This makes a number of
|
|
explicit checks unnecessary, making the test scripts themselves simpler
|
|
to write and easier to read.
|
|
|
|
The TestCommon class is a subclass of the TestCmd class. In essence,
|
|
TestCommon is a wrapper that handles common TestCmd error conditions in
|
|
useful ways. You can use TestCommon directly, or subclass it for your
|
|
program and add additional (or override) methods to tailor it to your
|
|
program's specific needs. Alternatively, the TestCommon class serves
|
|
as a useful example of how to define your own TestCmd subclass.
|
|
|
|
As a subclass of TestCmd, TestCommon provides access to all of the
|
|
variables and methods from the TestCmd module. Consequently, you can
|
|
use any variable or method documented in the TestCmd module without
|
|
having to explicitly import TestCmd.
|
|
|
|
A TestCommon environment object is created via the usual invocation:
|
|
|
|
import TestCommon
|
|
test = TestCommon.TestCommon()
|
|
|
|
You can use all of the TestCmd keyword arguments when instantiating a
|
|
TestCommon object; see the TestCmd documentation for details.
|
|
|
|
Here is an overview of the methods and keyword arguments that are
|
|
provided by the TestCommon class:
|
|
|
|
test.must_be_writable('file1', ['file2', ...])
|
|
|
|
test.must_contain('file', 'required text\n')
|
|
|
|
test.must_contain_all_lines(output, lines, ['title', find])
|
|
|
|
test.must_contain_any_line(output, lines, ['title', find])
|
|
|
|
test.must_exist('file1', ['file2', ...])
|
|
|
|
test.must_match('file', "expected contents\n")
|
|
|
|
test.must_not_be_writable('file1', ['file2', ...])
|
|
|
|
test.must_not_contain('file', 'banned text\n')
|
|
|
|
test.must_not_contain_any_line(output, lines, ['title', find])
|
|
|
|
test.must_not_exist('file1', ['file2', ...])
|
|
|
|
test.run(options = "options to be prepended to arguments",
|
|
stdout = "expected standard output from the program",
|
|
stderr = "expected error output from the program",
|
|
status = expected_status,
|
|
match = match_function)
|
|
|
|
The TestCommon module also provides the following variables
|
|
|
|
TestCommon.python_executable
|
|
TestCommon.exe_suffix
|
|
TestCommon.obj_suffix
|
|
TestCommon.shobj_prefix
|
|
TestCommon.shobj_suffix
|
|
TestCommon.lib_prefix
|
|
TestCommon.lib_suffix
|
|
TestCommon.dll_prefix
|
|
TestCommon.dll_suffix
|
|
|
|
"""
|
|
|
|
# Copyright 2000-2010 Steven Knight
|
|
# This module is free software, and you may redistribute it and/or modify
|
|
# it under the same terms as Python itself, so long as this copyright message
|
|
# and disclaimer are retained in their original form.
|
|
#
|
|
# IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
|
|
# SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
|
|
# THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
|
# DAMAGE.
|
|
#
|
|
# THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
|
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
|
# PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
|
|
# AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
|
|
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
|
|
|
|
__author__ = "Steven Knight <knight at baldmt dot com>"
|
|
__revision__ = "TestCommon.py 0.37.D001 2010/01/11 16:55:50 knight"
|
|
__version__ = "0.37"
|
|
|
|
import copy
|
|
import os
|
|
import os.path
|
|
import stat
|
|
import string
|
|
import sys
|
|
import types
|
|
import UserList
|
|
|
|
from TestCmd import *
|
|
from TestCmd import __all__
|
|
|
|
__all__.extend([ 'TestCommon',
|
|
'exe_suffix',
|
|
'obj_suffix',
|
|
'shobj_prefix',
|
|
'shobj_suffix',
|
|
'lib_prefix',
|
|
'lib_suffix',
|
|
'dll_prefix',
|
|
'dll_suffix',
|
|
])
|
|
|
|
# Variables that describe the prefixes and suffixes on this system.
|
|
if sys.platform == 'win32':
|
|
exe_suffix = '.exe'
|
|
obj_suffix = '.obj'
|
|
shobj_suffix = '.obj'
|
|
shobj_prefix = ''
|
|
lib_prefix = ''
|
|
lib_suffix = '.lib'
|
|
dll_prefix = ''
|
|
dll_suffix = '.dll'
|
|
elif sys.platform == 'cygwin':
|
|
exe_suffix = '.exe'
|
|
obj_suffix = '.o'
|
|
shobj_suffix = '.os'
|
|
shobj_prefix = ''
|
|
lib_prefix = 'lib'
|
|
lib_suffix = '.a'
|
|
dll_prefix = ''
|
|
dll_suffix = '.dll'
|
|
elif string.find(sys.platform, 'irix') != -1:
|
|
exe_suffix = ''
|
|
obj_suffix = '.o'
|
|
shobj_suffix = '.o'
|
|
shobj_prefix = ''
|
|
lib_prefix = 'lib'
|
|
lib_suffix = '.a'
|
|
dll_prefix = 'lib'
|
|
dll_suffix = '.so'
|
|
elif string.find(sys.platform, 'darwin') != -1:
|
|
exe_suffix = ''
|
|
obj_suffix = '.o'
|
|
shobj_suffix = '.os'
|
|
shobj_prefix = ''
|
|
lib_prefix = 'lib'
|
|
lib_suffix = '.a'
|
|
dll_prefix = 'lib'
|
|
dll_suffix = '.dylib'
|
|
elif string.find(sys.platform, 'sunos') != -1:
|
|
exe_suffix = ''
|
|
obj_suffix = '.o'
|
|
shobj_suffix = '.os'
|
|
shobj_prefix = 'so_'
|
|
lib_prefix = 'lib'
|
|
lib_suffix = '.a'
|
|
dll_prefix = 'lib'
|
|
dll_suffix = '.dylib'
|
|
else:
|
|
exe_suffix = ''
|
|
obj_suffix = '.o'
|
|
shobj_suffix = '.os'
|
|
shobj_prefix = ''
|
|
lib_prefix = 'lib'
|
|
lib_suffix = '.a'
|
|
dll_prefix = 'lib'
|
|
dll_suffix = '.so'
|
|
|
|
def is_List(e):
|
|
return type(e) is types.ListType \
|
|
or isinstance(e, UserList.UserList)
|
|
|
|
def is_writable(f):
|
|
mode = os.stat(f)[stat.ST_MODE]
|
|
return mode & stat.S_IWUSR
|
|
|
|
def separate_files(flist):
|
|
existing = []
|
|
missing = []
|
|
for f in flist:
|
|
if os.path.exists(f):
|
|
existing.append(f)
|
|
else:
|
|
missing.append(f)
|
|
return existing, missing
|
|
|
|
if os.name == 'posix':
|
|
def _failed(self, status = 0):
|
|
if self.status is None or status is None:
|
|
return None
|
|
return _status(self) != status
|
|
def _status(self):
|
|
return self.status
|
|
elif os.name == 'nt':
|
|
def _failed(self, status = 0):
|
|
return not (self.status is None or status is None) and \
|
|
self.status != status
|
|
def _status(self):
|
|
return self.status
|
|
|
|
class TestCommon(TestCmd):
|
|
|
|
# Additional methods from the Perl Test::Cmd::Common module
|
|
# that we may wish to add in the future:
|
|
#
|
|
# $test->subdir('subdir', ...);
|
|
#
|
|
# $test->copy('src_file', 'dst_file');
|
|
|
|
def __init__(self, **kw):
|
|
"""Initialize a new TestCommon instance. This involves just
|
|
calling the base class initialization, and then changing directory
|
|
to the workdir.
|
|
"""
|
|
apply(TestCmd.__init__, [self], kw)
|
|
os.chdir(self.workdir)
|
|
|
|
def must_be_writable(self, *files):
|
|
"""Ensures that the specified file(s) exist and are writable.
|
|
An individual file can be specified as a list of directory names,
|
|
in which case the pathname will be constructed by concatenating
|
|
them. Exits FAILED if any of the files does not exist or is
|
|
not writable.
|
|
"""
|
|
files = map(lambda x: is_List(x) and apply(os.path.join, x) or x, files)
|
|
existing, missing = separate_files(files)
|
|
unwritable = filter(lambda x, iw=is_writable: not iw(x), existing)
|
|
if missing:
|
|
print "Missing files: `%s'" % string.join(missing, "', `")
|
|
if unwritable:
|
|
print "Unwritable files: `%s'" % string.join(unwritable, "', `")
|
|
self.fail_test(missing + unwritable)
|
|
|
|
def must_contain(self, file, required, mode = 'rb'):
|
|
"""Ensures that the specified file contains the required text.
|
|
"""
|
|
file_contents = self.read(file, mode)
|
|
contains = (string.find(file_contents, required) != -1)
|
|
if not contains:
|
|
print "File `%s' does not contain required string." % file
|
|
print self.banner('Required string ')
|
|
print required
|
|
print self.banner('%s contents ' % file)
|
|
print file_contents
|
|
self.fail_test(not contains)
|
|
|
|
def must_contain_all_lines(self, output, lines, title=None, find=None):
|
|
"""Ensures that the specified output string (first argument)
|
|
contains all of the specified lines (second argument).
|
|
|
|
An optional third argument can be used to describe the type
|
|
of output being searched, and only shows up in failure output.
|
|
|
|
An optional fourth argument can be used to supply a different
|
|
function, of the form "find(line, output), to use when searching
|
|
for lines in the output.
|
|
"""
|
|
if find is None:
|
|
find = lambda o, l: string.find(o, l) != -1
|
|
missing = []
|
|
for line in lines:
|
|
if not find(output, line):
|
|
missing.append(line)
|
|
|
|
if missing:
|
|
if title is None:
|
|
title = 'output'
|
|
sys.stdout.write("Missing expected lines from %s:\n" % title)
|
|
for line in missing:
|
|
sys.stdout.write(' ' + repr(line) + '\n')
|
|
sys.stdout.write(self.banner(title + ' '))
|
|
sys.stdout.write(output)
|
|
self.fail_test()
|
|
|
|
def must_contain_any_line(self, output, lines, title=None, find=None):
|
|
"""Ensures that the specified output string (first argument)
|
|
contains at least one of the specified lines (second argument).
|
|
|
|
An optional third argument can be used to describe the type
|
|
of output being searched, and only shows up in failure output.
|
|
|
|
An optional fourth argument can be used to supply a different
|
|
function, of the form "find(line, output), to use when searching
|
|
for lines in the output.
|
|
"""
|
|
if find is None:
|
|
find = lambda o, l: string.find(o, l) != -1
|
|
for line in lines:
|
|
if find(output, line):
|
|
return
|
|
|
|
if title is None:
|
|
title = 'output'
|
|
sys.stdout.write("Missing any expected line from %s:\n" % title)
|
|
for line in lines:
|
|
sys.stdout.write(' ' + repr(line) + '\n')
|
|
sys.stdout.write(self.banner(title + ' '))
|
|
sys.stdout.write(output)
|
|
self.fail_test()
|
|
|
|
def must_contain_lines(self, lines, output, title=None):
|
|
# Deprecated; retain for backwards compatibility.
|
|
return self.must_contain_all_lines(output, lines, title)
|
|
|
|
def must_exist(self, *files):
|
|
"""Ensures that the specified file(s) must exist. An individual
|
|
file be specified as a list of directory names, in which case the
|
|
pathname will be constructed by concatenating them. Exits FAILED
|
|
if any of the files does not exist.
|
|
"""
|
|
files = map(lambda x: is_List(x) and apply(os.path.join, x) or x, files)
|
|
missing = filter(lambda x: not os.path.exists(x), files)
|
|
if missing:
|
|
print "Missing files: `%s'" % string.join(missing, "', `")
|
|
self.fail_test(missing)
|
|
|
|
def must_match(self, file, expect, mode = 'rb'):
|
|
"""Matches the contents of the specified file (first argument)
|
|
against the expected contents (second argument). The expected
|
|
contents are a list of lines or a string which will be split
|
|
on newlines.
|
|
"""
|
|
file_contents = self.read(file, mode)
|
|
try:
|
|
self.fail_test(not self.match(file_contents, expect))
|
|
except KeyboardInterrupt:
|
|
raise
|
|
except:
|
|
print "Unexpected contents of `%s'" % file
|
|
self.diff(expect, file_contents, 'contents ')
|
|
raise
|
|
|
|
def must_not_contain(self, file, banned, mode = 'rb'):
|
|
"""Ensures that the specified file doesn't contain the banned text.
|
|
"""
|
|
file_contents = self.read(file, mode)
|
|
contains = (string.find(file_contents, banned) != -1)
|
|
if contains:
|
|
print "File `%s' contains banned string." % file
|
|
print self.banner('Banned string ')
|
|
print banned
|
|
print self.banner('%s contents ' % file)
|
|
print file_contents
|
|
self.fail_test(contains)
|
|
|
|
def must_not_contain_any_line(self, output, lines, title=None, find=None):
|
|
"""Ensures that the specified output string (first argument)
|
|
does not contain any of the specified lines (second argument).
|
|
|
|
An optional third argument can be used to describe the type
|
|
of output being searched, and only shows up in failure output.
|
|
|
|
An optional fourth argument can be used to supply a different
|
|
function, of the form "find(line, output), to use when searching
|
|
for lines in the output.
|
|
"""
|
|
if find is None:
|
|
find = lambda o, l: string.find(o, l) != -1
|
|
unexpected = []
|
|
for line in lines:
|
|
if find(output, line):
|
|
unexpected.append(line)
|
|
|
|
if unexpected:
|
|
if title is None:
|
|
title = 'output'
|
|
sys.stdout.write("Unexpected lines in %s:\n" % title)
|
|
for line in unexpected:
|
|
sys.stdout.write(' ' + repr(line) + '\n')
|
|
sys.stdout.write(self.banner(title + ' '))
|
|
sys.stdout.write(output)
|
|
self.fail_test()
|
|
|
|
def must_not_contain_lines(self, lines, output, title=None):
|
|
return self.must_not_contain_any_line(output, lines, title)
|
|
|
|
def must_not_exist(self, *files):
|
|
"""Ensures that the specified file(s) must not exist.
|
|
An individual file be specified as a list of directory names, in
|
|
which case the pathname will be constructed by concatenating them.
|
|
Exits FAILED if any of the files exists.
|
|
"""
|
|
files = map(lambda x: is_List(x) and apply(os.path.join, x) or x, files)
|
|
existing = filter(os.path.exists, files)
|
|
if existing:
|
|
print "Unexpected files exist: `%s'" % string.join(existing, "', `")
|
|
self.fail_test(existing)
|
|
|
|
|
|
def must_not_be_writable(self, *files):
|
|
"""Ensures that the specified file(s) exist and are not writable.
|
|
An individual file can be specified as a list of directory names,
|
|
in which case the pathname will be constructed by concatenating
|
|
them. Exits FAILED if any of the files does not exist or is
|
|
writable.
|
|
"""
|
|
files = map(lambda x: is_List(x) and apply(os.path.join, x) or x, files)
|
|
existing, missing = separate_files(files)
|
|
writable = filter(is_writable, existing)
|
|
if missing:
|
|
print "Missing files: `%s'" % string.join(missing, "', `")
|
|
if writable:
|
|
print "Writable files: `%s'" % string.join(writable, "', `")
|
|
self.fail_test(missing + writable)
|
|
|
|
def _complete(self, actual_stdout, expected_stdout,
|
|
actual_stderr, expected_stderr, status, match):
|
|
"""
|
|
Post-processes running a subcommand, checking for failure
|
|
status and displaying output appropriately.
|
|
"""
|
|
if _failed(self, status):
|
|
expect = ''
|
|
if status != 0:
|
|
expect = " (expected %s)" % str(status)
|
|
print "%s returned %s%s" % (self.program, str(_status(self)), expect)
|
|
print self.banner('STDOUT ')
|
|
print actual_stdout
|
|
print self.banner('STDERR ')
|
|
print actual_stderr
|
|
self.fail_test()
|
|
if not expected_stdout is None and not match(actual_stdout, expected_stdout):
|
|
self.diff(expected_stdout, actual_stdout, 'STDOUT ')
|
|
if actual_stderr:
|
|
print self.banner('STDERR ')
|
|
print actual_stderr
|
|
self.fail_test()
|
|
if not expected_stderr is None and not match(actual_stderr, expected_stderr):
|
|
print self.banner('STDOUT ')
|
|
print actual_stdout
|
|
self.diff(expected_stderr, actual_stderr, 'STDERR ')
|
|
self.fail_test()
|
|
|
|
def start(self, program = None,
|
|
interpreter = None,
|
|
arguments = None,
|
|
universal_newlines = None,
|
|
**kw):
|
|
"""
|
|
Starts a program or script for the test environment.
|
|
|
|
This handles the "options" keyword argument and exceptions.
|
|
"""
|
|
try:
|
|
options = kw['options']
|
|
del kw['options']
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
if options:
|
|
if arguments is None:
|
|
arguments = options
|
|
else:
|
|
arguments = options + " " + arguments
|
|
try:
|
|
return apply(TestCmd.start,
|
|
(self, program, interpreter, arguments, universal_newlines),
|
|
kw)
|
|
except KeyboardInterrupt:
|
|
raise
|
|
except Exception, e:
|
|
print self.banner('STDOUT ')
|
|
try:
|
|
print self.stdout()
|
|
except IndexError:
|
|
pass
|
|
print self.banner('STDERR ')
|
|
try:
|
|
print self.stderr()
|
|
except IndexError:
|
|
pass
|
|
cmd_args = self.command_args(program, interpreter, arguments)
|
|
sys.stderr.write('Exception trying to execute: %s\n' % cmd_args)
|
|
raise e
|
|
|
|
def finish(self, popen, stdout = None, stderr = '', status = 0, **kw):
|
|
"""
|
|
Finishes and waits for the process being run under control of
|
|
the specified popen argument. Additional arguments are similar
|
|
to those of the run() method:
|
|
|
|
stdout The expected standard output from
|
|
the command. A value of None means
|
|
don't test standard output.
|
|
|
|
stderr The expected error output from
|
|
the command. A value of None means
|
|
don't test error output.
|
|
|
|
status The expected exit status from the
|
|
command. A value of None means don't
|
|
test exit status.
|
|
"""
|
|
apply(TestCmd.finish, (self, popen,), kw)
|
|
match = kw.get('match', self.match)
|
|
self._complete(self.stdout(), stdout,
|
|
self.stderr(), stderr, status, match)
|
|
|
|
def run(self, options = None, arguments = None,
|
|
stdout = None, stderr = '', status = 0, **kw):
|
|
"""Runs the program under test, checking that the test succeeded.
|
|
|
|
The arguments are the same as the base TestCmd.run() method,
|
|
with the addition of:
|
|
|
|
options Extra options that get appended to the beginning
|
|
of the arguments.
|
|
|
|
stdout The expected standard output from
|
|
the command. A value of None means
|
|
don't test standard output.
|
|
|
|
stderr The expected error output from
|
|
the command. A value of None means
|
|
don't test error output.
|
|
|
|
status The expected exit status from the
|
|
command. A value of None means don't
|
|
test exit status.
|
|
|
|
By default, this expects a successful exit (status = 0), does
|
|
not test standard output (stdout = None), and expects that error
|
|
output is empty (stderr = "").
|
|
"""
|
|
if options:
|
|
if arguments is None:
|
|
arguments = options
|
|
else:
|
|
arguments = options + " " + arguments
|
|
kw['arguments'] = arguments
|
|
try:
|
|
match = kw['match']
|
|
del kw['match']
|
|
except KeyError:
|
|
match = self.match
|
|
apply(TestCmd.run, [self], kw)
|
|
self._complete(self.stdout(), stdout,
|
|
self.stderr(), stderr, status, match)
|
|
|
|
def skip_test(self, message="Skipping test.\n"):
|
|
"""Skips a test.
|
|
|
|
Proper test-skipping behavior is dependent on the external
|
|
TESTCOMMON_PASS_SKIPS environment variable. If set, we treat
|
|
the skip as a PASS (exit 0), and otherwise treat it as NO RESULT.
|
|
In either case, we print the specified message as an indication
|
|
that the substance of the test was skipped.
|
|
|
|
(This was originally added to support development under Aegis.
|
|
Technically, skipping a test is a NO RESULT, but Aegis would
|
|
treat that as a test failure and prevent the change from going to
|
|
the next step. Since we ddn't want to force anyone using Aegis
|
|
to have to install absolutely every tool used by the tests, we
|
|
would actually report to Aegis that a skipped test has PASSED
|
|
so that the workflow isn't held up.)
|
|
"""
|
|
if message:
|
|
sys.stdout.write(message)
|
|
sys.stdout.flush()
|
|
pass_skips = os.environ.get('TESTCOMMON_PASS_SKIPS')
|
|
if pass_skips in [None, 0, '0']:
|
|
# skip=1 means skip this function when showing where this
|
|
# result came from. They only care about the line where the
|
|
# script called test.skip_test(), not the line number where
|
|
# we call test.no_result().
|
|
self.no_result(skip=1)
|
|
else:
|
|
# We're under the development directory for this change,
|
|
# so this is an Aegis invocation; pass the test (exit 0).
|
|
self.pass_test()
|
|
|
|
# Local Variables:
|
|
# tab-width:4
|
|
# indent-tabs-mode:nil
|
|
# End:
|
|
# vim: set expandtab tabstop=4 shiftwidth=4:
|
|
|