3
# Copyright (C) 2002, 2003 by Martin Pool <mbp@samba.org>
4
# Copyright (C) 2003 by Tim Potter <tpot@samba.org>
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
21
"""comfychair: a Python-based instrument of software torture.
23
Copyright (C) 2002, 2003 by Martin Pool <mbp@samba.org>
24
Copyright (C) 2003 by Tim Potter <tpot@samba.org>
26
This is a test framework designed for testing programs written in
27
Python, or (through a fork/exec interface) any other language.
29
For more information, see the file README.comfychair.
31
To run a test suite based on ComfyChair, just run it as a program.
34
import sys, re, shutil
38
"""A base class for tests. This class defines required functions which
39
can optionally be overridden by subclasses. It also provides some
40
utility functions for"""
44
self.background_pids = []
47
self._save_environment()
48
self.add_cleanup(self.teardown)
51
# --------------------------------------------------
52
# Save and restore directory
53
def _enter_rundir(self):
55
self.basedir = os.getcwd()
56
self.add_cleanup(self._restore_directory)
57
self.rundir = os.path.join(self.basedir,
59
self.__class__.__name__)
60
self.tmpdir = os.path.join(self.rundir, 'tmp')
61
shutil.rmtree(self.rundir, ignore_errors=1)
62
os.makedirs(self.tmpdir)
64
os.environ['LANG'] = 'C'
66
def _restore_directory(self):
68
os.chdir(self.basedir)
70
# --------------------------------------------------
71
# Save and restore environment
72
def _save_environment(self):
74
self._saved_environ = os.environ.copy()
75
self.add_cleanup(self._restore_environment)
77
def _restore_environment(self):
80
os.environ.update(self._saved_environ)
84
"""Set up test fixture."""
88
"""Tear down test fixture."""
96
def add_cleanup(self, c):
97
"""Queue a cleanup to be run when the test is complete."""
98
self._cleanups.append(c)
100
def apply_cleanups(self, debugger):
101
"""Apply cleanup functions and return error code.
104
0 on success; 2 if a KeyboardInterrupt occurs; 1 if any other exception
107
while self._cleanups:
109
apply(self._cleanups.pop())
110
except KeyboardInterrupt:
111
print "interrupted during cleanups"
112
_report_error(self, debugger)
115
print "error during cleanups"
116
_report_error(self, debugger)
120
def fail(self, reason = ""):
121
"""Say the test failed."""
122
raise AssertionError(reason)
125
#############################################################
126
# Requisition methods
128
def require(self, predicate, message):
129
"""Check a predicate for running this test.
131
If the predicate value is not true, the test is skipped with a message explaining
134
raise NotRunError, message
136
def require_root(self):
137
"""Skip this test unless run by root."""
139
self.require(os.getuid() == 0,
140
"must be root to run this test")
142
#############################################################
145
def assert_(self, expr, reason = ""):
147
raise AssertionError(reason)
149
def assert_equal(self, a, b):
151
raise AssertionError("assertEquals failed: %s" % `(a, b)`)
153
def assert_notequal(self, a, b):
155
raise AssertionError("assertNotEqual failed: %s" % `(a, b)`)
157
def assert_re_match(self, pattern, s):
158
"""Assert that a string matches a particular pattern
161
pattern string: regular expression
162
s string: to be matched
165
AssertionError if not matched
167
if not re.match(pattern, s):
168
raise AssertionError("string does not match regexp\n"
170
" re: %s" % (`s`, `pattern`))
172
def assert_re_search(self, pattern, s):
173
"""Assert that a string *contains* a particular pattern
176
pattern string: regular expression
177
s string: to be searched
180
AssertionError if not matched
182
if not re.search(pattern, s):
183
raise AssertionError("string does not contain regexp\n"
185
" re: %s" % (`s`, `pattern`))
188
def assert_no_file(self, filename):
190
assert not os.path.exists(filename), ("file exists but should not: %s" % filename)
193
#############################################################
194
# Methods for running programs
196
def runcmd_background(self, cmd):
198
self.test_log = self.test_log + "Run in background:\n" + `cmd` + "\n"
203
os.execvp("/bin/sh", ["/bin/sh", "-c", cmd])
206
self.test_log = self.test_log + "pid: %d\n" % pid
210
def runcmd(self, cmd, expectedResult = 0):
211
"""Run a command, fail if the command returns an unexpected exit
212
code. Return the output produced."""
213
rc, output, stderr = self.runcmd_unchecked(cmd)
214
if rc != expectedResult:
215
raise AssertionError("""command returned %d; expected %s: \"%s\"
219
%s""" % (rc, expectedResult, cmd, output, stderr))
221
return output, stderr
224
def run_captured(self, cmd):
225
"""Run a command, capturing stdout and stderr.
227
Based in part on popen2.py
229
Returns (waitstatus, stdout, stderr)."""
236
openmode = os.O_WRONLY|os.O_CREAT|os.O_TRUNC
238
outfd = os.open('%d.out' % pid, openmode, 0666)
242
errfd = os.open('%d.err' % pid, openmode, 0666)
246
if isinstance(cmd, types.StringType):
247
cmd = ['/bin/sh', '-c', cmd]
249
os.execvp(cmd[0], cmd)
254
exited_pid, waitstatus = os.waitpid(pid, 0)
255
stdout = open('%d.out' % pid).read()
256
stderr = open('%d.err' % pid).read()
257
return waitstatus, stdout, stderr
260
def runcmd_unchecked(self, cmd, skip_on_noexec = 0):
261
"""Invoke a command; return (exitcode, stdout, stderr)"""
263
waitstatus, stdout, stderr = self.run_captured(cmd)
264
assert not os.WIFSIGNALED(waitstatus), \
265
("%s terminated with signal %d" % (`cmd`, os.WTERMSIG(waitstatus)))
266
rc = os.WEXITSTATUS(waitstatus)
267
self.test_log = self.test_log + ("""Run command: %s
268
Wait status: %#x (exit code %d, signal %d)
272
%s""" % (cmd, waitstatus, os.WEXITSTATUS(waitstatus), os.WTERMSIG(waitstatus),
274
if skip_on_noexec and rc == 127:
275
# Either we could not execute the command or the command
276
# returned exit code 127. According to system(3) we can't
277
# tell the difference.
278
raise NotRunError, "could not execute %s" % `cmd`
279
return rc, stdout, stderr
282
def explain_failure(self, exc_info = None):
288
"""Log a message to the test log. This message is displayed if
289
the test fails, or when the runtests function is invoked with
290
the verbose option."""
291
self.test_log = self.test_log + msg + "\n"
294
class NotRunError(Exception):
295
"""Raised if a test must be skipped because of missing resources"""
296
def __init__(self, value = None):
300
def _report_error(case, debugger):
301
"""Ask the test case to explain failure, and optionally run a debugger
304
case TestCase instance
305
debugger if true, a debugger function to be applied to the traceback
309
print "-----------------------------------------------------------------"
312
traceback.print_exc(file=sys.stdout)
313
case.explain_failure()
314
print "-----------------------------------------------------------------"
321
def runtest(testcase_class, ret, verbose=0, debugger=None, subtest=0):
322
"""Instantiate test class, run it, and catch and report exceptions.
325
testcase_class a class derived from TestCase
326
ret return status, an integer
327
verbose an integer (used as boolean)
328
debugger debugger object to be applied to errors
329
subtest an integer (used as boolean)
335
If subtest is true, then the ordinary information about the
336
test progress is not printed.
339
print "%-30s" % _test_name(testcase_class),
340
def failure_print(message):
343
def failure_print(message):
344
print '[%s %s]' % (_test_name(testcase_class), message)
346
# flush now so that long running tests are easier to follow
352
# run test and sometimes show result
353
obj = testcase_class()
358
except KeyboardInterrupt:
359
failure_print("INTERRUPT")
361
_report_error(obj, debugger)
363
except NotRunError, msg:
364
failure_print("NOTRUN, %s" % msg.value)
366
failure_print("FAIL")
368
_report_error(obj, debugger)
372
ret = obj.apply_cleanups(debugger) or ret
373
# Display log file if we're verbose
374
if ret == 0 and verbose:
375
obj.explain_failure()
379
def runtests(test_list, verbose = 0, debugger = None):
380
"""Run a series of tests.
383
test_list sequence of TestCase classes
384
verbose print more information as testing proceeds
385
debugger debugger object to be applied to errors
388
unix return code: 0 for success, 1 for failures, 2 for test failure
392
for testcase_class in test_list:
394
ret = runtest(testcase_class, ret, verbose=verbose,
396
except KeyboardInterrupt:
402
def _test_name(test_class):
403
"""Return a human-readable name for a test class.
406
return test_class.__name__
412
"""Help for people running tests"""
414
print """%s: software test suite based on ComfyChair
417
To run all tests, just run this program. To run particular tests,
418
list them on the command line.
421
--help show usage message
422
--list list available tests
423
--verbose, -v show more information while running tests
424
--post-mortem, -p enter Python debugger on error
428
def print_list(test_list):
429
"""Show list of available tests"""
430
for test_class in test_list:
431
print " %s" % _test_name(test_class)
434
def main(tests, extra_tests=[]):
435
"""Main entry point for test suites based on ComfyChair.
438
tests Sequence of TestCase subclasses to be run by default.
439
extra_tests Sequence of TestCase subclasses that are available but
442
Test suites should contain this boilerplate:
444
if __name__ == '__main__':
445
comfychair.main(tests)
447
This function handles standard options such as --help and --list, and
448
by default runs all tests in the suggested order.
450
Calls sys.exit() on completion.
458
opts, args = getopt.getopt(argv[1:], 'pv',
459
['help', 'list', 'verbose', 'post-mortem'])
460
for opt, opt_arg in opts:
464
elif opt == '--list':
465
print_list(tests + extra_tests)
467
elif opt == '--verbose' or opt == '-v':
469
elif opt == '--post-mortem' or opt == '-p':
471
debugger = pdb.post_mortem
474
all_tests = tests + extra_tests
477
by_name[_test_name(t)] = t
480
which_tests.append(by_name[name])
484
sys.exit(runtests(which_tests, verbose=opt_verbose,
488
if __name__ == '__main__':