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