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('file', 'banned text\n') 50 51 test.must_not_contain_any_line(output, lines, ['title', find]) 52 53 test.must_not_exist('file1', ['file2', ...]) 54 55 test.run(options = "options to be prepended to arguments", 56 stdout = "expected standard output from the program", 57 stderr = "expected error output from the program", 58 status = expected_status, 59 match = match_function) 60 61 The TestCommon module also provides the following variables 62 63 TestCommon.python_executable 64 TestCommon.exe_suffix 65 TestCommon.obj_suffix 66 TestCommon.shobj_prefix 67 TestCommon.shobj_suffix 68 TestCommon.lib_prefix 69 TestCommon.lib_suffix 70 TestCommon.dll_prefix 71 TestCommon.dll_suffix 72 73 """ 74 75 # Copyright 2000-2010 Steven Knight 76 # This module is free software, and you may redistribute it and/or modify 77 # it under the same terms as Python itself, so long as this copyright message 78 # and disclaimer are retained in their original form. 79 # 80 # IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, 81 # SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF 82 # THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 83 # DAMAGE. 84 # 85 # THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 86 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 87 # PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, 88 # AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, 89 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 90 91 __author__ = "Steven Knight <knight at baldmt dot com>" 92 __revision__ = "TestCommon.py 0.37.D001 2010/01/11 16:55:50 knight" 93 __version__ = "0.37" 94 95 import copy 96 import os 97 import os.path 98 import stat 99 import string 100 import sys 101 import types 102 import UserList 103 104 from TestCmd import * 105 from TestCmd import __all__ 106 107 __all__.extend([ 'TestCommon', 108 'exe_suffix', 109 'obj_suffix', 110 'shobj_prefix', 111 'shobj_suffix', 112 'lib_prefix', 113 'lib_suffix', 114 'dll_prefix', 115 'dll_suffix', 116 ]) 117 118 # Variables that describe the prefixes and suffixes on this system. 119 if sys.platform == 'win32': 120 exe_suffix = '.exe' 121 obj_suffix = '.obj' 122 shobj_suffix = '.obj' 123 shobj_prefix = '' 124 lib_prefix = '' 125 lib_suffix = '.lib' 126 dll_prefix = '' 127 dll_suffix = '.dll' 128 elif sys.platform == 'cygwin': 129 exe_suffix = '.exe' 130 obj_suffix = '.o' 131 shobj_suffix = '.os' 132 shobj_prefix = '' 133 lib_prefix = 'lib' 134 lib_suffix = '.a' 135 dll_prefix = '' 136 dll_suffix = '.dll' 137 elif string.find(sys.platform, 'irix') != -1: 138 exe_suffix = '' 139 obj_suffix = '.o' 140 shobj_suffix = '.o' 141 shobj_prefix = '' 142 lib_prefix = 'lib' 143 lib_suffix = '.a' 144 dll_prefix = 'lib' 145 dll_suffix = '.so' 146 elif string.find(sys.platform, 'darwin') != -1: 147 exe_suffix = '' 148 obj_suffix = '.o' 149 shobj_suffix = '.os' 150 shobj_prefix = '' 151 lib_prefix = 'lib' 152 lib_suffix = '.a' 153 dll_prefix = 'lib' 154 dll_suffix = '.dylib' 155 elif string.find(sys.platform, 'sunos') != -1: 156 exe_suffix = '' 157 obj_suffix = '.o' 158 shobj_suffix = '.os' 159 shobj_prefix = 'so_' 160 lib_prefix = 'lib' 161 lib_suffix = '.a' 162 dll_prefix = 'lib' 163 dll_suffix = '.dylib' 164 else: 165 exe_suffix = '' 166 obj_suffix = '.o' 167 shobj_suffix = '.os' 168 shobj_prefix = '' 169 lib_prefix = 'lib' 170 lib_suffix = '.a' 171 dll_prefix = 'lib' 172 dll_suffix = '.so' 173 174 def is_List(e): 175 return type(e) is types.ListType \ 176 or isinstance(e, UserList.UserList) 177 178 def is_writable(f): 179 mode = os.stat(f)[stat.ST_MODE] 180 return mode & stat.S_IWUSR 181 182 def separate_files(flist): 183 existing = [] 184 missing = [] 185 for f in flist: 186 if os.path.exists(f): 187 existing.append(f) 188 else: 189 missing.append(f) 190 return existing, missing 191 192 def _failed(self, status = 0): 193 if self.status is None or status is None: 194 return None 195 try: 196 return _status(self) not in status 197 except TypeError: 198 # status wasn't an iterable 199 return _status(self) != status 200 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(self, file, banned, mode = 'rb'): 337 """Ensures that the specified file doesn't contain the banned text. 338 """ 339 file_contents = self.read(file, mode) 340 contains = (string.find(file_contents, banned) != -1) 341 if contains: 342 print "File `%s' contains banned string." % file 343 print self.banner('Banned string ') 344 print banned 345 print self.banner('%s contents ' % file) 346 print file_contents 347 self.fail_test(contains) 348 349 def must_not_contain_any_line(self, output, lines, title=None, find=None): 350 """Ensures that the specified output string (first argument) 351 does not contain any of the specified lines (second argument). 352 353 An optional third argument can be used to describe the type 354 of output being searched, and only shows up in failure output. 355 356 An optional fourth argument can be used to supply a different 357 function, of the form "find(line, output), to use when searching 358 for lines in the output. 359 """ 360 if find is None: 361 find = lambda o, l: string.find(o, l) != -1 362 unexpected = [] 363 for line in lines: 364 if find(output, line): 365 unexpected.append(line) 366 367 if unexpected: 368 if title is None: 369 title = 'output' 370 sys.stdout.write("Unexpected lines in %s:\n" % title) 371 for line in unexpected: 372 sys.stdout.write(' ' + repr(line) + '\n') 373 sys.stdout.write(self.banner(title + ' ')) 374 sys.stdout.write(output) 375 self.fail_test() 376 377 def must_not_contain_lines(self, lines, output, title=None): 378 return self.must_not_contain_any_line(output, lines, title) 379 380 def must_not_exist(self, *files): 381 """Ensures that the specified file(s) must not exist. 382 An individual file be specified as a list of directory names, in 383 which case the pathname will be constructed by concatenating them. 384 Exits FAILED if any of the files exists. 385 """ 386 files = map(lambda x: is_List(x) and apply(os.path.join, x) or x, files) 387 existing = filter(os.path.exists, files) 388 if existing: 389 print "Unexpected files exist: `%s'" % string.join(existing, "', `") 390 self.fail_test(existing) 391 392 def must_not_be_writable(self, *files): 393 """Ensures that the specified file(s) exist and are not writable. 394 An individual file can be specified as a list of directory names, 395 in which case the pathname will be constructed by concatenating 396 them. Exits FAILED if any of the files does not exist or is 397 writable. 398 """ 399 files = map(lambda x: is_List(x) and apply(os.path.join, x) or x, files) 400 existing, missing = separate_files(files) 401 writable = filter(is_writable, existing) 402 if missing: 403 print "Missing files: `%s'" % string.join(missing, "', `") 404 if writable: 405 print "Writable files: `%s'" % string.join(writable, "', `") 406 self.fail_test(missing + writable) 407 408 def _complete(self, actual_stdout, expected_stdout, 409 actual_stderr, expected_stderr, status, match): 410 """ 411 Post-processes running a subcommand, checking for failure 412 status and displaying output appropriately. 413 """ 414 if _failed(self, status): 415 expect = '' 416 if status != 0: 417 expect = " (expected %s)" % str(status) 418 print "%s returned %s%s" % (self.program, str(_status(self)), expect) 419 print self.banner('STDOUT ') 420 print actual_stdout 421 print self.banner('STDERR ') 422 print actual_stderr 423 self.fail_test() 424 if not expected_stdout is None and not match(actual_stdout, expected_stdout): 425 self.diff(expected_stdout, actual_stdout, 'STDOUT ') 426 if actual_stderr: 427 print self.banner('STDERR ') 428 print actual_stderr 429 self.fail_test() 430 if not expected_stderr is None and not match(actual_stderr, expected_stderr): 431 print self.banner('STDOUT ') 432 print actual_stdout 433 self.diff(expected_stderr, actual_stderr, 'STDERR ') 434 self.fail_test() 435 436 def start(self, program = None, 437 interpreter = None, 438 arguments = None, 439 universal_newlines = None, 440 **kw): 441 """ 442 Starts a program or script for the test environment. 443 444 This handles the "options" keyword argument and exceptions. 445 """ 446 options = kw.pop('options', None) 447 if options: 448 if arguments is None: 449 arguments = options 450 else: 451 arguments = options + " " + arguments 452 453 try: 454 return apply(TestCmd.start, 455 (self, program, interpreter, arguments, universal_newlines), 456 kw) 457 except KeyboardInterrupt: 458 raise 459 except Exception, e: 460 print self.banner('STDOUT ') 461 try: 462 print self.stdout() 463 except IndexError: 464 pass 465 print self.banner('STDERR ') 466 try: 467 print self.stderr() 468 except IndexError: 469 pass 470 cmd_args = self.command_args(program, interpreter, arguments) 471 sys.stderr.write('Exception trying to execute: %s\n' % cmd_args) 472 raise e 473 474 def finish(self, popen, stdout = None, stderr = '', status = 0, **kw): 475 """ 476 Finishes and waits for the process being run under control of 477 the specified popen argument. Additional arguments are similar 478 to those of the run() method: 479 480 stdout The expected standard output from 481 the command. A value of None means 482 don't test standard output. 483 484 stderr The expected error output from 485 the command. A value of None means 486 don't test error output. 487 488 status The expected exit status from the 489 command. A value of None means don't 490 test exit status. 491 """ 492 apply(TestCmd.finish, (self, popen,), kw) 493 match = kw.get('match', self.match) 494 self._complete(self.stdout(), stdout, 495 self.stderr(), stderr, status, match) 496 497 def run(self, options = None, arguments = None, 498 stdout = None, stderr = '', status = 0, **kw): 499 """Runs the program under test, checking that the test succeeded. 500 501 The arguments are the same as the base TestCmd.run() method, 502 with the addition of: 503 504 options Extra options that get appended to the beginning 505 of the arguments. 506 507 stdout The expected standard output from 508 the command. A value of None means 509 don't test standard output. 510 511 stderr The expected error output from 512 the command. A value of None means 513 don't test error output. 514 515 status The expected exit status from the 516 command. A value of None means don't 517 test exit status. 518 519 By default, this expects a successful exit (status = 0), does 520 not test standard output (stdout = None), and expects that error 521 output is empty (stderr = ""). 522 """ 523 if options: 524 if arguments is None: 525 arguments = options 526 else: 527 arguments = options + " " + arguments 528 kw['arguments'] = arguments 529 match = kw.pop('match', self.match) 530 apply(TestCmd.run, [self], kw) 531 self._complete(self.stdout(), stdout, 532 self.stderr(), stderr, status, match) 533 534 def skip_test(self, message="Skipping test.\n"): 535 """Skips a test. 536 537 Proper test-skipping behavior is dependent on the external 538 TESTCOMMON_PASS_SKIPS environment variable. If set, we treat 539 the skip as a PASS (exit 0), and otherwise treat it as NO RESULT. 540 In either case, we print the specified message as an indication 541 that the substance of the test was skipped. 542 543 (This was originally added to support development under Aegis. 544 Technically, skipping a test is a NO RESULT, but Aegis would 545 treat that as a test failure and prevent the change from going to 546 the next step. Since we ddn't want to force anyone using Aegis 547 to have to install absolutely every tool used by the tests, we 548 would actually report to Aegis that a skipped test has PASSED 549 so that the workflow isn't held up.) 550 """ 551 if message: 552 sys.stdout.write(message) 553 sys.stdout.flush() 554 pass_skips = os.environ.get('TESTCOMMON_PASS_SKIPS') 555 if pass_skips in [None, 0, '0']: 556 # skip=1 means skip this function when showing where this 557 # result came from. They only care about the line where the 558 # script called test.skip_test(), not the line number where 559 # we call test.no_result(). 560 self.no_result(skip=1) 561 else: 562 # We're under the development directory for this change, 563 # so this is an Aegis invocation; pass the test (exit 0). 564 self.pass_test() 565 566 # Local Variables: 567 # tab-width:4 568 # indent-tabs-mode:nil 569 # End: 570 # vim: set expandtab tabstop=4 shiftwidth=4: 571