| 1 | #! /usr/bin/env python | 
|---|
| 2 |  | 
|---|
| 3 | # Copyright (C) 2002, 2003 by Martin Pool <mbp@samba.org> | 
|---|
| 4 | # Copyright (C) 2003 by Tim Potter <tpot@samba.org> | 
|---|
| 5 | # | 
|---|
| 6 | # This program is free software; you can redistribute it and/or | 
|---|
| 7 | # modify it under the terms of the GNU General Public License as | 
|---|
| 8 | # published by the Free Software Foundation; either version 3 of the | 
|---|
| 9 | # License, or (at your option) any later version. | 
|---|
| 10 | # | 
|---|
| 11 | # This program is distributed in the hope that it will be useful, but | 
|---|
| 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of | 
|---|
| 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | 
|---|
| 14 | # General Public License for more details. | 
|---|
| 15 | # | 
|---|
| 16 | # You should have received a copy of the GNU General Public License | 
|---|
| 17 | # along with this program; if not, see <http://www.gnu.org/licenses/>. | 
|---|
| 18 |  | 
|---|
| 19 | """comfychair: a Python-based instrument of software torture. | 
|---|
| 20 |  | 
|---|
| 21 | Copyright (C) 2002, 2003 by Martin Pool <mbp@samba.org> | 
|---|
| 22 | Copyright (C) 2003 by Tim Potter <tpot@samba.org> | 
|---|
| 23 |  | 
|---|
| 24 | This is a test framework designed for testing programs written in | 
|---|
| 25 | Python, or (through a fork/exec interface) any other language. | 
|---|
| 26 |  | 
|---|
| 27 | For more information, see the file README.comfychair. | 
|---|
| 28 |  | 
|---|
| 29 | To run a test suite based on ComfyChair, just run it as a program. | 
|---|
| 30 | """ | 
|---|
| 31 |  | 
|---|
| 32 | import sys, re | 
|---|
| 33 |  | 
|---|
| 34 |  | 
|---|
| 35 | class TestCase: | 
|---|
| 36 | """A base class for tests.  This class defines required functions which | 
|---|
| 37 | can optionally be overridden by subclasses.  It also provides some | 
|---|
| 38 | utility functions for""" | 
|---|
| 39 |  | 
|---|
| 40 | def __init__(self): | 
|---|
| 41 | self.test_log = "" | 
|---|
| 42 | self.background_pids = [] | 
|---|
| 43 | self._cleanups = [] | 
|---|
| 44 | self._enter_rundir() | 
|---|
| 45 | self._save_environment() | 
|---|
| 46 | self.add_cleanup(self.teardown) | 
|---|
| 47 |  | 
|---|
| 48 |  | 
|---|
| 49 | # -------------------------------------------------- | 
|---|
| 50 | # Save and restore directory | 
|---|
| 51 | def _enter_rundir(self): | 
|---|
| 52 | import os | 
|---|
| 53 | self.basedir = os.getcwd() | 
|---|
| 54 | self.add_cleanup(self._restore_directory) | 
|---|
| 55 | self.rundir = os.path.join(self.basedir, | 
|---|
| 56 | 'testtmp', | 
|---|
| 57 | self.__class__.__name__) | 
|---|
| 58 | self.tmpdir = os.path.join(self.rundir, 'tmp') | 
|---|
| 59 | os.system("rm -fr %s" % self.rundir) | 
|---|
| 60 | os.makedirs(self.tmpdir) | 
|---|
| 61 | os.system("mkdir -p %s" % self.rundir) | 
|---|
| 62 | os.chdir(self.rundir) | 
|---|
| 63 |  | 
|---|
| 64 | def _restore_directory(self): | 
|---|
| 65 | import os | 
|---|
| 66 | os.chdir(self.basedir) | 
|---|
| 67 |  | 
|---|
| 68 | # -------------------------------------------------- | 
|---|
| 69 | # Save and restore environment | 
|---|
| 70 | def _save_environment(self): | 
|---|
| 71 | import os | 
|---|
| 72 | self._saved_environ = os.environ.copy() | 
|---|
| 73 | self.add_cleanup(self._restore_environment) | 
|---|
| 74 |  | 
|---|
| 75 | def _restore_environment(self): | 
|---|
| 76 | import os | 
|---|
| 77 | os.environ.clear() | 
|---|
| 78 | os.environ.update(self._saved_environ) | 
|---|
| 79 |  | 
|---|
| 80 |  | 
|---|
| 81 | def setup(self): | 
|---|
| 82 | """Set up test fixture.""" | 
|---|
| 83 | pass | 
|---|
| 84 |  | 
|---|
| 85 | def teardown(self): | 
|---|
| 86 | """Tear down test fixture.""" | 
|---|
| 87 | pass | 
|---|
| 88 |  | 
|---|
| 89 | def runtest(self): | 
|---|
| 90 | """Run the test.""" | 
|---|
| 91 | pass | 
|---|
| 92 |  | 
|---|
| 93 |  | 
|---|
| 94 | def add_cleanup(self, c): | 
|---|
| 95 | """Queue a cleanup to be run when the test is complete.""" | 
|---|
| 96 | self._cleanups.append(c) | 
|---|
| 97 |  | 
|---|
| 98 |  | 
|---|
| 99 | def fail(self, reason = ""): | 
|---|
| 100 | """Say the test failed.""" | 
|---|
| 101 | raise AssertionError(reason) | 
|---|
| 102 |  | 
|---|
| 103 |  | 
|---|
| 104 | ############################################################# | 
|---|
| 105 | # Requisition methods | 
|---|
| 106 |  | 
|---|
| 107 | def require(self, predicate, message): | 
|---|
| 108 | """Check a predicate for running this test. | 
|---|
| 109 |  | 
|---|
| 110 | If the predicate value is not true, the test is skipped with a message explaining | 
|---|
| 111 | why.""" | 
|---|
| 112 | if not predicate: | 
|---|
| 113 | raise NotRunError, message | 
|---|
| 114 |  | 
|---|
| 115 | def require_root(self): | 
|---|
| 116 | """Skip this test unless run by root.""" | 
|---|
| 117 | import os | 
|---|
| 118 | self.require(os.getuid() == 0, | 
|---|
| 119 | "must be root to run this test") | 
|---|
| 120 |  | 
|---|
| 121 | ############################################################# | 
|---|
| 122 | # Assertion methods | 
|---|
| 123 |  | 
|---|
| 124 | def assert_(self, expr, reason = ""): | 
|---|
| 125 | if not expr: | 
|---|
| 126 | raise AssertionError(reason) | 
|---|
| 127 |  | 
|---|
| 128 | def assert_equal(self, a, b): | 
|---|
| 129 | if not a == b: | 
|---|
| 130 | raise AssertionError("assertEquals failed: %s" % `(a, b)`) | 
|---|
| 131 |  | 
|---|
| 132 | def assert_notequal(self, a, b): | 
|---|
| 133 | if a == b: | 
|---|
| 134 | raise AssertionError("assertNotEqual failed: %s" % `(a, b)`) | 
|---|
| 135 |  | 
|---|
| 136 | def assert_re_match(self, pattern, s): | 
|---|
| 137 | """Assert that a string matches a particular pattern | 
|---|
| 138 |  | 
|---|
| 139 | Inputs: | 
|---|
| 140 | pattern      string: regular expression | 
|---|
| 141 | s            string: to be matched | 
|---|
| 142 |  | 
|---|
| 143 | Raises: | 
|---|
| 144 | AssertionError if not matched | 
|---|
| 145 | """ | 
|---|
| 146 | if not re.match(pattern, s): | 
|---|
| 147 | raise AssertionError("string does not match regexp\n" | 
|---|
| 148 | "    string: %s\n" | 
|---|
| 149 | "    re: %s" % (`s`, `pattern`)) | 
|---|
| 150 |  | 
|---|
| 151 | def assert_re_search(self, pattern, s): | 
|---|
| 152 | """Assert that a string *contains* a particular pattern | 
|---|
| 153 |  | 
|---|
| 154 | Inputs: | 
|---|
| 155 | pattern      string: regular expression | 
|---|
| 156 | s            string: to be searched | 
|---|
| 157 |  | 
|---|
| 158 | Raises: | 
|---|
| 159 | AssertionError if not matched | 
|---|
| 160 | """ | 
|---|
| 161 | if not re.search(pattern, s): | 
|---|
| 162 | raise AssertionError("string does not contain regexp\n" | 
|---|
| 163 | "    string: %s\n" | 
|---|
| 164 | "    re: %s" % (`s`, `pattern`)) | 
|---|
| 165 |  | 
|---|
| 166 |  | 
|---|
| 167 | def assert_no_file(self, filename): | 
|---|
| 168 | import os.path | 
|---|
| 169 | assert not os.path.exists(filename), ("file exists but should not: %s" % filename) | 
|---|
| 170 |  | 
|---|
| 171 |  | 
|---|
| 172 | ############################################################# | 
|---|
| 173 | # Methods for running programs | 
|---|
| 174 |  | 
|---|
| 175 | def runcmd_background(self, cmd): | 
|---|
| 176 | import os | 
|---|
| 177 | self.test_log = self.test_log + "Run in background:\n" + `cmd` + "\n" | 
|---|
| 178 | pid = os.fork() | 
|---|
| 179 | if pid == 0: | 
|---|
| 180 | # child | 
|---|
| 181 | try: | 
|---|
| 182 | os.execvp("/bin/sh", ["/bin/sh", "-c", cmd]) | 
|---|
| 183 | finally: | 
|---|
| 184 | os._exit(127) | 
|---|
| 185 | self.test_log = self.test_log + "pid: %d\n" % pid | 
|---|
| 186 | return pid | 
|---|
| 187 |  | 
|---|
| 188 |  | 
|---|
| 189 | def runcmd(self, cmd, expectedResult = 0): | 
|---|
| 190 | """Run a command, fail if the command returns an unexpected exit | 
|---|
| 191 | code.  Return the output produced.""" | 
|---|
| 192 | rc, output, stderr = self.runcmd_unchecked(cmd) | 
|---|
| 193 | if rc != expectedResult: | 
|---|
| 194 | raise AssertionError("""command returned %d; expected %s: \"%s\" | 
|---|
| 195 | stdout: | 
|---|
| 196 | %s | 
|---|
| 197 | stderr: | 
|---|
| 198 | %s""" % (rc, expectedResult, cmd, output, stderr)) | 
|---|
| 199 |  | 
|---|
| 200 | return output, stderr | 
|---|
| 201 |  | 
|---|
| 202 |  | 
|---|
| 203 | def run_captured(self, cmd): | 
|---|
| 204 | """Run a command, capturing stdout and stderr. | 
|---|
| 205 |  | 
|---|
| 206 | Based in part on popen2.py | 
|---|
| 207 |  | 
|---|
| 208 | Returns (waitstatus, stdout, stderr).""" | 
|---|
| 209 | import os, types | 
|---|
| 210 | pid = os.fork() | 
|---|
| 211 | if pid == 0: | 
|---|
| 212 | # child | 
|---|
| 213 | try: | 
|---|
| 214 | pid = os.getpid() | 
|---|
| 215 | openmode = os.O_WRONLY|os.O_CREAT|os.O_TRUNC | 
|---|
| 216 |  | 
|---|
| 217 | outfd = os.open('%d.out' % pid, openmode, 0666) | 
|---|
| 218 | os.dup2(outfd, 1) | 
|---|
| 219 | os.close(outfd) | 
|---|
| 220 |  | 
|---|
| 221 | errfd = os.open('%d.err' % pid, openmode, 0666) | 
|---|
| 222 | os.dup2(errfd, 2) | 
|---|
| 223 | os.close(errfd) | 
|---|
| 224 |  | 
|---|
| 225 | if isinstance(cmd, types.StringType): | 
|---|
| 226 | cmd = ['/bin/sh', '-c', cmd] | 
|---|
| 227 |  | 
|---|
| 228 | os.execvp(cmd[0], cmd) | 
|---|
| 229 | finally: | 
|---|
| 230 | os._exit(127) | 
|---|
| 231 | else: | 
|---|
| 232 | # parent | 
|---|
| 233 | exited_pid, waitstatus = os.waitpid(pid, 0) | 
|---|
| 234 | stdout = open('%d.out' % pid).read() | 
|---|
| 235 | stderr = open('%d.err' % pid).read() | 
|---|
| 236 | return waitstatus, stdout, stderr | 
|---|
| 237 |  | 
|---|
| 238 |  | 
|---|
| 239 | def runcmd_unchecked(self, cmd, skip_on_noexec = 0): | 
|---|
| 240 | """Invoke a command; return (exitcode, stdout, stderr)""" | 
|---|
| 241 | import os | 
|---|
| 242 | waitstatus, stdout, stderr = self.run_captured(cmd) | 
|---|
| 243 | assert not os.WIFSIGNALED(waitstatus), \ | 
|---|
| 244 | ("%s terminated with signal %d" % (`cmd`, os.WTERMSIG(waitstatus))) | 
|---|
| 245 | rc = os.WEXITSTATUS(waitstatus) | 
|---|
| 246 | self.test_log = self.test_log + ("""Run command: %s | 
|---|
| 247 | Wait status: %#x (exit code %d, signal %d) | 
|---|
| 248 | stdout: | 
|---|
| 249 | %s | 
|---|
| 250 | stderr: | 
|---|
| 251 | %s""" % (cmd, waitstatus, os.WEXITSTATUS(waitstatus), os.WTERMSIG(waitstatus), | 
|---|
| 252 | stdout, stderr)) | 
|---|
| 253 | if skip_on_noexec and rc == 127: | 
|---|
| 254 | # Either we could not execute the command or the command | 
|---|
| 255 | # returned exit code 127.  According to system(3) we can't | 
|---|
| 256 | # tell the difference. | 
|---|
| 257 | raise NotRunError, "could not execute %s" % `cmd` | 
|---|
| 258 | return rc, stdout, stderr | 
|---|
| 259 |  | 
|---|
| 260 |  | 
|---|
| 261 | def explain_failure(self, exc_info = None): | 
|---|
| 262 | print "test_log:" | 
|---|
| 263 | print self.test_log | 
|---|
| 264 |  | 
|---|
| 265 |  | 
|---|
| 266 | def log(self, msg): | 
|---|
| 267 | """Log a message to the test log.  This message is displayed if | 
|---|
| 268 | the test fails, or when the runtests function is invoked with | 
|---|
| 269 | the verbose option.""" | 
|---|
| 270 | self.test_log = self.test_log + msg + "\n" | 
|---|
| 271 |  | 
|---|
| 272 |  | 
|---|
| 273 | class NotRunError(Exception): | 
|---|
| 274 | """Raised if a test must be skipped because of missing resources""" | 
|---|
| 275 | def __init__(self, value = None): | 
|---|
| 276 | self.value = value | 
|---|
| 277 |  | 
|---|
| 278 |  | 
|---|
| 279 | def _report_error(case, debugger): | 
|---|
| 280 | """Ask the test case to explain failure, and optionally run a debugger | 
|---|
| 281 |  | 
|---|
| 282 | Input: | 
|---|
| 283 | case         TestCase instance | 
|---|
| 284 | debugger     if true, a debugger function to be applied to the traceback | 
|---|
| 285 | """ | 
|---|
| 286 | import sys | 
|---|
| 287 | ex = sys.exc_info() | 
|---|
| 288 | print "-----------------------------------------------------------------" | 
|---|
| 289 | if ex: | 
|---|
| 290 | import traceback | 
|---|
| 291 | traceback.print_exc(file=sys.stdout) | 
|---|
| 292 | case.explain_failure() | 
|---|
| 293 | print "-----------------------------------------------------------------" | 
|---|
| 294 |  | 
|---|
| 295 | if debugger: | 
|---|
| 296 | tb = ex[2] | 
|---|
| 297 | debugger(tb) | 
|---|
| 298 |  | 
|---|
| 299 |  | 
|---|
| 300 | def runtests(test_list, verbose = 0, debugger = None): | 
|---|
| 301 | """Run a series of tests. | 
|---|
| 302 |  | 
|---|
| 303 | Inputs: | 
|---|
| 304 | test_list    sequence of TestCase classes | 
|---|
| 305 | verbose      print more information as testing proceeds | 
|---|
| 306 | debugger     debugger object to be applied to errors | 
|---|
| 307 |  | 
|---|
| 308 | Returns: | 
|---|
| 309 | unix return code: 0 for success, 1 for failures, 2 for test failure | 
|---|
| 310 | """ | 
|---|
| 311 | import traceback | 
|---|
| 312 | ret = 0 | 
|---|
| 313 | for test_class in test_list: | 
|---|
| 314 | print "%-30s" % _test_name(test_class), | 
|---|
| 315 | # flush now so that long running tests are easier to follow | 
|---|
| 316 | sys.stdout.flush() | 
|---|
| 317 |  | 
|---|
| 318 | obj = None | 
|---|
| 319 | try: | 
|---|
| 320 | try: # run test and show result | 
|---|
| 321 | obj = test_class() | 
|---|
| 322 | obj.setup() | 
|---|
| 323 | obj.runtest() | 
|---|
| 324 | print "OK" | 
|---|
| 325 | except KeyboardInterrupt: | 
|---|
| 326 | print "INTERRUPT" | 
|---|
| 327 | _report_error(obj, debugger) | 
|---|
| 328 | ret = 2 | 
|---|
| 329 | break | 
|---|
| 330 | except NotRunError, msg: | 
|---|
| 331 | print "NOTRUN, %s" % msg.value | 
|---|
| 332 | except: | 
|---|
| 333 | print "FAIL" | 
|---|
| 334 | _report_error(obj, debugger) | 
|---|
| 335 | ret = 1 | 
|---|
| 336 | finally: | 
|---|
| 337 | while obj and obj._cleanups: | 
|---|
| 338 | try: | 
|---|
| 339 | apply(obj._cleanups.pop()) | 
|---|
| 340 | except KeyboardInterrupt: | 
|---|
| 341 | print "interrupted during teardown" | 
|---|
| 342 | _report_error(obj, debugger) | 
|---|
| 343 | ret = 2 | 
|---|
| 344 | break | 
|---|
| 345 | except: | 
|---|
| 346 | print "error during teardown" | 
|---|
| 347 | _report_error(obj, debugger) | 
|---|
| 348 | ret = 1 | 
|---|
| 349 | # Display log file if we're verbose | 
|---|
| 350 | if ret == 0 and verbose: | 
|---|
| 351 | obj.explain_failure() | 
|---|
| 352 |  | 
|---|
| 353 | return ret | 
|---|
| 354 |  | 
|---|
| 355 |  | 
|---|
| 356 | def _test_name(test_class): | 
|---|
| 357 | """Return a human-readable name for a test class. | 
|---|
| 358 | """ | 
|---|
| 359 | try: | 
|---|
| 360 | return test_class.__name__ | 
|---|
| 361 | except: | 
|---|
| 362 | return `test_class` | 
|---|
| 363 |  | 
|---|
| 364 |  | 
|---|
| 365 | def print_help(): | 
|---|
| 366 | """Help for people running tests""" | 
|---|
| 367 | import sys | 
|---|
| 368 | print """%s: software test suite based on ComfyChair | 
|---|
| 369 |  | 
|---|
| 370 | usage: | 
|---|
| 371 | To run all tests, just run this program.  To run particular tests, | 
|---|
| 372 | list them on the command line. | 
|---|
| 373 |  | 
|---|
| 374 | options: | 
|---|
| 375 | --help              show usage message | 
|---|
| 376 | --list              list available tests | 
|---|
| 377 | --verbose, -v       show more information while running tests | 
|---|
| 378 | --post-mortem, -p   enter Python debugger on error | 
|---|
| 379 | """ % sys.argv[0] | 
|---|
| 380 |  | 
|---|
| 381 |  | 
|---|
| 382 | def print_list(test_list): | 
|---|
| 383 | """Show list of available tests""" | 
|---|
| 384 | for test_class in test_list: | 
|---|
| 385 | print "    %s" % _test_name(test_class) | 
|---|
| 386 |  | 
|---|
| 387 |  | 
|---|
| 388 | def main(tests, extra_tests=[]): | 
|---|
| 389 | """Main entry point for test suites based on ComfyChair. | 
|---|
| 390 |  | 
|---|
| 391 | inputs: | 
|---|
| 392 | tests       Sequence of TestCase subclasses to be run by default. | 
|---|
| 393 | extra_tests Sequence of TestCase subclasses that are available but | 
|---|
| 394 | not run by default. | 
|---|
| 395 |  | 
|---|
| 396 | Test suites should contain this boilerplate: | 
|---|
| 397 |  | 
|---|
| 398 | if __name__ == '__main__': | 
|---|
| 399 | comfychair.main(tests) | 
|---|
| 400 |  | 
|---|
| 401 | This function handles standard options such as --help and --list, and | 
|---|
| 402 | by default runs all tests in the suggested order. | 
|---|
| 403 |  | 
|---|
| 404 | Calls sys.exit() on completion. | 
|---|
| 405 | """ | 
|---|
| 406 | from sys import argv | 
|---|
| 407 | import getopt, sys | 
|---|
| 408 |  | 
|---|
| 409 | opt_verbose = 0 | 
|---|
| 410 | debugger = None | 
|---|
| 411 |  | 
|---|
| 412 | opts, args = getopt.getopt(argv[1:], 'pv', | 
|---|
| 413 | ['help', 'list', 'verbose', 'post-mortem']) | 
|---|
| 414 | for opt, opt_arg in opts: | 
|---|
| 415 | if opt == '--help': | 
|---|
| 416 | print_help() | 
|---|
| 417 | return | 
|---|
| 418 | elif opt == '--list': | 
|---|
| 419 | print_list(tests + extra_tests) | 
|---|
| 420 | return | 
|---|
| 421 | elif opt == '--verbose' or opt == '-v': | 
|---|
| 422 | opt_verbose = 1 | 
|---|
| 423 | elif opt == '--post-mortem' or opt == '-p': | 
|---|
| 424 | import pdb | 
|---|
| 425 | debugger = pdb.post_mortem | 
|---|
| 426 |  | 
|---|
| 427 | if args: | 
|---|
| 428 | all_tests = tests + extra_tests | 
|---|
| 429 | by_name = {} | 
|---|
| 430 | for t in all_tests: | 
|---|
| 431 | by_name[_test_name(t)] = t | 
|---|
| 432 | which_tests = [] | 
|---|
| 433 | for name in args: | 
|---|
| 434 | which_tests.append(by_name[name]) | 
|---|
| 435 | else: | 
|---|
| 436 | which_tests = tests | 
|---|
| 437 |  | 
|---|
| 438 | sys.exit(runtests(which_tests, verbose=opt_verbose, | 
|---|
| 439 | debugger=debugger)) | 
|---|
| 440 |  | 
|---|
| 441 |  | 
|---|
| 442 | if __name__ == '__main__': | 
|---|
| 443 | print __doc__ | 
|---|