diff options
Diffstat (limited to 'src/third_party/wiredtiger/test/suite/wttest.py')
-rw-r--r-- | src/third_party/wiredtiger/test/suite/wttest.py | 546 |
1 files changed, 546 insertions, 0 deletions
diff --git a/src/third_party/wiredtiger/test/suite/wttest.py b/src/third_party/wiredtiger/test/suite/wttest.py new file mode 100644 index 00000000000..b5a58d1566f --- /dev/null +++ b/src/third_party/wiredtiger/test/suite/wttest.py @@ -0,0 +1,546 @@ +#!/usr/bin/env python +# +# Public Domain 2014-2016 MongoDB, Inc. +# Public Domain 2008-2014 WiredTiger, Inc. +# +# This is free and unencumbered software released into the public domain. +# +# Anyone is free to copy, modify, publish, use, compile, sell, or +# distribute this software, either in source code form or as a compiled +# binary, for any purpose, commercial or non-commercial, and by any +# means. +# +# In jurisdictions that recognize copyright laws, the author or authors +# of this software dedicate any and all copyright interest in the +# software to the public domain. We make this dedication for the benefit +# of the public at large and to the detriment of our heirs and +# successors. We intend this dedication to be an overt act of +# relinquishment in perpetuity of all present and future rights to this +# software under copyright law. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# WiredTigerTestCase +# parent class for all test cases +# + +# If unittest2 is available, use it in preference to (the old) unittest +try: + import unittest2 as unittest +except ImportError: + import unittest + +from contextlib import contextmanager +import os, re, shutil, sys, time, traceback +import wtscenario +import wiredtiger + +def shortenWithEllipsis(s, maxlen): + if len(s) > maxlen: + s = s[0:maxlen-3] + '...' + return s + +class CapturedFd(object): + """ + CapturedFd encapsulates a file descriptor (e.g. 1 or 2) that is diverted + to a file. We use this to capture and check the C stdout/stderr. + Meanwhile we reset Python's sys.stdout, sys.stderr, using duped copies + of the original 1, 2 fds. The end result is that Python's sys.stdout + sys.stderr behave normally (e.g. go to the tty), while the C stdout/stderr + ends up in a file that we can verify. + """ + def __init__(self, filename, desc): + self.filename = filename + self.desc = desc + self.expectpos = 0 + self.file = None + + def readFileFrom(self, filename, pos, maxchars): + """ + Read a file starting at a given position, + returning the beginning of its contents + """ + with open(filename, 'r') as f: + f.seek(pos) + return shortenWithEllipsis(f.read(maxchars+1), maxchars) + + def capture(self): + """ + Start capturing the file descriptor. + Note that the original targetFd is closed, we expect + that the caller has duped it and passed the dup to us + in the constructor. + """ + self.file = open(self.filename, 'w') + return self.file + + def release(self): + """ + Stop capturing. + """ + self.file.close() + self.file = None + + def check(self, testcase): + """ + Check to see that there is no unexpected output in the captured output + file. If there is, raise it as a test failure. + This is generally called after 'release' is called. + """ + if self.file != None: + self.file.flush() + filesize = os.path.getsize(self.filename) + if filesize > self.expectpos: + contents = self.readFileFrom(self.filename, self.expectpos, 10000) + WiredTigerTestCase.prout('ERROR: ' + self.filename + + ' unexpected ' + self.desc + + ', contains:\n"' + contents + '"') + testcase.fail('unexpected ' + self.desc + ', contains: "' + + shortenWithEllipsis(contents,100) + '"') + self.expectpos = filesize + + def checkAdditional(self, testcase, expect): + """ + Check to see that an additional string has been added to the + output file. If it has not, raise it as a test failure. + In any case, reset the expected pos to account for the new output. + """ + if self.file != None: + self.file.flush() + gotstr = self.readFileFrom(self.filename, self.expectpos, 1000) + testcase.assertEqual(gotstr, expect, 'in ' + self.desc + + ', expected "' + expect + '", but got "' + + gotstr + '"') + self.expectpos = os.path.getsize(self.filename) + + def checkAdditionalPattern(self, testcase, pat): + """ + Check to see that an additional string has been added to the + output file. If it has not, raise it as a test failure. + In any case, reset the expected pos to account for the new output. + """ + if self.file != None: + self.file.flush() + gotstr = self.readFileFrom(self.filename, self.expectpos, 1000) + if re.search(pat, gotstr) == None: + testcase.fail('in ' + self.desc + + ', expected pattern "' + pat + '", but got "' + + gotstr + '"') + self.expectpos = os.path.getsize(self.filename) + + +class TestSuiteConnection(object): + def __init__(self, conn, connlist): + connlist.append(conn) + self._conn = conn + self._connlist = connlist + + def close(self): + self._connlist.remove(self._conn) + return self._conn.close() + + # Proxy everything except what we explicitly define to the + # wrapped connection + def __getattr__(self, attr): + if attr in self.__dict__: + return getattr(self, attr) + else: + return getattr(self._conn, attr) + + +class WiredTigerTestCase(unittest.TestCase): + _globalSetup = False + _printOnceSeen = {} + + # conn_config can be overridden to add to basic connection configuration. + # Can be a string or a callable function or lambda expression. + conn_config = '' + + @staticmethod + def globalSetup(preserveFiles = False, useTimestamp = False, + gdbSub = False, verbose = 1, dirarg = None, + longtest = False): + WiredTigerTestCase._preserveFiles = preserveFiles + d = 'WT_TEST' if dirarg == None else dirarg + if useTimestamp: + d += '.' + time.strftime('%Y%m%d-%H%M%S', time.localtime()) + shutil.rmtree(d, ignore_errors=True) + os.makedirs(d) + wtscenario.set_long_run(longtest) + WiredTigerTestCase._parentTestdir = d + WiredTigerTestCase._origcwd = os.getcwd() + WiredTigerTestCase._resultfile = open(os.path.join(d, 'results.txt'), "w", 0) # unbuffered + WiredTigerTestCase._gdbSubprocess = gdbSub + WiredTigerTestCase._longtest = longtest + WiredTigerTestCase._verbose = verbose + WiredTigerTestCase._dupout = os.dup(sys.stdout.fileno()) + WiredTigerTestCase._stdout = sys.stdout + WiredTigerTestCase._stderr = sys.stderr + WiredTigerTestCase._concurrent = False + WiredTigerTestCase._globalSetup = True + WiredTigerTestCase._ttyDescriptor = None + + def fdSetUp(self): + self.captureout = CapturedFd('stdout.txt', 'standard output') + self.captureerr = CapturedFd('stderr.txt', 'error output') + sys.stdout = self.captureout.capture() + sys.stderr = self.captureerr.capture() + + def fdTearDown(self): + # restore stderr/stdout + self.captureout.release() + self.captureerr.release() + sys.stdout = WiredTigerTestCase._stdout + sys.stderr = WiredTigerTestCase._stderr + + def __init__(self, *args, **kwargs): + if hasattr(self, 'scenarios'): + assert(len(self.scenarios) == len(dict(self.scenarios))) + unittest.TestCase.__init__(self, *args, **kwargs) + if not self._globalSetup: + WiredTigerTestCase.globalSetup() + + def __str__(self): + # when running with scenarios, if the number_scenarios() method + # is used, then each scenario is given a number, which can + # help distinguish tests. + scen = '' + if hasattr(self, 'scenario_number') and hasattr(self, 'scenario_name'): + scen = '(scenario ' + str(self.scenario_number) + \ + ': ' + self.scenario_name + ')' + return self.simpleName() + scen + + def simpleName(self): + return "%s.%s.%s" % (self.__module__, + self.className(), self._testMethodName) + + # Can be overridden, but first consider setting self.conn_config . + def setUpConnectionOpen(self, home): + self.home = home + config = self.conn_config + if hasattr(config, '__call__'): + config = config(home) + # In case the open starts additional threads, flush first to + # avoid confusion. + sys.stdout.flush() + conn_param = 'create,error_prefix="%s: ",%s' % (self.shortid(), config) + try: + conn = self.wiredtiger_open(home, conn_param) + except wiredtiger.WiredTigerError as e: + print "Failed wiredtiger_open: dir '%s', config '%s'" % \ + (home, conn_param) + raise e + self.pr(`conn`) + return conn + + # Replacement for wiredtiger.wiredtiger_open that returns + # a proxied connection that knows to close it itself at the + # end of the run, unless it was already closed. + def wiredtiger_open(self, home=None, config=''): + conn = wiredtiger.wiredtiger_open(home, config) + return TestSuiteConnection(conn, self._connections) + + # Can be overridden + def setUpSessionOpen(self, conn): + return conn.open_session(None) + + # Can be overridden + def close_conn(self): + """ + Close the connection if already open. + """ + if self.conn != None: + self.conn.close() + self.conn = None + + def open_conn(self): + """ + Open the connection if already closed. + """ + if self.conn == None: + self.conn = self.setUpConnectionOpen(".") + self.session = self.setUpSessionOpen(self.conn) + + def reopen_conn(self): + """ + Reopen the connection. + """ + self.close_conn() + self.open_conn() + + def setUp(self): + if not hasattr(self.__class__, 'wt_ntests'): + self.__class__.wt_ntests = 0 + if WiredTigerTestCase._concurrent: + self.testsubdir = self.shortid() + '.' + str(self.__class__.wt_ntests) + else: + self.testsubdir = self.className() + '.' + str(self.__class__.wt_ntests) + self.testdir = os.path.join(WiredTigerTestCase._parentTestdir, self.testsubdir) + self.__class__.wt_ntests += 1 + if WiredTigerTestCase._verbose > 2: + self.prhead('started in ' + self.testdir, True) + # tearDown needs connections list, set it here in case the open fails. + self._connections = [] + self.origcwd = os.getcwd() + shutil.rmtree(self.testdir, ignore_errors=True) + if os.path.exists(self.testdir): + raise Exception(self.testdir + ": cannot remove directory") + os.makedirs(self.testdir) + os.chdir(self.testdir) + self.fdSetUp() + # tearDown needs a conn field, set it here in case the open fails. + self.conn = None + try: + self.conn = self.setUpConnectionOpen(".") + self.session = self.setUpSessionOpen(self.conn) + except: + self.tearDown() + raise + + def tearDown(self): + excinfo = sys.exc_info() + passed = (excinfo == (None, None, None)) + if passed: + skipped = False + else: + skipped = (excinfo[0] == unittest.SkipTest) + self.pr('finishing') + + # Close all connections that weren't explicitly closed. + # Connections left open (as a result of a test failure) + # can result in cascading errors. We also make sure + # self.conn is on the list of active connections. + if not self.conn in self._connections: + self._connections.append(self.conn) + for conn in self._connections: + try: + conn.close() + except: + pass + self._connections = [] + + try: + self.fdTearDown() + # Only check for unexpected output if the test passed + if passed: + self.captureout.check(self) + self.captureerr.check(self) + finally: + # always get back to original directory + os.chdir(self.origcwd) + + # Clean up unless there's a failure + if (passed or skipped) and not WiredTigerTestCase._preserveFiles: + shutil.rmtree(self.testdir, ignore_errors=True) + else: + self.pr('preserving directory ' + self.testdir) + + if not passed and not skipped: + print "ERROR in " + str(self) + self.pr('FAIL') + self.prexception(excinfo) + self.pr('preserving directory ' + self.testdir) + if WiredTigerTestCase._verbose > 2: + self.prhead('TEST COMPLETED') + + def backup(self, backup_dir, session=None): + if session is None: + session = self.session + shutil.rmtree(backup_dir, ignore_errors=True) + os.mkdir(backup_dir) + bkp_cursor = session.open_cursor('backup:', None, None) + while True: + ret = bkp_cursor.next() + if ret != 0: + break + shutil.copy(bkp_cursor.get_key(), backup_dir) + self.assertEqual(ret, wiredtiger.WT_NOTFOUND) + bkp_cursor.close() + + @contextmanager + def expectedStdout(self, expect): + self.captureout.check(self) + yield + self.captureout.checkAdditional(self, expect) + + @contextmanager + def expectedStderr(self, expect): + self.captureerr.check(self) + yield + self.captureerr.checkAdditional(self, expect) + + @contextmanager + def expectedStdoutPattern(self, pat): + self.captureout.check(self) + yield + self.captureout.checkAdditionalPattern(self, pat) + + @contextmanager + def expectedStderrPattern(self, pat): + self.captureerr.check(self) + yield + self.captureerr.checkAdditionalPattern(self, pat) + + def assertRaisesWithMessage(self, exceptionType, expr, message): + """ + Like TestCase.assertRaises(), but also checks to see + that a message is printed on stderr. If message starts + and ends with a slash, it is considered a pattern that + must appear in stderr (it need not encompass the entire + error output). Otherwise, the message must match verbatim, + including any trailing newlines. + """ + if len(message) > 2 and message[0] == '/' and message[-1] == '/': + with self.expectedStderrPattern(message[1:-1]): + self.assertRaises(exceptionType, expr) + else: + with self.expectedStderr(message): + self.assertRaises(exceptionType, expr) + + def exceptionToStderr(self, expr): + """ + Used by assertRaisesHavingMessage to convert an expression + that throws an error to an expression that throws the + same error but also has the exception string on stderr. + """ + try: + expr() + except BaseException, err: + sys.stderr.write('Exception: ' + str(err)) + raise + + def assertRaisesHavingMessage(self, exceptionType, expr, message): + """ + Like TestCase.assertRaises(), but also checks to see + that the assert exception, when string-ified, includes a message. + If message starts and ends with a slash, it is considered a pattern that + must appear (it need not encompass the entire message). + Otherwise, the message must match verbatim. + """ + self.assertRaisesWithMessage( + exceptionType, lambda: self.exceptionToStderr(expr), message) + + @staticmethod + def printOnce(msg): + # There's a race condition with multiple threads, + # but we won't worry about it. We err on the side + # of printing the message too many times. + if not msg in WiredTigerTestCase._printOnceSeen: + WiredTigerTestCase._printOnceSeen[msg] = msg + WiredTigerTestCase.prout(msg) + + def KNOWN_FAILURE(self, name): + myname = self.simpleName() + msg = '**** ' + myname + ' HAS A KNOWN FAILURE: ' + name + ' ****' + self.printOnce(msg) + self.skipTest('KNOWN FAILURE: ' + name) + + def KNOWN_LIMITATION(self, name): + myname = self.simpleName() + msg = '**** ' + myname + ' HAS A KNOWN LIMITATION: ' + name + ' ****' + self.printOnce(msg) + + @staticmethod + def printVerbose(level, message): + if level <= WiredTigerTestCase._verbose: + WiredTigerTestCase.prout(message) + + def verbose(self, level, message): + WiredTigerTestCase.printVerbose(level, message) + + def prout(self, s): + WiredTigerTestCase.prout(s) + + @staticmethod + def prout(s): + os.write(WiredTigerTestCase._dupout, s + '\n') + + def pr(self, s): + """ + print a progress line for testing + """ + msg = ' ' + self.shortid() + ': ' + s + WiredTigerTestCase._resultfile.write(msg + '\n') + + def prhead(self, s, *beginning): + """ + print a header line for testing, something important + """ + msg = '' + if len(beginning) > 0: + msg += '\n' + msg += ' ' + self.shortid() + ': ' + s + self.prout(msg) + WiredTigerTestCase._resultfile.write(msg + '\n') + + def prexception(self, excinfo): + WiredTigerTestCase._resultfile.write('\n') + traceback.print_exception(excinfo[0], excinfo[1], excinfo[2], None, WiredTigerTestCase._resultfile) + WiredTigerTestCase._resultfile.write('\n') + + # print directly to tty, useful for debugging + def tty(self, message): + WiredTigerTestCase.tty(message) + + @staticmethod + def tty(message): + if WiredTigerTestCase._ttyDescriptor == None: + WiredTigerTestCase._ttyDescriptor = open('/dev/tty', 'w') + WiredTigerTestCase._ttyDescriptor.write(message + '\n') + + def ttyVerbose(self, level, message): + WiredTigerTestCase.ttyVerbose(level, message) + + @staticmethod + def ttyVerbose(level, message): + if level <= WiredTigerTestCase._verbose: + WiredTigerTestCase.tty(message) + + def shortid(self): + return self.id().replace("__main__.","") + + def className(self): + return self.__class__.__name__ + + +def longtest(description): + """ + Used as a function decorator, for example, @wttest.longtest("description"). + The decorator indicates that this test function should only be included + when running the test suite with the --long option. + """ + def runit_decorator(func): + return func + if not WiredTigerTestCase._longtest: + return unittest.skip(description + ' (enable with --long)') + else: + return runit_decorator + +def islongtest(): + return WiredTigerTestCase._longtest + +def runsuite(suite, parallel): + suite_to_run = suite + if parallel > 1: + from concurrencytest import ConcurrentTestSuite, fork_for_tests + if not WiredTigerTestCase._globalSetup: + WiredTigerTestCase.globalSetup() + WiredTigerTestCase._concurrent = True + suite_to_run = ConcurrentTestSuite(suite, fork_for_tests(parallel)) + try: + return unittest.TextTestRunner( + verbosity=WiredTigerTestCase._verbose).run(suite_to_run) + except BaseException as e: + # This should not happen for regular test errors, unittest should catch everything + print('ERROR: running test: ', e) + raise e + +def run(name='__main__'): + result = runsuite(unittest.TestLoader().loadTestsFromName(name), False) + sys.exit(not result.wasSuccessful()) |