#!/usr/bin/env python # # Public Domain 2014-present 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. # # syscall.py # Command line syscall test runner # # Usage: python syscall.py [ options ] # # For each .run file below the current directory, run the corresponding # program and collect strace output, comparing it to the contents of # the .run file. # # Options: # --preserve preserve the outputs in a WT_TEST.* subdirectory # of the build directory. # --verbose verbose output to show step by step how run files are # compared to output files. # HOW TO DEBUG FAILURES OR CREATE A NEW TEST # # It will be helpful to look at an existing run file (ending in .run) # while reading this. If you are debugging a failure, also have the # output file (stderr.txt) available for reference. These files are # generated in a WT_TEST.* subdirectory of the build directory and # preserved in case of a failure, or when the --preserve option is used. # # For each run file under this directory, this script runs the program # built for that directory under the 'strace' program and captures the # output from that. (On OS/X it runs 'dtruss' instead of 'strace', otherwise # it is largely the same). We want to compare the strace output to a known # reference. The program typically has some of its own output, this is # interleaved into the strace output and provides 'anchor points' during # the comparison. # # The purpose of this output comparison is to determine if there are any # system calls that we should be doing that are not happening. We'd also catch # if there are any extra syscalls that we are doing. For example, if we # are expecting that some operation, like WT_SESSION->create, must do an # fdatasync at a particular point to enforce durability guarantees, # it would be pretty bad if a future code change inadvertently stopped # doing the fdatasync. This wouldn't be picked up by normal testing. It might # be detected by asynchronously killing a test run and seeing if a # recovered database gives proper results. Or it might not. This script # attempts to add certainty to our guarantees. # # The run file is a template for what the resulting strace output should # look like. The challenge is that seemingly minor changes to WiredTiger # implementation or even runtime libraries may change what the overall output # looks like. The run file can be written to allow runs that are resilient # against such changes. # # This script's first action is to read the run file after it is run through # the 'cpp' preprocessor. That means that the run file can use #ifdefs, # #defines and #includes, as well as /**/ and // comments. The output # of the preprocessor is then parsed. We expect to see a few directives # first, each has a string argument as described here: # # SYSTEM("....."); to tell us what system the script can run on, the arg # currently is either "Linux" and "Darwin". # TRACE("....."); a comma separated list of system calls that # we are looking at. Other system calls are ignored. # RUN(""); arguments to the executable. # # When the RUN directive is seen, it indicates that this header portion is # complete, there are no more directives. At this point, the target program # is executed via strace. The remaining part of the run file is used to # match the output of strace. # # The string '...' in the run file matches anything, and can be used to skip # over system dependent parts of the strace output. If '...' appears on a line # by itself, it matches any number of lines. If it appears immediately after a # string, it matches a string that begins with the pattern. (e.g. "foo"... # matches any string that starts with "foo"). It can also appear as a function # argument where it matches any number of arguments. # # Lines of strace generally look something like: # open("./WiredTiger.lock", O_RDWR|O_CREAT|O_CLOEXEC, 0666) = 3 # # where the result of the syscall appears at the end. A matching line in # a run file could look like this: # fd = open("./WiredTiger.lock", O_RDWR|O_CREAT|O_CLOEXEC, 0666); # # or: # fd = open("./WiredTiger"..., O_RDWR|O_CREAT|O_CLOEXEC, 0666); # # or: # fd = open("./WiredTiger"..., ...); # # In each of these cases, the 'fd' (which can be any variable name) becomes # bound to the value in the strace output, in this case '3'. So if later the # run file contains: # write(fd, ""..., 20); # # then we would expect this to match strace output for a write of 20 bytes # using file descriptor 3. # # Expressions are evaluated using the Python parser, so that # hex and octal numbers are accepted, and constant values can be or-ed. # Some limited number of defines are known (see 'defines_used' below), # so that the run file can contain 'O_RDONLY' and it will match a numeric # expression (as it appears in the output of dtruss on OS/X). from __future__ import print_function import argparse, distutils.spawn, fnmatch, os, platform, re, shutil, \ subprocess, sys # A class that represents a context in which predefined constants can be # set, and new variables can be assigned. class VariableContext(object): def __getitem__(self, key): if key not in dir(self) or key.startswith('__'): raise KeyError(key) return getattr(self, key) def __setitem__(self, key, value): setattr(self, key, value) ################################################################ # Changable parameters # We expect these values to evolve as tests are added or modified. # Generally, system calls must be wrapped in an ASSERT_XX() "macro". # Exceptions are calls in this list that return 0 on success, or # those that are hardcoded in Runner.call_compare() calls_returning_zero = [ 'close', 'ftruncate', 'fdatasync', 'rename' ] # Encapsulate all the defines we can use in our scripts. # When this program is run, we'll find out their actual values on # the host system. defines_used = [ 'HAVE_FTRUNCATE', 'O_ACCMODE', 'O_APPEND', 'O_ASYNC', 'O_CLOEXEC', 'O_CREAT', 'O_EXCL', 'O_EXLOCK', 'O_NOATIME', 'O_NOFOLLOW', 'O_NONBLOCK', 'O_RDONLY', 'O_RDWR', 'O_SHLOCK', 'O_TRUNC', 'O_WRONLY', 'WT_USE_OPENAT' ] ################################################################ # Patterns that are used to match the .run file and/or the output. ident = r'([a-zA-Z_][a-zA-Z0-9_]*)' outputpat = re.compile(r'OUTPUT\("([^"]*)"\)') argpat = re.compile(r'''((?:[^,"']|"[^"]*"|'[^']*')+)''') discardpat = re.compile(r';') # e.g. fd = open("blah", 0, 0); assignpat = re.compile(ident + r'\s*=\s*' + ident + r'(\([^;]*\));') # e.g. ASSERT_EQ(close(fd), 0); assertpat = re.compile(r'ASSERT_([ENLG][QET])\s*\(\s*' + ident + r'\s*(\(.*\))\s*,\s*([a-zA-Z0-9_]+)\);') # e.g. close(fd); must return 0 callpat = re.compile(ident + r'(\(.*\));') # e.g. open("blah", 0x0, 0x0) = 6 0 # We capture the errno (e.g. "0" or "Err#60"), but don't do anything with it. # We don't currently test anything that is errno dependent. dtruss_pat = re.compile(ident + r'(\(.*\))\s*=\s*(-*[0-9xA-F]+)\s+([-A-Za-z#0-9]*)') # At the top of the dtruss output is a fixed string. dtruss_init_pat = re.compile(r'\s*SYSCALL\(args\)\s*=\s*return\s*') strace_pat = re.compile(ident + r'(\(.*\))\s*=\s(-*[0-9]+)()') tracepat = re.compile(r'TRACE\("([^"]*)"\)') runpat = re.compile(r'RUN\(([^\)]*)\)') systempat = re.compile(r'SYSTEM\("([^"]*)"\)') # If tracepat matches, set map['trace_syscalls'] to the 0'th group, etc. headpatterns = [ [ tracepat, 'trace_syscalls', 0], [ systempat, 'required_system', 0], [ runpat, 'run_args', 0] ] pwrite_in = r'pwrite64' pwrite_out = r'pwrite' # To create breakpoints while debugging this script def bp(): import pdb pdb.set_trace() def msg(s): print("syscall.py: " + s, file=sys.stderr) def die(s): msg(s) sys.exit(1) # If wttop appears as a prefix of pathname, strip it off. def simplify_path(wttop, pathname): wttop = os.path.join(wttop, "") if pathname.startswith(wttop): pathname = pathname[len(wttop):] return pathname def printfile(pathname, abbrev): print("================================================================") print(abbrev + " (" + pathname + "):") with open(pathname, 'r') as f: shutil.copyfileobj(f, sys.stdout) print("================================================================") # A line from a file: a modified string with the file name and line number # associated with it. class FileLine(str): filename = None linenum = 0 def __new__(cls, filename, linenum, value, *args, **kwargs): result = super(FileLine, cls).__new__(cls, value) result.filename = filename result.linenum = linenum return result def prefix(self): return self.filename + ':' + str(self.linenum) + ': ' def range_prefix(self, otherline): if self == otherline: othernum = '' elif otherline == None: othernum = '-EOF' else: othernum = '-' + str(otherline.linenum) return self.filename + ':' + str(self.linenum) + othernum + ': ' def normalize(self): changed = re.sub(pwrite_in, pwrite_out, self) if changed == self: normalized = self else: normalized = FileLine(self.filename, self.linenum, str(changed)) return normalized # Manage reading from a file, tracking line numbers. class Reader(object): # 'raw' means we don't ignore any lines # 'is_cpp' means input lines beginning with '#' indicate file/linenumber def __init__(self, wttop, filename, f, raw = True, is_cpp = False): self.wttop = wttop self.orig_filename = filename self.filename = filename self.f = f self.linenum = 1 self.raw = raw self.is_cpp = is_cpp self.context = [] if not self.f: die(self.filename + ': cannot open') def __enter__(self): if not self.f: return False return self def __exit__(self, typ, value, traceback): if self.f: self.f.close() self.f = None # Return True if the line is to be ignored. def ignore(self, line): if self.raw: return False return line == '' # strip a line of comments def strip_line(self, line): if not line: return None line = line.strip() if self.is_cpp and line.startswith('#'): parts = line.split() if len(parts) < 3 or not parts[1].isdigit(): msg('bad cpp input: ' + line) line = '' self.linenum = int(parts[1]) - 1 self.filename = parts[2].strip('"') if self.filename == '': self.filename = self.orig_filename if '//' in line: if line.startswith('//'): line = '' else: # This isn't exactly right, it would see "; //" # within a string or comment. m = re.match(r'^(.*;|.*\.\.\.)\s*//', line) if m: line = m.groups()[0].strip() return line def readline(self): rawline = self.f.readline() line = self.strip_line(rawline) self.add_context(rawline) while line != None and self.ignore(line): self.linenum += 1 rawline = self.f.readline() line = self.strip_line(rawline) self.add_context(rawline) if line: line = FileLine(self.filename, self.linenum, line) self.linenum += 1 else: line = '' # make this somewhat compatible with file.readline return line def get_context(self): s = '' for line in self.context: s += ' ' + str(line) return s def add_context(self, line): self.context.append(str(self.linenum) + ': ' + line) self.context = self.context[-5:] def close(self): self.f.close() # Read from a regular file. class FileReader(Reader): def __init__(self, wttop, filename, raw = True): return super(FileReader, self).__init__(wttop, filename, open(filename), raw, False) # Read from the C preprocessor run on a file. class PreprocessedReader(Reader): def __init__(self, wttop, filename, predefines, raw = True): sourcedir = os.path.dirname(filename) cmd = ['cc', '-E', '-I' + sourcedir] for name in dir(predefines): if not name.startswith('__'): cmd.append('-D' + name + '=' + str(predefines[name])) cmd.append('-') proc = subprocess.Popen(cmd, stdin=open(filename), stdout=subprocess.PIPE, universal_newlines=True) super(PreprocessedReader, self).__init__(wttop, filename, proc.stdout, raw, True) # Track options discovered in the 'head' section of the .run file. class HeadOpts: def __init__(self): self.run_args = None self.required_system = None self.trace_syscalls = None # Manage a run of the target program characterized by a .run file, # comparing output from the run and reporting differences. class Runner: def __init__(self, wttopdir, runfilename, exedir, testexe, strace, args, variables, defines): self.variables = variables self.defines = defines self.wttopdir = wttopdir self.runfilename = runfilename self.testexe = testexe self.exedir = exedir self.strace = strace self.args = args self.headopts = HeadOpts() self.dircreated = False self.strip_syscalls = None outfilename = args.outfilename errfilename = args.errfilename if outfilename == None: self.outfilename = os.path.join(exedir, 'stdout.txt') else: self.outfilename = outfilename if errfilename == None: self.errfilename = os.path.join(exedir, 'stderr.txt') else: self.errfilename = errfilename self.runfile = PreprocessedReader(self.wttopdir, runfilename, self.defines, False) def init(self, systemtype): # Read up until 'RUN()', setting attributes of self.headopts runline = '?' m = None while runline: runline = self.runfile.readline() m = None for pat,attr,group in headpatterns: m = re.match(pat, runline) if m: setattr(self.headopts, attr, m.groups()[group]) break if not m: self.fail(runline, "unknown header option: " + runline) return [ False, False ] if self.headopts.run_args != None: # found RUN()? break if not self.headopts.trace_syscalls: msg("'" + self.runfile.filename + "': needs TRACE(...)") return [ False, False ] runargs = self.headopts.run_args.strip() if len(runargs) > 0: if len(runargs) < 2 or runargs[0] != '"' or runargs[-1] != '"': msg("'" + self.runfile.filename + "': Missing double quotes in RUN arguments") return [ False, False ] runargs = runargs[1:-1] self.runargs = runargs.split() #print("SYSCALLS: " + self.headopts.trace_syscalls if self.headopts.required_system != None and \ self.headopts.required_system != systemtype: msg("skipping '" + self.runfile.filename + "': for '" + self.headopts.required_system + "', this system is '" + systemtype + "'") return [ False, True ] return [ True, False ] def close(self, forcePreserve): self.runfile.close() if self.exedir and self.dircreated and \ not self.args.preserve and not forcePreserve: os.chdir('..') shutil.rmtree(self.exedir) def fail(self, line, s): # make it work if line is None or is a plain string. try: prefix = simplify_path(self.wttopdir, line.prefix()) except: prefix = 'syscall.py: ' print(prefix + s, file=sys.stderr) def failrange(self, errfile, line, lineto, s): # make it work if line is None or is a plain string. try: prefix = simplify_path(self.wttopdir, line.range_prefix(lineto)) except: prefix = 'syscall.py: ' print(prefix + s + '\n' + errfile.get_context(), file=sys.stderr) def str_match(self, s1, s2): fuzzyRight = False if len(s1) < 2 or len(s2) < 2: return False if s1[-3:] == '...': fuzzyRight = True s1 = s1[:-3] if s2[-3:] == '...': s2 = s2[:-3] if s1[0] != '"' or s1[-1] != '"' or s2[0] != '"' or s2[-1] != '"': return False s1 = s1[1:-1] s2 = s2[1:-1] # We allow a trailing \0 if s1[-2:] == '\\0': s1 = s1[:-2] if s2[-2:] == '\\0': s2 = s2[:-2] if fuzzyRight: return s2.startswith(s2) else: return s1 == s2 def expr_eval(self, s): return eval(s, {}, self.variables) def arg_match(self, a1, a2): a1 = a1.strip() a2 = a2.strip() if a1 == a2: return True if len(a1) == 0 or len(a2) == 0: return False if a1[0] == '"': return self.str_match(a1, a2) #print(' arg_match: <' + a1 + '> <' + a2 + '>') try: a1value = self.expr_eval(a1) except Exception: self.fail(a1, 'unknown expression: ' + a1) return False try: a2value = self.expr_eval(a2) except Exception: self.fail(a2, 'unknown expression: ' + a2) return False return a1value == a2value or int(a1value) == int(a2value) def split_args(self, s): if s[0] == '(': s = s[1:] if s[-1] == ')': s = s[:-1] return argpat.split(s)[1::2] def args_match(self, args1, args2): #print('args_match: ' + str(s1) + ', ' + str(s2)) pos = 0 for a1 in args1: a1 = a1.strip() if a1 == '...': # match anything? return True if pos >= len(args2): return False if not self.arg_match(a1, args2[pos]): return False pos += 1 if pos < len(args2): return False return True # func(args); is shorthand for for ASSERT_EQ(func(args), xxx); # where xxx may be 0 or may be derived from one of the args. def call_compare(self, callname, result, eargs, errline): if callname in calls_returning_zero: return self.compare("EQ", result, "0", errline) elif callname == 'pwrite' or callname == 'pwrite64': return self.compare("EQ", re.sub(pwrite_in, pwrite_out, result), re.sub(pwrite_in, pwrite_out, eargs[2]), errline) else: self.fail(errline, 'call ' + callname + ': not known, use ASSERT_EQ()') def compare(self, compareop, left, right, errline): l = self.expr_eval(left) r = self.expr_eval(right) if (compareop == "EQ" and l == r) or \ (compareop == "NE" and l != r) or \ (compareop == "LT" and l < r) or \ (compareop == "LE" and l <= r) or \ (compareop == "GT" and l > r) or \ (compareop == "GE" and l >= r): return True else: self.fail(errline, 'call returned value: ' + left + ', comparison: (' + left + ' ' + compareop + ' ' + right + ') at line: ' + errline) return False def match_report(self, runline, errline, verbose, skiplines, result, desc): if result: if verbose: print('MATCH:') print(' ' + runline.prefix() + runline) print(' ' + errline.prefix() + errline) else: if verbose: if not skiplines: msg('Expecting ' + desc) print(' ' + runline.prefix() + runline + ' does not match:') print(' ' + errline.prefix() + errline) else: print(' (... match) ' + errline.prefix() + errline) return result def match(self, runline, errline, verbose, skiplines): m = re.match(outputpat, runline) if m: outwant = m.groups()[0] return self.match_report(runline, errline, verbose, skiplines, errline == outwant, 'output line') if self.args.systype == 'Linux': em = re.match(strace_pat, errline) elif self.args.systype == 'Darwin': em = re.match(dtruss_pat, errline) if not em: self.fail(errline, 'Unknown strace/dtruss output: ' + errline) return False gotcall = em.groups()[0] # filtering syscalls here if needed. If it's not a match, # mark the errline so it is retried. if self.strip_syscalls != None and gotcall not in self.strip_syscalls: errline.skip = True return False m = re.match(assignpat, runline) if m: if m.groups()[1] != gotcall: return self.match_report(runline, errline, verbose, skiplines, False, 'syscall to match assignment') rargs = self.split_args(m.groups()[2]) eargs = self.split_args(em.groups()[1]) result = self.args_match(rargs, eargs) if result: self.variables[m.groups()[0]] = em.groups()[2] return self.match_report(runline, errline, verbose, skiplines, result, 'syscall to match assignment') # pattern groups using example ASSERT_EQ(close(fd), 0); # 0 : comparison op ("EQ") # 1 : function call name "close" # 2 : function call args "(fd)" # 3 : comparitor "0" m = re.match(assertpat, runline) if m: if m.groups()[1] != gotcall: return self.match_report(runline, errline, verbose, skiplines, False, 'syscall to match ASSERT') rargs = self.split_args(m.groups()[2]) eargs = self.split_args(em.groups()[1]) result = self.args_match(rargs, eargs) if not result: return self.match_report(runline, errline, verbose, skiplines, result, 'syscall to match ASSERT') result = self.compare(m.groups()[0], em.groups()[2], m.groups()[3], errline) return self.match_report(runline, errline, verbose, skiplines, result, 'ASSERT') # A call without an enclosing ASSERT is reduced to an ASSERT, # depending on the particular system call. m = re.match(callpat, runline) if m: if m.groups()[0] != gotcall: return self.match_report(runline, errline, verbose, skiplines, False, 'syscall') rargs = self.split_args(m.groups()[1]) eargs = self.split_args(em.groups()[1]) result = self.args_match(rargs, eargs) if not result: return self.match_report(runline, errline, verbose, skiplines, result, 'syscall') result = self.call_compare(m.groups()[0], em.groups()[2], eargs, errline) return self.match_report(runline, errline, verbose, skiplines, result, 'syscall') self.fail(runline, 'unrecognized pattern in runfile:' + runline) return False def match_lines(self): outfile = FileReader(self.wttopdir, self.outfilename, True) errfile = FileReader(self.wttopdir, self.errfilename, True) if outfile.readline(): self.fail(None, 'output file has content, expected to be empty') return False with outfile, errfile: runlines = self.order_runfile(self.runfile) errline = errfile.readline() if re.match(dtruss_init_pat, errline): errline = errfile.readline() errline = errline.normalize() skiplines = False for runline in runlines: runline = runline.normalize() if runline == '...': skiplines = True if self.args.verbose: print('Fuzzy matching:') print(' ' + runline.prefix() + runline) continue first_errline = errline while errline and not self.match(runline, errline, self.args.verbose, skiplines): if skiplines or hasattr(errline, 'skip'): errline = errfile.readline().normalize() else: self.fail(runline, "expecting " + runline) self.failrange(errfile, first_errline, errline, "does not match") return False if not errline: self.fail(runline, "failed to match line: " + runline) self.failrange(errfile, first_errline, errline, "does not match") return False errline = errfile.readline() if re.match(dtruss_init_pat, errline): errline = errfile.readline() errline = errline.normalize() skiplines = False if errline and not skiplines: self.fail(errline, "extra lines seen starting at " + errline) return False return True def order_runfile(self, f): # In OS X, dtruss is implemented using dtrace's apparently buffered # printf writes to stdout, but that is all redirected to stderr. # Because of that, the test program's writes to stderr do not # interleave with dtruss output as it does with Linux's strace # (which writes directly to stderr). On OS X, we get the program's # output first, we compensate for this by moving all the # OUTPUT statements in the runfile to match first. This simple # approach will break if there is more data generated by OUTPUT # statements than a stdio buffer's size. matchout = (self.args.systype == 'Darwin') out = [] nonout = [] s = f.readline() while s: if matchout and re.match(outputpat, s): out.append(s) elif not re.match(discardpat, s): nonout.append(s) s = f.readline() out.extend(nonout) return out def run(self): if not self.exedir: self.fail(None, "Execution directory not set") return False if not os.path.isfile(self.testexe): msg("'" + self.testexe + "': no such file") return False shutil.rmtree(self.exedir, ignore_errors=True) os.mkdir(self.exedir) self.dircreated = True os.chdir(self.exedir) callargs = list(self.strace) trace_syscalls = self.headopts.trace_syscalls if self.args.systype == 'Linux': callargs.extend(['-e', 'trace=' + trace_syscalls ]) elif self.args.systype == 'Darwin': # dtrace has no option to limit the syscalls to be traced, # so we'll filter the output. self.strip_syscalls = re.sub(pwrite_in, pwrite_out, self.headopts.trace_syscalls).split(',') callargs.append(self.testexe) callargs.extend(self.runargs) outfile = open(self.outfilename, 'w') errfile = open(self.errfilename, 'w') if self.args.verbose: print('RUNNING: ' + str(callargs)) subret = subprocess.call(callargs, stdout=outfile, stderr=errfile) outfile.close() errfile.close() if subret != 0: msg("'" + self.testexe + "': exit value " + str(subret)) printfile(self.outfilename, "output") printfile(self.errfilename, "error") return False return True # Run the syscall program. class SyscallCommand: def __init__(self, disttop, builddir): self.disttop = disttop self.builddir = builddir def parse_args(self, argv): srcdir = os.path.join(self.disttop, 'test', 'syscall') self.exetopdir = os.path.join(self.builddir, 'test', 'syscall') self.incdir1 = os.path.join(self.disttop, 'src', 'include') self.incdir2 = self.builddir ap = argparse.ArgumentParser('Syscall test runner') ap.add_argument('--systype', help='override system type (Linux/Windows/Darwin)') ap.add_argument('--errfile', dest='errfilename', help='do not run the program, use this file as stderr') ap.add_argument('--outfile', dest='outfilename', help='do not run the program, use this file as stdout') ap.add_argument('--preserve', action="store_true", help='keep the WT_TEST.* directories') ap.add_argument('--verbose', action="store_true", help='add some verbose information') ap.add_argument('tests', nargs='*', help='the tests to run (defaults to all)') args = ap.parse_args() if not args.systype: args.systype = platform.system() # Linux, Windows, Darwin self.dorun = True if args.errfilename or args.outfilename: if len(args.tests) != 1: msg("one test is required when --errfile or --outfile" + " is specified") return False if not args.outfilename: args.outfilename = os.devnull if not args.errfilename: args.errfilename = os.devnull self.dorun = False # for now, we permit Linux and Darwin straceexe = None if args.systype == 'Linux': strace = [ 'strace' ] straceexe = 'strace' elif args.systype == 'Darwin': strace = [ 'sudo', 'dtruss' ] straceexe = 'dtruss' else: msg("systype '" + args.systype + "' unsupported") return False if not distutils.spawn.find_executable(straceexe): msg("strace: does not exist") return False self.args = args self.strace = strace return True def runone(self, runfilename, exedir, testexe, args): result = True runner = Runner(self.disttop, runfilename, exedir, testexe, self.strace, args, self.variables, self.defines) okay, skip = runner.init(args.systype) if not okay: if not skip: result = False else: if testexe: print('running ' + testexe) if not runner.run(): result = False if result: print('comparing:') print(' ' + simplify_path(self.disttop, runfilename)) print(' ' + simplify_path(self.disttop, runner.errfilename)) result = runner.match_lines() if not result and args.verbose: printfile(runfilename, "runfile") printfile(runner.errfilename, "trace output") runner.close(not result) if not result: print('************************ FAILED ************************') print(' see results in ' + exedir) print('') return result # Create a C program to get values for all defines we need. # The output of the program is Python code that we'll execute # directly to set the values. def build_system_defines(self): # variables is a symbol table that is used to # evaluate expressions both in the .run file and # in the output file. This is needed for strace, # which shows system call flags in symbolic form. self.variables = VariableContext() # defines is a symbol table that is used to # create preprocessor defines, effectively evaluating # all flag defines in the .run file. self.defines = VariableContext() program = \ '#include \n' + \ '#include \n' + \ '#include \n' + \ 'int main() {\n' for define in defines_used: program += '#ifdef ' + define + '\n' # output is Python that sets attributes of 'o'. program += ' printf("o.' + define + '=%d\\n", ' + \ define + ');\n' program += '#endif\n' program += \ ' return(0);\n' + \ '}\n' probe_c = os.path.join(self.exetopdir, "syscall_probe.c") probe_exe = os.path.join(self.exetopdir, "syscall_probe") with open(probe_c, "w") as f: f.write(program) ccargs = ['cc', '-o', probe_exe] ccargs.append('-I' + self.incdir1) ccargs.append('-I' + self.incdir2) if self.args.systype == 'Linux': ccargs.append('-D_GNU_SOURCE') ccargs.append(probe_c) subret = subprocess.call(ccargs) if subret != 0: msg("probe compilation returned " + str(subret)) return False proc = subprocess.Popen([probe_exe], stdout=subprocess.PIPE, universal_newlines=True) out, err = proc.communicate() subret = proc.returncode if subret != 0 or err: msg("probe run returned " + str(subret) + ", error=" + str(err)) return False if self.args.verbose: print('probe output:\n' + out) o = self.defines # The 'o' object will be modified. exec(out) # Run the produced Python. o = self.variables # Set these in variables too, so strace exec(out) # symbolic output is evaluated. if not self.args.preserve: os.remove(probe_c) os.remove(probe_exe) return True def execute(self): args = self.args result = True if not self.build_system_defines(): die('cannot build system defines') if not self.dorun: for testname in args.tests: result = self.runone(testname, None, None, args) and result else: if len(args.tests) > 0: tests = [] for arg in args.tests: abspath = os.path.abspath(arg) tests.append([os.path.dirname(abspath), [], [os.path.basename(abspath)]]) else: tests = os.walk(syscalldir) os.chdir(self.exetopdir) for path, subdirs, files in tests: testnum = -1 if len(files) <= 1 else 0 for name in files: if fnmatch.fnmatch(name, '*.run'): testname = os.path.basename(os.path.normpath(path)) runfilename = os.path.join(path, name) testexe = os.path.join(self.exetopdir, 'test_' + testname) exedir = os.path.join(self.exetopdir, 'WT_TEST.' + testname) # If there are multiple tests in this directory, # give each one its own execution dir. if testnum >= 0: exedir += '.' + str(testnum) testnum += 1 result = self.runone(runfilename, exedir, testexe, args) and result return result # Set paths, determining the top of the build. syscalldir = sys.path[0] wt_disttop = os.path.dirname(os.path.dirname(syscalldir)) # Note: this code is borrowed from test/suite/run.py # Check for a local build that contains the wt utility. First check in # current working directory, then in build_posix and finally in the disttop # directory. This isn't ideal - if a user has multiple builds in a tree we # could pick the wrong one. if os.path.isfile(os.path.join(os.getcwd(), 'wt')): wt_builddir = os.getcwd() elif os.path.isfile(os.path.join(wt_disttop, 'wt')): wt_builddir = wt_disttop elif os.path.isfile(os.path.join(wt_disttop, 'build_posix', 'wt')): wt_builddir = os.path.join(wt_disttop, 'build_posix') elif os.path.isfile(os.path.join(wt_disttop, 'wt.exe')): wt_builddir = wt_disttop else: die('unable to find useable WiredTiger build') cmd = SyscallCommand(wt_disttop, wt_builddir) if not cmd.parse_args(sys.argv): die('bad usage') if not cmd.execute(): print('For a HOW TO on debugging, see the top of syscall.py', file=sys.stderr) sys.exit(1) sys.exit(0)