| OLD | NEW |
| (Empty) | |
| 1 """ |
| 2 TestCommon.py: a testing framework for commands and scripts |
| 3 with commonly useful error handling |
| 4 |
| 5 The TestCommon module provides a simple, high-level interface for writing |
| 6 tests of executable commands and scripts, especially commands and scripts |
| 7 that interact with the file system. All methods throw exceptions and |
| 8 exit on failure, with useful error messages. This makes a number of |
| 9 explicit checks unnecessary, making the test scripts themselves simpler |
| 10 to write and easier to read. |
| 11 |
| 12 The TestCommon class is a subclass of the TestCmd class. In essence, |
| 13 TestCommon is a wrapper that handles common TestCmd error conditions in |
| 14 useful ways. You can use TestCommon directly, or subclass it for your |
| 15 program and add additional (or override) methods to tailor it to your |
| 16 program's specific needs. Alternatively, the TestCommon class serves |
| 17 as a useful example of how to define your own TestCmd subclass. |
| 18 |
| 19 As a subclass of TestCmd, TestCommon provides access to all of the |
| 20 variables and methods from the TestCmd module. Consequently, you can |
| 21 use any variable or method documented in the TestCmd module without |
| 22 having to explicitly import TestCmd. |
| 23 |
| 24 A TestCommon environment object is created via the usual invocation: |
| 25 |
| 26 import TestCommon |
| 27 test = TestCommon.TestCommon() |
| 28 |
| 29 You can use all of the TestCmd keyword arguments when instantiating a |
| 30 TestCommon object; see the TestCmd documentation for details. |
| 31 |
| 32 Here is an overview of the methods and keyword arguments that are |
| 33 provided by the TestCommon class: |
| 34 |
| 35 test.must_be_writable('file1', ['file2', ...]) |
| 36 |
| 37 test.must_contain('file', 'required text\n') |
| 38 |
| 39 test.must_contain_all_lines(output, lines, ['title', find]) |
| 40 |
| 41 test.must_contain_any_line(output, lines, ['title', find]) |
| 42 |
| 43 test.must_exist('file1', ['file2', ...]) |
| 44 |
| 45 test.must_match('file', "expected contents\n") |
| 46 |
| 47 test.must_not_be_writable('file1', ['file2', ...]) |
| 48 |
| 49 test.must_not_contain_any_line(output, lines, ['title', find]) |
| 50 |
| 51 test.must_not_exist('file1', ['file2', ...]) |
| 52 |
| 53 test.run(options = "options to be prepended to arguments", |
| 54 stdout = "expected standard output from the program", |
| 55 stderr = "expected error output from the program", |
| 56 status = expected_status, |
| 57 match = match_function) |
| 58 |
| 59 The TestCommon module also provides the following variables |
| 60 |
| 61 TestCommon.python_executable |
| 62 TestCommon.exe_suffix |
| 63 TestCommon.obj_suffix |
| 64 TestCommon.shobj_prefix |
| 65 TestCommon.shobj_suffix |
| 66 TestCommon.lib_prefix |
| 67 TestCommon.lib_suffix |
| 68 TestCommon.dll_prefix |
| 69 TestCommon.dll_suffix |
| 70 |
| 71 """ |
| 72 |
| 73 # Copyright 2000, 2001, 2002, 2003, 2004 Steven Knight |
| 74 # This module is free software, and you may redistribute it and/or modify |
| 75 # it under the same terms as Python itself, so long as this copyright message |
| 76 # and disclaimer are retained in their original form. |
| 77 # |
| 78 # IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, |
| 79 # SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF |
| 80 # THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH |
| 81 # DAMAGE. |
| 82 # |
| 83 # THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT |
| 84 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A |
| 85 # PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, |
| 86 # AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, |
| 87 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. |
| 88 |
| 89 __author__ = "Steven Knight <knight at baldmt dot com>" |
| 90 __revision__ = "TestCommon.py 0.36.D001 2009/07/24 08:45:26 knight" |
| 91 __version__ = "0.36" |
| 92 |
| 93 import copy |
| 94 import os |
| 95 import os.path |
| 96 import stat |
| 97 import string |
| 98 import sys |
| 99 import types |
| 100 import UserList |
| 101 |
| 102 from TestCmd import * |
| 103 from TestCmd import __all__ |
| 104 |
| 105 __all__.extend([ 'TestCommon', |
| 106 'exe_suffix', |
| 107 'obj_suffix', |
| 108 'shobj_prefix', |
| 109 'shobj_suffix', |
| 110 'lib_prefix', |
| 111 'lib_suffix', |
| 112 'dll_prefix', |
| 113 'dll_suffix', |
| 114 ]) |
| 115 |
| 116 # Variables that describe the prefixes and suffixes on this system. |
| 117 if sys.platform == 'win32': |
| 118 exe_suffix = '.exe' |
| 119 obj_suffix = '.obj' |
| 120 shobj_suffix = '.obj' |
| 121 shobj_prefix = '' |
| 122 lib_prefix = '' |
| 123 lib_suffix = '.lib' |
| 124 dll_prefix = '' |
| 125 dll_suffix = '.dll' |
| 126 elif sys.platform == 'cygwin': |
| 127 exe_suffix = '.exe' |
| 128 obj_suffix = '.o' |
| 129 shobj_suffix = '.os' |
| 130 shobj_prefix = '' |
| 131 lib_prefix = 'lib' |
| 132 lib_suffix = '.a' |
| 133 dll_prefix = '' |
| 134 dll_suffix = '.dll' |
| 135 elif string.find(sys.platform, 'irix') != -1: |
| 136 exe_suffix = '' |
| 137 obj_suffix = '.o' |
| 138 shobj_suffix = '.o' |
| 139 shobj_prefix = '' |
| 140 lib_prefix = 'lib' |
| 141 lib_suffix = '.a' |
| 142 dll_prefix = 'lib' |
| 143 dll_suffix = '.so' |
| 144 elif string.find(sys.platform, 'darwin') != -1: |
| 145 exe_suffix = '' |
| 146 obj_suffix = '.o' |
| 147 shobj_suffix = '.os' |
| 148 shobj_prefix = '' |
| 149 lib_prefix = 'lib' |
| 150 lib_suffix = '.a' |
| 151 dll_prefix = 'lib' |
| 152 dll_suffix = '.dylib' |
| 153 elif string.find(sys.platform, 'sunos') != -1: |
| 154 exe_suffix = '' |
| 155 obj_suffix = '.o' |
| 156 shobj_suffix = '.os' |
| 157 shobj_prefix = 'so_' |
| 158 lib_prefix = 'lib' |
| 159 lib_suffix = '.a' |
| 160 dll_prefix = 'lib' |
| 161 dll_suffix = '.dylib' |
| 162 else: |
| 163 exe_suffix = '' |
| 164 obj_suffix = '.o' |
| 165 shobj_suffix = '.os' |
| 166 shobj_prefix = '' |
| 167 lib_prefix = 'lib' |
| 168 lib_suffix = '.a' |
| 169 dll_prefix = 'lib' |
| 170 dll_suffix = '.so' |
| 171 |
| 172 def is_List(e): |
| 173 return type(e) is types.ListType \ |
| 174 or isinstance(e, UserList.UserList) |
| 175 |
| 176 def is_writable(f): |
| 177 mode = os.stat(f)[stat.ST_MODE] |
| 178 return mode & stat.S_IWUSR |
| 179 |
| 180 def separate_files(flist): |
| 181 existing = [] |
| 182 missing = [] |
| 183 for f in flist: |
| 184 if os.path.exists(f): |
| 185 existing.append(f) |
| 186 else: |
| 187 missing.append(f) |
| 188 return existing, missing |
| 189 |
| 190 if os.name == 'posix': |
| 191 def _failed(self, status = 0): |
| 192 if self.status is None or status is None: |
| 193 return None |
| 194 return _status(self) != status |
| 195 def _status(self): |
| 196 return self.status |
| 197 elif os.name == 'nt': |
| 198 def _failed(self, status = 0): |
| 199 return not (self.status is None or status is None) and \ |
| 200 self.status != status |
| 201 def _status(self): |
| 202 return self.status |
| 203 |
| 204 class TestCommon(TestCmd): |
| 205 |
| 206 # Additional methods from the Perl Test::Cmd::Common module |
| 207 # that we may wish to add in the future: |
| 208 # |
| 209 # $test->subdir('subdir', ...); |
| 210 # |
| 211 # $test->copy('src_file', 'dst_file'); |
| 212 |
| 213 def __init__(self, **kw): |
| 214 """Initialize a new TestCommon instance. This involves just |
| 215 calling the base class initialization, and then changing directory |
| 216 to the workdir. |
| 217 """ |
| 218 apply(TestCmd.__init__, [self], kw) |
| 219 os.chdir(self.workdir) |
| 220 |
| 221 def must_be_writable(self, *files): |
| 222 """Ensures that the specified file(s) exist and are writable. |
| 223 An individual file can be specified as a list of directory names, |
| 224 in which case the pathname will be constructed by concatenating |
| 225 them. Exits FAILED if any of the files does not exist or is |
| 226 not writable. |
| 227 """ |
| 228 files = map(lambda x: is_List(x) and apply(os.path.join, x) or x, files) |
| 229 existing, missing = separate_files(files) |
| 230 unwritable = filter(lambda x, iw=is_writable: not iw(x), existing) |
| 231 if missing: |
| 232 print "Missing files: `%s'" % string.join(missing, "', `") |
| 233 if unwritable: |
| 234 print "Unwritable files: `%s'" % string.join(unwritable, "', `") |
| 235 self.fail_test(missing + unwritable) |
| 236 |
| 237 def must_contain(self, file, required, mode = 'rb'): |
| 238 """Ensures that the specified file contains the required text. |
| 239 """ |
| 240 file_contents = self.read(file, mode) |
| 241 contains = (string.find(file_contents, required) != -1) |
| 242 if not contains: |
| 243 print "File `%s' does not contain required string." % file |
| 244 print self.banner('Required string ') |
| 245 print required |
| 246 print self.banner('%s contents ' % file) |
| 247 print file_contents |
| 248 self.fail_test(not contains) |
| 249 |
| 250 def must_contain_all_lines(self, output, lines, title=None, find=None): |
| 251 """Ensures that the specified output string (first argument) |
| 252 contains all of the specified lines (second argument). |
| 253 |
| 254 An optional third argument can be used to describe the type |
| 255 of output being searched, and only shows up in failure output. |
| 256 |
| 257 An optional fourth argument can be used to supply a different |
| 258 function, of the form "find(line, output), to use when searching |
| 259 for lines in the output. |
| 260 """ |
| 261 if find is None: |
| 262 find = lambda o, l: string.find(o, l) != -1 |
| 263 missing = [] |
| 264 for line in lines: |
| 265 if not find(output, line): |
| 266 missing.append(line) |
| 267 |
| 268 if missing: |
| 269 if title is None: |
| 270 title = 'output' |
| 271 sys.stdout.write("Missing expected lines from %s:\n" % title) |
| 272 for line in missing: |
| 273 sys.stdout.write(' ' + repr(line) + '\n') |
| 274 sys.stdout.write(self.banner(title + ' ')) |
| 275 sys.stdout.write(output) |
| 276 self.fail_test() |
| 277 |
| 278 def must_contain_any_line(self, output, lines, title=None, find=None): |
| 279 """Ensures that the specified output string (first argument) |
| 280 contains at least one of the specified lines (second argument). |
| 281 |
| 282 An optional third argument can be used to describe the type |
| 283 of output being searched, and only shows up in failure output. |
| 284 |
| 285 An optional fourth argument can be used to supply a different |
| 286 function, of the form "find(line, output), to use when searching |
| 287 for lines in the output. |
| 288 """ |
| 289 if find is None: |
| 290 find = lambda o, l: string.find(o, l) != -1 |
| 291 for line in lines: |
| 292 if find(output, line): |
| 293 return |
| 294 |
| 295 if title is None: |
| 296 title = 'output' |
| 297 sys.stdout.write("Missing any expected line from %s:\n" % title) |
| 298 for line in lines: |
| 299 sys.stdout.write(' ' + repr(line) + '\n') |
| 300 sys.stdout.write(self.banner(title + ' ')) |
| 301 sys.stdout.write(output) |
| 302 self.fail_test() |
| 303 |
| 304 def must_contain_lines(self, lines, output, title=None): |
| 305 # Deprecated; retain for backwards compatibility. |
| 306 return self.must_contain_all_lines(output, lines, title) |
| 307 |
| 308 def must_exist(self, *files): |
| 309 """Ensures that the specified file(s) must exist. An individual |
| 310 file be specified as a list of directory names, in which case the |
| 311 pathname will be constructed by concatenating them. Exits FAILED |
| 312 if any of the files does not exist. |
| 313 """ |
| 314 files = map(lambda x: is_List(x) and apply(os.path.join, x) or x, files) |
| 315 missing = filter(lambda x: not os.path.exists(x), files) |
| 316 if missing: |
| 317 print "Missing files: `%s'" % string.join(missing, "', `") |
| 318 self.fail_test(missing) |
| 319 |
| 320 def must_match(self, file, expect, mode = 'rb'): |
| 321 """Matches the contents of the specified file (first argument) |
| 322 against the expected contents (second argument). The expected |
| 323 contents are a list of lines or a string which will be split |
| 324 on newlines. |
| 325 """ |
| 326 file_contents = self.read(file, mode) |
| 327 try: |
| 328 self.fail_test(not self.match(file_contents, expect)) |
| 329 except KeyboardInterrupt: |
| 330 raise |
| 331 except: |
| 332 print "Unexpected contents of `%s'" % file |
| 333 self.diff(expect, file_contents, 'contents ') |
| 334 raise |
| 335 |
| 336 def must_not_contain_any_line(self, output, lines, title=None, find=None): |
| 337 """Ensures that the specified output string (first argument) |
| 338 does not contain any of the specified lines (second argument). |
| 339 |
| 340 An optional third argument can be used to describe the type |
| 341 of output being searched, and only shows up in failure output. |
| 342 |
| 343 An optional fourth argument can be used to supply a different |
| 344 function, of the form "find(line, output), to use when searching |
| 345 for lines in the output. |
| 346 """ |
| 347 if find is None: |
| 348 find = lambda o, l: string.find(o, l) != -1 |
| 349 unexpected = [] |
| 350 for line in lines: |
| 351 if find(output, line): |
| 352 unexpected.append(line) |
| 353 |
| 354 if unexpected: |
| 355 if title is None: |
| 356 title = 'output' |
| 357 sys.stdout.write("Unexpected lines in %s:\n" % title) |
| 358 for line in unexpected: |
| 359 sys.stdout.write(' ' + repr(line) + '\n') |
| 360 sys.stdout.write(self.banner(title + ' ')) |
| 361 sys.stdout.write(output) |
| 362 self.fail_test() |
| 363 |
| 364 def must_not_contain_lines(self, lines, output, title=None): |
| 365 return self.must_not_contain_any_line(output, lines, title) |
| 366 |
| 367 def must_not_exist(self, *files): |
| 368 """Ensures that the specified file(s) must not exist. |
| 369 An individual file be specified as a list of directory names, in |
| 370 which case the pathname will be constructed by concatenating them. |
| 371 Exits FAILED if any of the files exists. |
| 372 """ |
| 373 files = map(lambda x: is_List(x) and apply(os.path.join, x) or x, files) |
| 374 existing = filter(os.path.exists, files) |
| 375 if existing: |
| 376 print "Unexpected files exist: `%s'" % string.join(existing, "', `") |
| 377 self.fail_test(existing) |
| 378 |
| 379 |
| 380 def must_not_be_writable(self, *files): |
| 381 """Ensures that the specified file(s) exist and are not writable. |
| 382 An individual file can be specified as a list of directory names, |
| 383 in which case the pathname will be constructed by concatenating |
| 384 them. Exits FAILED if any of the files does not exist or is |
| 385 writable. |
| 386 """ |
| 387 files = map(lambda x: is_List(x) and apply(os.path.join, x) or x, files) |
| 388 existing, missing = separate_files(files) |
| 389 writable = filter(is_writable, existing) |
| 390 if missing: |
| 391 print "Missing files: `%s'" % string.join(missing, "', `") |
| 392 if writable: |
| 393 print "Writable files: `%s'" % string.join(writable, "', `") |
| 394 self.fail_test(missing + writable) |
| 395 |
| 396 def _complete(self, actual_stdout, expected_stdout, |
| 397 actual_stderr, expected_stderr, status, match): |
| 398 """ |
| 399 Post-processes running a subcommand, checking for failure |
| 400 status and displaying output appropriately. |
| 401 """ |
| 402 if _failed(self, status): |
| 403 expect = '' |
| 404 if status != 0: |
| 405 expect = " (expected %s)" % str(status) |
| 406 print "%s returned %s%s" % (self.program, str(_status(self)), expect
) |
| 407 print self.banner('STDOUT ') |
| 408 print actual_stdout |
| 409 print self.banner('STDERR ') |
| 410 print actual_stderr |
| 411 self.fail_test() |
| 412 if not expected_stdout is None and not match(actual_stdout, expected_std
out): |
| 413 self.diff(expected_stdout, actual_stdout, 'STDOUT ') |
| 414 if actual_stderr: |
| 415 print self.banner('STDERR ') |
| 416 print actual_stderr |
| 417 self.fail_test() |
| 418 if not expected_stderr is None and not match(actual_stderr, expected_std
err): |
| 419 print self.banner('STDOUT ') |
| 420 print actual_stdout |
| 421 self.diff(expected_stderr, actual_stderr, 'STDERR ') |
| 422 self.fail_test() |
| 423 |
| 424 def start(self, program = None, |
| 425 interpreter = None, |
| 426 arguments = None, |
| 427 universal_newlines = None, |
| 428 **kw): |
| 429 """ |
| 430 Starts a program or script for the test environment. |
| 431 |
| 432 This handles the "options" keyword argument and exceptions. |
| 433 """ |
| 434 try: |
| 435 options = kw['options'] |
| 436 del kw['options'] |
| 437 except KeyError: |
| 438 pass |
| 439 else: |
| 440 if options: |
| 441 if arguments is None: |
| 442 arguments = options |
| 443 else: |
| 444 arguments = options + " " + arguments |
| 445 try: |
| 446 return apply(TestCmd.start, |
| 447 (self, program, interpreter, arguments, universal_newli
nes), |
| 448 kw) |
| 449 except KeyboardInterrupt: |
| 450 raise |
| 451 except Exception, e: |
| 452 print self.banner('STDOUT ') |
| 453 try: |
| 454 print self.stdout() |
| 455 except IndexError: |
| 456 pass |
| 457 print self.banner('STDERR ') |
| 458 try: |
| 459 print self.stderr() |
| 460 except IndexError: |
| 461 pass |
| 462 cmd_args = self.command_args(program, interpreter, arguments) |
| 463 sys.stderr.write('Exception trying to execute: %s\n' % cmd_args) |
| 464 raise e |
| 465 |
| 466 def finish(self, popen, stdout = None, stderr = '', status = 0, **kw): |
| 467 """ |
| 468 Finishes and waits for the process being run under control of |
| 469 the specified popen argument. Additional arguments are similar |
| 470 to those of the run() method: |
| 471 |
| 472 stdout The expected standard output from |
| 473 the command. A value of None means |
| 474 don't test standard output. |
| 475 |
| 476 stderr The expected error output from |
| 477 the command. A value of None means |
| 478 don't test error output. |
| 479 |
| 480 status The expected exit status from the |
| 481 command. A value of None means don't |
| 482 test exit status. |
| 483 """ |
| 484 apply(TestCmd.finish, (self, popen,), kw) |
| 485 match = kw.get('match', self.match) |
| 486 self._complete(self.stdout(), stdout, |
| 487 self.stderr(), stderr, status, match) |
| 488 |
| 489 def run(self, options = None, arguments = None, |
| 490 stdout = None, stderr = '', status = 0, **kw): |
| 491 """Runs the program under test, checking that the test succeeded. |
| 492 |
| 493 The arguments are the same as the base TestCmd.run() method, |
| 494 with the addition of: |
| 495 |
| 496 options Extra options that get appended to the beginning |
| 497 of the arguments. |
| 498 |
| 499 stdout The expected standard output from |
| 500 the command. A value of None means |
| 501 don't test standard output. |
| 502 |
| 503 stderr The expected error output from |
| 504 the command. A value of None means |
| 505 don't test error output. |
| 506 |
| 507 status The expected exit status from the |
| 508 command. A value of None means don't |
| 509 test exit status. |
| 510 |
| 511 By default, this expects a successful exit (status = 0), does |
| 512 not test standard output (stdout = None), and expects that error |
| 513 output is empty (stderr = ""). |
| 514 """ |
| 515 if options: |
| 516 if arguments is None: |
| 517 arguments = options |
| 518 else: |
| 519 arguments = options + " " + arguments |
| 520 kw['arguments'] = arguments |
| 521 try: |
| 522 match = kw['match'] |
| 523 del kw['match'] |
| 524 except KeyError: |
| 525 match = self.match |
| 526 apply(TestCmd.run, [self], kw) |
| 527 self._complete(self.stdout(), stdout, |
| 528 self.stderr(), stderr, status, match) |
| 529 |
| 530 def skip_test(self, message="Skipping test.\n"): |
| 531 """Skips a test. |
| 532 |
| 533 Proper test-skipping behavior is dependent on the external |
| 534 TESTCOMMON_PASS_SKIPS environment variable. If set, we treat |
| 535 the skip as a PASS (exit 0), and otherwise treat it as NO RESULT. |
| 536 In either case, we print the specified message as an indication |
| 537 that the substance of the test was skipped. |
| 538 |
| 539 (This was originally added to support development under Aegis. |
| 540 Technically, skipping a test is a NO RESULT, but Aegis would |
| 541 treat that as a test failure and prevent the change from going to |
| 542 the next step. Since we ddn't want to force anyone using Aegis |
| 543 to have to install absolutely every tool used by the tests, we |
| 544 would actually report to Aegis that a skipped test has PASSED |
| 545 so that the workflow isn't held up.) |
| 546 """ |
| 547 if message: |
| 548 sys.stdout.write(message) |
| 549 sys.stdout.flush() |
| 550 pass_skips = os.environ.get('TESTCOMMON_PASS_SKIPS') |
| 551 if pass_skips in [None, 0, '0']: |
| 552 # skip=1 means skip this function when showing where this |
| 553 # result came from. They only care about the line where the |
| 554 # script called test.skip_test(), not the line number where |
| 555 # we call test.no_result(). |
| 556 self.no_result(skip=1) |
| 557 else: |
| 558 # We're under the development directory for this change, |
| 559 # so this is an Aegis invocation; pass the test (exit 0). |
| 560 self.pass_test() |
| 561 |
| 562 # Local Variables: |
| 563 # tab-width:4 |
| 564 # indent-tabs-mode:nil |
| 565 # End: |
| 566 # vim: set expandtab tabstop=4 shiftwidth=4: |
| OLD | NEW |