1#!/usr/bin/env python 2# 3# Public Domain 2014-2018 MongoDB, Inc. 4# Public Domain 2008-2014 WiredTiger, Inc. 5# 6# This is free and unencumbered software released into the public domain. 7# 8# Anyone is free to copy, modify, publish, use, compile, sell, or 9# distribute this software, either in source code form or as a compiled 10# binary, for any purpose, commercial or non-commercial, and by any 11# means. 12# 13# In jurisdictions that recognize copyright laws, the author or authors 14# of this software dedicate any and all copyright interest in the 15# software to the public domain. We make this dedication for the benefit 16# of the public at large and to the detriment of our heirs and 17# successors. We intend this dedication to be an overt act of 18# relinquishment in perpetuity of all present and future rights to this 19# software under copyright law. 20# 21# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 24# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 25# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 26# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27# OTHER DEALINGS IN THE SOFTWARE. 28# 29# WiredTigerTestCase 30# parent class for all test cases 31# 32 33# If unittest2 is available, use it in preference to (the old) unittest 34try: 35 import unittest2 as unittest 36except ImportError: 37 import unittest 38 39from contextlib import contextmanager 40import glob, os, re, shutil, sys, time, traceback 41import wiredtiger, wtscenario 42 43def shortenWithEllipsis(s, maxlen): 44 if len(s) > maxlen: 45 s = s[0:maxlen-3] + '...' 46 return s 47 48class CapturedFd(object): 49 """ 50 CapturedFd encapsulates a file descriptor (e.g. 1 or 2) that is diverted 51 to a file. We use this to capture and check the C stdout/stderr. 52 Meanwhile we reset Python's sys.stdout, sys.stderr, using duped copies 53 of the original 1, 2 fds. The end result is that Python's sys.stdout 54 sys.stderr behave normally (e.g. go to the tty), while the C stdout/stderr 55 ends up in a file that we can verify. 56 """ 57 def __init__(self, filename, desc): 58 self.filename = filename 59 self.desc = desc 60 self.expectpos = 0 61 self.file = None 62 63 def readFileFrom(self, filename, pos, maxchars): 64 """ 65 Read a file starting at a given position, 66 returning the beginning of its contents 67 """ 68 with open(filename, 'r') as f: 69 f.seek(pos) 70 return shortenWithEllipsis(f.read(maxchars+1), maxchars) 71 72 def capture(self): 73 """ 74 Start capturing the file descriptor. 75 Note that the original targetFd is closed, we expect 76 that the caller has duped it and passed the dup to us 77 in the constructor. 78 """ 79 self.file = open(self.filename, 'w') 80 return self.file 81 82 def release(self): 83 """ 84 Stop capturing. 85 """ 86 self.file.close() 87 self.file = None 88 89 def check(self, testcase): 90 """ 91 Check to see that there is no unexpected output in the captured output 92 file. If there is, raise it as a test failure. 93 This is generally called after 'release' is called. 94 """ 95 if self.file != None: 96 self.file.flush() 97 filesize = os.path.getsize(self.filename) 98 if filesize > self.expectpos: 99 contents = self.readFileFrom(self.filename, self.expectpos, 10000) 100 WiredTigerTestCase.prout('ERROR: ' + self.filename + 101 ' unexpected ' + self.desc + 102 ', contains:\n"' + contents + '"') 103 testcase.fail('unexpected ' + self.desc + ', contains: "' + 104 contents + '"') 105 self.expectpos = filesize 106 107 def checkAdditional(self, testcase, expect): 108 """ 109 Check to see that an additional string has been added to the 110 output file. If it has not, raise it as a test failure. 111 In any case, reset the expected pos to account for the new output. 112 """ 113 if self.file != None: 114 self.file.flush() 115 gotstr = self.readFileFrom(self.filename, self.expectpos, 1000) 116 testcase.assertEqual(gotstr, expect, 'in ' + self.desc + 117 ', expected "' + expect + '", but got "' + 118 gotstr + '"') 119 self.expectpos = os.path.getsize(self.filename) 120 121 def checkAdditionalPattern(self, testcase, pat): 122 """ 123 Check to see that an additional string has been added to the 124 output file. If it has not, raise it as a test failure. 125 In any case, reset the expected pos to account for the new output. 126 """ 127 if self.file != None: 128 self.file.flush() 129 gotstr = self.readFileFrom(self.filename, self.expectpos, 1000) 130 if re.search(pat, gotstr) == None: 131 testcase.fail('in ' + self.desc + 132 ', expected pattern "' + pat + '", but got "' + 133 gotstr + '"') 134 self.expectpos = os.path.getsize(self.filename) 135 136class TestSuiteConnection(object): 137 def __init__(self, conn, connlist): 138 connlist.append(conn) 139 self._conn = conn 140 self._connlist = connlist 141 142 def close(self, config=''): 143 self._connlist.remove(self._conn) 144 return self._conn.close(config) 145 146 # Proxy everything except what we explicitly define to the 147 # wrapped connection 148 def __getattr__(self, attr): 149 if attr in self.__dict__: 150 return getattr(self, attr) 151 else: 152 return getattr(self._conn, attr) 153 154# Just like a list of strings, but with a convenience function 155class ExtensionList(list): 156 skipIfMissing = False 157 def extension(self, dirname, name, extarg=None): 158 if name != None and name != 'none': 159 ext = '' if extarg == None else '=' + extarg 160 self.append(dirname + '/' + name + ext) 161 162class WiredTigerTestCase(unittest.TestCase): 163 _globalSetup = False 164 _printOnceSeen = {} 165 166 # conn_config can be overridden to add to basic connection configuration. 167 # Can be a string or a callable function or lambda expression. 168 conn_config = '' 169 170 # session_config can be overridden to add to basic session configuration. 171 # Can be a string or a callable function or lambda expression. 172 session_config = '' 173 174 # conn_extensions can be overridden to add a list of extensions to load. 175 # Each entry is a string (directory and extension name) and optional config. 176 # Example: 177 # conn_extensions = ('extractors/csv_extractor', 178 # 'test/fail_fs={allow_writes=100}') 179 conn_extensions = () 180 181 @staticmethod 182 def globalSetup(preserveFiles = False, useTimestamp = False, 183 gdbSub = False, lldbSub = False, verbose = 1, builddir = None, dirarg = None, 184 longtest = False): 185 WiredTigerTestCase._preserveFiles = preserveFiles 186 d = 'WT_TEST' if dirarg == None else dirarg 187 if useTimestamp: 188 d += '.' + time.strftime('%Y%m%d-%H%M%S', time.localtime()) 189 shutil.rmtree(d, ignore_errors=True) 190 os.makedirs(d) 191 wtscenario.set_long_run(longtest) 192 WiredTigerTestCase._parentTestdir = d 193 WiredTigerTestCase._builddir = builddir 194 WiredTigerTestCase._origcwd = os.getcwd() 195 WiredTigerTestCase._resultfile = open(os.path.join(d, 'results.txt'), "w", 0) # unbuffered 196 WiredTigerTestCase._gdbSubprocess = gdbSub 197 WiredTigerTestCase._lldbSubprocess = lldbSub 198 WiredTigerTestCase._longtest = longtest 199 WiredTigerTestCase._verbose = verbose 200 WiredTigerTestCase._dupout = os.dup(sys.stdout.fileno()) 201 WiredTigerTestCase._stdout = sys.stdout 202 WiredTigerTestCase._stderr = sys.stderr 203 WiredTigerTestCase._concurrent = False 204 WiredTigerTestCase._globalSetup = True 205 WiredTigerTestCase._ttyDescriptor = None 206 207 def fdSetUp(self): 208 self.captureout = CapturedFd('stdout.txt', 'standard output') 209 self.captureerr = CapturedFd('stderr.txt', 'error output') 210 sys.stdout = self.captureout.capture() 211 sys.stderr = self.captureerr.capture() 212 213 def fdTearDown(self): 214 # restore stderr/stdout 215 self.captureout.release() 216 self.captureerr.release() 217 sys.stdout = WiredTigerTestCase._stdout 218 sys.stderr = WiredTigerTestCase._stderr 219 220 def __init__(self, *args, **kwargs): 221 if hasattr(self, 'scenarios'): 222 assert(len(self.scenarios) == len(dict(self.scenarios))) 223 unittest.TestCase.__init__(self, *args, **kwargs) 224 if not self._globalSetup: 225 WiredTigerTestCase.globalSetup() 226 227 def __str__(self): 228 # when running with scenarios, if the number_scenarios() method 229 # is used, then each scenario is given a number, which can 230 # help distinguish tests. 231 scen = '' 232 if hasattr(self, 'scenario_number') and hasattr(self, 'scenario_name'): 233 scen = ' -s ' + str(self.scenario_number) + \ 234 ' (' + self.scenario_name + ')' 235 return self.simpleName() + scen 236 237 def shortDesc(self): 238 ret_str = '' 239 if hasattr(self, 'scenario_number'): 240 ret_str = ' -s ' + str(self.scenario_number) 241 return self.simpleName() + ret_str 242 243 def simpleName(self): 244 return "%s.%s.%s" % (self.__module__, 245 self.className(), self._testMethodName) 246 247 # Return the wiredtiger_open extension argument for 248 # any needed shared library. 249 def extensionsConfig(self): 250 exts = self.conn_extensions 251 if hasattr(exts, '__call__'): 252 exts = ExtensionList() 253 self.conn_extensions(exts) 254 result = '' 255 extfiles = {} 256 skipIfMissing = False 257 if hasattr(exts, 'skip_if_missing'): 258 skipIfMissing = exts.skip_if_missing 259 for ext in exts: 260 extconf = '' 261 if '=' in ext: 262 splits = ext.split('=', 1) 263 ext = splits[0] 264 extconf = '=' + splits[1] 265 splits = ext.split('/') 266 if len(splits) != 2: 267 raise Exception(self.shortid() + 268 ": " + ext + 269 ": extension is not named <dir>/<name>") 270 libname = splits[1] 271 dirname = splits[0] 272 pat = os.path.join(WiredTigerTestCase._builddir, 'ext', 273 dirname, libname, '.libs', 'libwiredtiger_*.so') 274 filenames = glob.glob(pat) 275 if len(filenames) == 0: 276 if skipIfMissing: 277 self.skipTest('extension "' + ext + '" not built') 278 continue 279 else: 280 raise Exception(self.shortid() + 281 ": " + ext + 282 ": no extensions library found matching: " + pat) 283 elif len(filenames) > 1: 284 raise Exception(self.shortid() + 285 ": " + ext + 286 ": multiple extensions libraries found matching: " + pat) 287 complete = '"' + filenames[0] + '"' + extconf 288 if ext in extfiles: 289 if extfiles[ext] != complete: 290 raise Exception(self.shortid() + 291 ": non-matching extension arguments in " + 292 str(exts)) 293 else: 294 extfiles[ext] = complete 295 if len(extfiles) != 0: 296 result = ',extensions=[' + ','.join(extfiles.values()) + ']' 297 return result 298 299 # Can be overridden, but first consider setting self.conn_config 300 # or self.conn_extensions 301 def setUpConnectionOpen(self, home): 302 self.home = home 303 config = self.conn_config 304 if hasattr(config, '__call__'): 305 config = self.conn_config() 306 config += self.extensionsConfig() 307 # In case the open starts additional threads, flush first to 308 # avoid confusion. 309 sys.stdout.flush() 310 conn_param = 'create,error_prefix="%s",%s' % (self.shortid(), config) 311 try: 312 conn = self.wiredtiger_open(home, conn_param) 313 except wiredtiger.WiredTigerError as e: 314 print "Failed wiredtiger_open: dir '%s', config '%s'" % \ 315 (home, conn_param) 316 raise e 317 self.pr(`conn`) 318 return conn 319 320 # Replacement for wiredtiger.wiredtiger_open that returns 321 # a proxied connection that knows to close it itself at the 322 # end of the run, unless it was already closed. 323 def wiredtiger_open(self, home=None, config=''): 324 conn = wiredtiger.wiredtiger_open(home, config) 325 return TestSuiteConnection(conn, self._connections) 326 327 # Can be overridden, but first consider setting self.session_config 328 def setUpSessionOpen(self, conn): 329 config = self.session_config 330 if hasattr(config, '__call__'): 331 config = self.session_config() 332 return conn.open_session(config) 333 334 # Can be overridden 335 def close_conn(self, config=''): 336 """ 337 Close the connection if already open. 338 """ 339 if self.conn != None: 340 self.conn.close(config) 341 self.conn = None 342 343 def open_conn(self, directory=".", config=None): 344 """ 345 Open the connection if already closed. 346 """ 347 if self.conn == None: 348 if config != None: 349 self._old_config = self.conn_config 350 self.conn_config = config 351 self.conn = self.setUpConnectionOpen(directory) 352 if config != None: 353 self.conn_config = self._old_config 354 self.session = self.setUpSessionOpen(self.conn) 355 356 def reopen_conn(self, directory=".", config=None): 357 """ 358 Reopen the connection. 359 """ 360 self.close_conn() 361 self.open_conn(directory, config) 362 363 def setUp(self): 364 if not hasattr(self.__class__, 'wt_ntests'): 365 self.__class__.wt_ntests = 0 366 if WiredTigerTestCase._concurrent: 367 self.testsubdir = self.shortid() + '.' + str(self.__class__.wt_ntests) 368 else: 369 self.testsubdir = self.className() + '.' + str(self.__class__.wt_ntests) 370 self.testdir = os.path.join(WiredTigerTestCase._parentTestdir, self.testsubdir) 371 self.__class__.wt_ntests += 1 372 self.starttime = time.time() 373 if WiredTigerTestCase._verbose > 2: 374 self.prhead('started in ' + self.testdir, True) 375 # tearDown needs connections list, set it here in case the open fails. 376 self._connections = [] 377 self.origcwd = os.getcwd() 378 shutil.rmtree(self.testdir, ignore_errors=True) 379 if os.path.exists(self.testdir): 380 raise Exception(self.testdir + ": cannot remove directory") 381 os.makedirs(self.testdir) 382 os.chdir(self.testdir) 383 with open('testname.txt', 'w+') as namefile: 384 namefile.write(str(self) + '\n') 385 self.fdSetUp() 386 # tearDown needs a conn field, set it here in case the open fails. 387 self.conn = None 388 try: 389 self.conn = self.setUpConnectionOpen(".") 390 self.session = self.setUpSessionOpen(self.conn) 391 except: 392 self.tearDown() 393 raise 394 395 def tearDown(self): 396 excinfo = sys.exc_info() 397 passed = (excinfo == (None, None, None)) 398 if passed: 399 skipped = False 400 else: 401 skipped = (excinfo[0] == unittest.SkipTest) 402 self.pr('finishing') 403 404 # Close all connections that weren't explicitly closed. 405 # Connections left open (as a result of a test failure) 406 # can result in cascading errors. We also make sure 407 # self.conn is on the list of active connections. 408 if not self.conn in self._connections: 409 self._connections.append(self.conn) 410 for conn in self._connections: 411 try: 412 conn.close() 413 except: 414 pass 415 self._connections = [] 416 417 try: 418 self.fdTearDown() 419 # Only check for unexpected output if the test passed 420 if passed: 421 self.captureout.check(self) 422 self.captureerr.check(self) 423 finally: 424 # always get back to original directory 425 os.chdir(self.origcwd) 426 427 # Make sure no read-only files or directories were left behind 428 os.chmod(self.testdir, 0777) 429 for root, dirs, files in os.walk(self.testdir): 430 for d in dirs: 431 os.chmod(os.path.join(root, d), 0777) 432 for f in files: 433 os.chmod(os.path.join(root, f), 0666) 434 435 # Clean up unless there's a failure 436 if (passed or skipped) and not WiredTigerTestCase._preserveFiles: 437 shutil.rmtree(self.testdir, ignore_errors=True) 438 else: 439 self.pr('preserving directory ' + self.testdir) 440 441 elapsed = time.time() - self.starttime 442 if elapsed > 0.001 and WiredTigerTestCase._verbose >= 2: 443 print "%s: %.2f seconds" % (str(self), elapsed) 444 if not passed and not skipped: 445 print "ERROR in " + str(self) 446 self.pr('FAIL') 447 self.prexception(excinfo) 448 self.pr('preserving directory ' + self.testdir) 449 if WiredTigerTestCase._verbose > 2: 450 self.prhead('TEST COMPLETED') 451 452 def backup(self, backup_dir, session=None): 453 if session is None: 454 session = self.session 455 shutil.rmtree(backup_dir, ignore_errors=True) 456 os.mkdir(backup_dir) 457 bkp_cursor = session.open_cursor('backup:', None, None) 458 while True: 459 ret = bkp_cursor.next() 460 if ret != 0: 461 break 462 shutil.copy(bkp_cursor.get_key(), backup_dir) 463 self.assertEqual(ret, wiredtiger.WT_NOTFOUND) 464 bkp_cursor.close() 465 466 @contextmanager 467 def expectedStdout(self, expect): 468 self.captureout.check(self) 469 yield 470 self.captureout.checkAdditional(self, expect) 471 472 @contextmanager 473 def expectedStderr(self, expect): 474 self.captureerr.check(self) 475 yield 476 self.captureerr.checkAdditional(self, expect) 477 478 @contextmanager 479 def expectedStdoutPattern(self, pat): 480 self.captureout.check(self) 481 yield 482 self.captureout.checkAdditionalPattern(self, pat) 483 484 @contextmanager 485 def expectedStderrPattern(self, pat): 486 self.captureerr.check(self) 487 yield 488 self.captureerr.checkAdditionalPattern(self, pat) 489 490 def assertRaisesWithMessage(self, exceptionType, expr, message): 491 """ 492 Like TestCase.assertRaises(), but also checks to see 493 that a message is printed on stderr. If message starts 494 and ends with a slash, it is considered a pattern that 495 must appear in stderr (it need not encompass the entire 496 error output). Otherwise, the message must match verbatim, 497 including any trailing newlines. 498 """ 499 if len(message) > 2 and message[0] == '/' and message[-1] == '/': 500 with self.expectedStderrPattern(message[1:-1]): 501 self.assertRaises(exceptionType, expr) 502 else: 503 with self.expectedStderr(message): 504 self.assertRaises(exceptionType, expr) 505 506 def assertRaisesException(self, exceptionType, expr, 507 exceptionString=None, optional=False): 508 """ 509 Like TestCase.assertRaises(), with some additional options. 510 If the exceptionString argument is used, the exception's string 511 must match it. If optional is set, then no assertion occurs 512 if the exception doesn't occur. 513 Returns true if the assertion is raised. 514 """ 515 raised = False 516 try: 517 expr() 518 except BaseException, err: 519 if not isinstance(err, exceptionType): 520 self.fail('Exception of incorrect type raised, got type: ' + \ 521 str(type(err))) 522 if exceptionString != None and exceptionString != str(err): 523 self.fail('Exception with incorrect string raised, got: "' + \ 524 str(err) + '"') 525 raised = True 526 if not raised and not optional: 527 self.fail('no assertion raised') 528 return raised 529 530 def raisesBusy(self, expr): 531 """ 532 Execute the expression, returning true if a 'Resource busy' 533 exception is raised, returning false if no exception is raised. 534 Any other exception raises a test suite failure. 535 """ 536 return self.assertRaisesException(wiredtiger.WiredTigerError, \ 537 expr, exceptionString='Resource busy', optional=True) 538 539 def assertTimestampsEqual(self, ts1, ts2): 540 """ 541 TestCase.assertEqual() for timestamps 542 """ 543 self.assertEqual(int(ts1, 16), int(ts2, 16)) 544 545 def exceptionToStderr(self, expr): 546 """ 547 Used by assertRaisesHavingMessage to convert an expression 548 that throws an error to an expression that throws the 549 same error but also has the exception string on stderr. 550 """ 551 try: 552 expr() 553 except BaseException, err: 554 sys.stderr.write('Exception: ' + str(err)) 555 raise 556 557 def assertRaisesHavingMessage(self, exceptionType, expr, message): 558 """ 559 Like TestCase.assertRaises(), but also checks to see 560 that the assert exception, when string-ified, includes a message. 561 If message starts and ends with a slash, it is considered a pattern that 562 must appear (it need not encompass the entire message). 563 Otherwise, the message must match verbatim. 564 """ 565 self.assertRaisesWithMessage( 566 exceptionType, lambda: self.exceptionToStderr(expr), message) 567 568 @staticmethod 569 def printOnce(msg): 570 # There's a race condition with multiple threads, 571 # but we won't worry about it. We err on the side 572 # of printing the message too many times. 573 if not msg in WiredTigerTestCase._printOnceSeen: 574 WiredTigerTestCase._printOnceSeen[msg] = msg 575 WiredTigerTestCase.prout(msg) 576 577 def KNOWN_FAILURE(self, name): 578 myname = self.simpleName() 579 msg = '**** ' + myname + ' HAS A KNOWN FAILURE: ' + name + ' ****' 580 self.printOnce(msg) 581 self.skipTest('KNOWN FAILURE: ' + name) 582 583 def KNOWN_LIMITATION(self, name): 584 myname = self.simpleName() 585 msg = '**** ' + myname + ' HAS A KNOWN LIMITATION: ' + name + ' ****' 586 self.printOnce(msg) 587 588 @staticmethod 589 def printVerbose(level, message): 590 if level <= WiredTigerTestCase._verbose: 591 WiredTigerTestCase.prout(message) 592 593 def verbose(self, level, message): 594 WiredTigerTestCase.printVerbose(level, message) 595 596 def prout(self, s): 597 WiredTigerTestCase.prout(s) 598 599 @staticmethod 600 def prout(s): 601 os.write(WiredTigerTestCase._dupout, s + '\n') 602 603 def pr(self, s): 604 """ 605 print a progress line for testing 606 """ 607 msg = ' ' + self.shortid() + ': ' + s 608 WiredTigerTestCase._resultfile.write(msg + '\n') 609 610 def prhead(self, s, *beginning): 611 """ 612 print a header line for testing, something important 613 """ 614 msg = '' 615 if len(beginning) > 0: 616 msg += '\n' 617 msg += ' ' + self.shortid() + ': ' + s 618 self.prout(msg) 619 WiredTigerTestCase._resultfile.write(msg + '\n') 620 621 def prexception(self, excinfo): 622 WiredTigerTestCase._resultfile.write('\n') 623 traceback.print_exception(excinfo[0], excinfo[1], excinfo[2], None, WiredTigerTestCase._resultfile) 624 WiredTigerTestCase._resultfile.write('\n') 625 626 # print directly to tty, useful for debugging 627 def tty(self, message): 628 WiredTigerTestCase.tty(message) 629 630 @staticmethod 631 def tty(message): 632 if WiredTigerTestCase._ttyDescriptor == None: 633 WiredTigerTestCase._ttyDescriptor = open('/dev/tty', 'w') 634 WiredTigerTestCase._ttyDescriptor.write(message + '\n') 635 636 def ttyVerbose(self, level, message): 637 WiredTigerTestCase.ttyVerbose(level, message) 638 639 @staticmethod 640 def ttyVerbose(level, message): 641 if level <= WiredTigerTestCase._verbose: 642 WiredTigerTestCase.tty(message) 643 644 def shortid(self): 645 return self.id().replace("__main__.","") 646 647 def className(self): 648 return self.__class__.__name__ 649 650def longtest(description): 651 """ 652 Used as a function decorator, for example, @wttest.longtest("description"). 653 The decorator indicates that this test function should only be included 654 when running the test suite with the --long option. 655 """ 656 def runit_decorator(func): 657 return func 658 if not WiredTigerTestCase._longtest: 659 return unittest.skip(description + ' (enable with --long)') 660 else: 661 return runit_decorator 662 663def islongtest(): 664 return WiredTigerTestCase._longtest 665 666def runsuite(suite, parallel): 667 suite_to_run = suite 668 if parallel > 1: 669 from concurrencytest import ConcurrentTestSuite, fork_for_tests 670 if not WiredTigerTestCase._globalSetup: 671 WiredTigerTestCase.globalSetup() 672 WiredTigerTestCase._concurrent = True 673 suite_to_run = ConcurrentTestSuite(suite, fork_for_tests(parallel)) 674 try: 675 return unittest.TextTestRunner( 676 verbosity=WiredTigerTestCase._verbose).run(suite_to_run) 677 except BaseException as e: 678 # This should not happen for regular test errors, unittest should catch everything 679 print('ERROR: running test: ', e) 680 raise e 681 682def run(name='__main__'): 683 result = runsuite(unittest.TestLoader().loadTestsFromName(name), False) 684 sys.exit(0 if result.wasSuccessful() else 1) 685