--- /dev/null
+#!/usr/bin/env python
+
+import errno
+import getopt
+import os
+import re
+import shutil
+import subprocess
+import sys
+import tempfile
+
+from xml.sax import handler
+from xml.sax import make_parser
+from xml.sax.handler import feature_namespaces
+
+gcc_path = 'gcc'
+gcc_interaspect_lib = None
+gcc_interaspect_src = None
+
+# A whole host of specialized exceptions for things that can go
+# horribly wrong with the input XML test case.
+class XMLDataException(Exception):
+ def getMessage(self):
+ return "Fatal error reading test case description";
+
+class MissingAttr(XMLDataException):
+ def __init__(self, element, attr):
+ self.element = element
+ self.attr = attr
+
+ def getMessage(self):
+ return ("Element <" + self.element
+ + "> missing required attribute: " + self.attr);
+
+class MisplacedElement(XMLDataException):
+ def __init__(self, element, parent):
+ self.element = element
+ self.parent = parent
+
+ def getMessage(self):
+ return "Element <" + self.element + "> outside of <" + self.parent + ">"
+
+class BadName(XMLDataException):
+ def __init__(self, attr, val):
+ self.attr = attr
+ self.val = val
+
+ def getMessage(self):
+ return ("Value of " + self.attr + " not a legal C function name: "
+ + self.val)
+
+class BadId(XMLDataException):
+ def __init__(self, id_name, val):
+ self.id_name = id_name
+ self.val = val
+
+ def getMessage(self):
+ return ("Invalid reference: No " + self.id_name + " with id "
+ + self.val)
+
+class DuplicateHook(XMLDataException):
+ def __init__(self, run_name, hook_name):
+ self.run_name = run_name
+ self.hook_name = hook_name
+
+ def getMessage(self):
+ return (self.run_name + ": Run contains two hooks with same name, "
+ + self.hook_name + ", but different arguments")
+
+# A plug-in includes a source file and the descriptions of each hook
+# that the plug-in might add a call for.
+class Plugin:
+ def __init__(self, plugin_id, source):
+ self.plugin_id = plugin_id
+ self.source = source
+ self.hooks = {}
+
+# A hook description comprises the C function name for the hook and
+# the types for all its arguments.
+class Hook:
+ def __init__(self, name):
+ self.name = name
+ self.arg_list = []
+
+class Arg:
+ def __init__(self, arg_id, arg_type):
+ self.arg_id = arg_id
+ self.arg_type = arg_type
+
+# A run has a target source file along with a list of plug-ins that
+# should be used when compiling the target and the hooks necessary for
+# the target to compile with those plug-ins. It also has the full
+# output that the target is supposed to produce (because of the
+# plug-in) when it runs.
+class Run:
+ def __init__(self, name, target_source):
+ self.name = name;
+ self.target_source = target_source;
+ self.plugin_list = []
+ self.hooks = {}
+ self.output = []
+
+# A Value is one entry in the output list for a Run object.
+class Value:
+ def __init__(self, val_type, val):
+ self.val_type = val_type
+ self.val = val
+
+# Called for two hooks with the same name, return True if they have
+# _exactly_ the same arguments.
+def hooksMatch(hook1, hook2):
+ assert hook1.name == hook2.name
+
+ if len(hook1.arg_list) != len(hook2.arg_list):
+ return False
+
+ for i in range(len(hook1.arg_list)):
+ arg1 = hook1.arg_list[i]
+ arg2 = hook2.arg_list[i]
+ if arg1.arg_id != arg2.arg_id or arg1.arg_type != arg2.arg_type:
+ return False
+
+ return True
+
+class TestcaseHandler(handler.ContentHandler):
+ plugins = {}
+ current_plugin = None
+ current_hook = None
+
+ run_list = []
+ current_run = None
+ current_call_hook = None
+
+ current_value_arg_id = None
+ current_value_cdata = ""
+
+ def isAllowedInC(self, token):
+ if not re.match('[a-zA-Z_][0-9a-zA-Z_]*', token):
+ return False
+ elif token == 'if':
+ return False
+ elif token == 'else':
+ return False
+ elif token == 'for':
+ return False
+ elif token == 'do':
+ return False
+ elif token == 'while':
+ return False
+ elif token == 'void':
+ return False
+ elif token == 'unsigned':
+ return False
+ elif token == 'short':
+ return False
+ elif token == 'long':
+ return False
+ elif token == 'int':
+ return False
+ elif token == 'float':
+ return False
+ elif token == 'double':
+ return False
+ else:
+ return True
+
+ def startTestcase(self, attrs):
+ self.name = attrs.get('name')
+ if self.name is None:
+ raise MissingAttr("testcase", "name")
+
+ def startPlugin(self, attrs):
+ plugin_id = attrs.get('id')
+ if plugin_id is None:
+ raise MissingAttr("plugin", "id")
+
+ source = attrs.get('source')
+ if source is None:
+ raise MissingAttr("plugin", "source")
+
+ self.current_plugin = Plugin(plugin_id, source)
+
+ def endPlugin(self):
+ self.plugins[self.current_plugin.plugin_id] = (self.current_plugin)
+ self.current_plugin = None
+
+ def startHook(self, attrs):
+ if self.current_hook is not None:
+ raise MisplacedElement("hook", "plugin");
+
+ name = attrs.get('name')
+ if name is None:
+ raise MissingAttr("hook", "name")
+ elif not self.isAllowedInC(name):
+ raise BadName("name", name)
+
+ self.current_hook = Hook(name)
+
+ def endHook(self):
+ if self.current_plugin is None:
+ raise MisplacedElement("hook", "plugin")
+
+ self.current_plugin.hooks[self.current_hook.name] = self.current_hook
+ self.current_hook = None
+
+ def startArg(self, attrs):
+ if self.current_hook is None:
+ raise MisplacedElement("arg", "hook")
+
+ arg_id = attrs.get('id')
+ if arg_id is None:
+ raise MissingAttr("arg", "id")
+
+ arg_type = attrs.get('type')
+ if arg_type is None:
+ raise MissingAttr("arg", "type")
+
+ arg = Arg(arg_id, arg_type)
+ self.current_hook.arg_list.append(arg)
+
+ def startRun(self, attrs):
+ if self.current_plugin is not None:
+ raise MisplacedElement("run", "testcase")
+
+ run_name = attrs.get('name')
+ if run_name is None:
+ raise MissingAttr("run", "name")
+
+ target_source = attrs.get('target')
+ if target_source is None:
+ raise MissingAttr("run", "target")
+
+ self.current_run = Run(run_name, target_source)
+
+ def endRun(self):
+ self.run_list.append(self.current_run)
+ self.current_run = None
+
+ def startUsing(self, attrs):
+ if self.current_run is None:
+ raise MisplacedElement("using", "run")
+
+ plugin_id = attrs.get('plugin')
+ if plugin_id is None:
+ raise MissingAttr("using", "plugin")
+
+ try:
+ plugin = self.plugins[plugin_id]
+ except KeyError as e:
+ raise BadId("plugin", plugin_id)
+
+ # Add all of this plug-in's hooks to the run.
+ for name, hook in plugin.hooks.iteritems():
+ dup_hook = None
+ try:
+ dup_hook = self.current_run.hooks[name]
+ except KeyError as e:
+ pass
+
+ if dup_hook is None:
+ # We don't have any hooks with this name.
+ self.current_run.hooks[name] = hook
+ else:
+ # There is already a hook with this name. Make sure
+ # it matches.
+ if not hooksMatch(hook, dup_hook):
+ raise DuplicateHook(self.current_run.name, name)
+
+ # And the plug-in itself!
+ self.current_run.plugin_list.append(plugin)
+
+ def startCall(self, attrs):
+ if self.current_run is None or self.current_call_hook is not None:
+ raise MisplacedElement("call", "run")
+
+ hook_name = attrs.get('name')
+ if hook_name is None:
+ raise MissingAttr("call", "name")
+
+ try:
+ hook = self.current_run.hooks[hook_name]
+ except KeyError as e:
+ raise BadId("hook", hook_name)
+
+ self.current_call_hook = hook
+
+ output_val = Value('Hook', hook_name)
+ self.current_run.output.append(output_val)
+
+ def endCall(self):
+ self.current_call_hook = None
+
+ def startValue(self, attrs):
+ if (self.current_call_hook is None
+ or self.current_value_arg_id is not None):
+ raise MisplacedElement("value", "call")
+
+ arg_id = attrs.get('id')
+ if arg_id is None:
+ raise MissingAttr("value", "id")
+
+ # We can't examine the cdata until the endValue event. Stash
+ # the argument name until then.
+ self.current_value_arg_id = arg_id
+ self.current_value_cdata = ""
+
+ def endValue(self):
+ assert self.current_call_hook is not None
+ assert self.current_value_arg_id is not None
+
+ # Find the arg with the given arg id. These arguments are not
+ # stored in an associative array because their order is
+ # important. These lists are way too small to justify the
+ # overhead of a separate index.
+ arg = None
+ for arg_it in self.current_call_hook.arg_list:
+ if self.current_value_arg_id == arg_it.arg_id:
+ arg = arg_it
+ if arg is None:
+ raise BadId("arg", self.current_value_arg_id)
+
+ new_value = Value(arg.arg_type, self.current_value_cdata)
+ self.current_run.output.append(new_value)
+
+ self.current_value_arg_id = None
+
+ # Parsing with SAX is simple but tedious. There's a start
+ # function for each tag and an end function for some tags. They
+ # just stash the data they find into a relevant data structure
+ # (after validating that it makes sense).
+ def startElement(self, name, attrs):
+ if name == 'testcase':
+ self.startTestcase(attrs)
+ elif name == 'plugin':
+ self.startPlugin(attrs)
+ elif name == 'hook':
+ self.startHook(attrs)
+ elif name == 'arg':
+ self.startArg(attrs)
+ elif name == 'run':
+ self.startRun(attrs)
+ elif name == 'using':
+ self.startUsing(attrs)
+ elif name == 'call':
+ self.startCall(attrs)
+ elif name == 'value':
+ self.startValue(attrs)
+
+ def endElement(self, name):
+ if name == 'plugin':
+ self.endPlugin()
+ elif name == 'hook':
+ self.endHook()
+ elif name == 'run':
+ self.endRun()
+ elif name == 'call':
+ self.endCall()
+ elif name == 'value':
+ self.endValue()
+
+ def characters(self, chars):
+ if self.current_value_arg_id is not None:
+ self.current_value_cdata += chars
+
+def getCheckFormat(c_type):
+ c_type = c_type.strip()
+ if re.match(r".*\bchar\s+\*", c_type):
+ return r"string: %s\n"
+ if re.match(r".*\*$", c_type):
+ return r"pointer: %p\n"
+ elif c_type == 'int':
+ return r"int: %d\n"
+ else:
+ return None
+
+# Print the C prototype and function body for the given plug-in hook
+# to the given stream.
+def printHook(stream, hook):
+ params = ['{0:s} arg{1:d}'.format(hook.arg_list[i].arg_type, i) for i in
+ range(len(hook.arg_list))]
+ param_text = ', '.join(params)
+
+ stream.write('void {0:s}({1:s})\n'.format(hook.name, param_text))
+ stream.write('{\n');
+ stream.write(' check_printf("hook: {0:s}\\n");\n'.format(hook.name))
+ for i in range(len(hook.arg_list)):
+ arg = hook.arg_list[i]
+ check_format = getCheckFormat(arg.arg_type)
+ if check_format is not None:
+ stream.write(' check_printf("{0:s}", arg{1:d});\n'.format(check_format, i))
+ stream.write('}\n');
+
+# Run GCC with the given arguments. On failure, print an appropriate
+# error and return False.
+# If the compile fails because of an error in the C file, runGCC also
+# prints compile_fail_msg.
+def runGCC(args, compile_fail_msg):
+ args = [gcc_path] + args
+ try:
+ gcc_proc = subprocess.Popen(args, stderr = subprocess.PIPE)
+ except OSError as e:
+ if e.errno == errno.ENOENT:
+ print "Fatal -- Bad path to GCC:", gcc_path
+ return False
+ else:
+ print "Fatal error launching GCC:"
+ print e.strerror
+ return False
+ (stdoutdata, stderrdata) = gcc_proc.communicate();
+ gcc_proc.wait();
+
+ if gcc_proc.returncode != 0:
+ print compile_fail_msg
+ print stderrdata
+ return False
+
+ return True
+
+# Compile and link an InterAspect plug-in! Several compilation files
+# get created in the given working directory. It is the caller's
+# responsibility to delete these files. No files will be left over in
+# other directories, though.
+#
+# The compiled plug-in file will be named plugin_base_name.so.1.0.0.
+#
+# On success, compilePlugin returns the path to the final plug-in .so
+# file (which will be in working_dir). On failure, the return value
+# is None.
+def compilePlugin(working_dir, plugin_id, plugin_base_name, plugin_source):
+ plugin_lib_name = '{0:s}/{1:s}.so.1.0.0'.format(working_dir,
+ plugin_base_name)
+
+
+ include_flag = '-I{0:s}/src'.format(gcc_interaspect_src)
+ cmd_args = ['-Wall', '-Werror', include_flag, '-fPIC', '-shared',
+ '-Wl,-soname,{0:s}.so.1'.format(plugin_base_name), '-o',
+ plugin_lib_name, plugin_source, gcc_interaspect_lib]
+ result = runGCC(cmd_args,
+ 'Fatal -- Failed to compile plugin: "{0:s}"'.format(plugin_id))
+ if not result:
+ return None
+
+ return plugin_lib_name
+
+# Compile the testcase instrumentation target, along with
+# auto-generated hook functions. Several compilation files get
+# created in the given working directory. It is the caller's
+# responsibility to delete these files. No files will be left over in
+# other directories, though.
+#
+# working_dir: A temporary directory to store intermediate files and
+# the resulting executable.
+#
+# target_source: The C source file for the target program (the program
+# that we intend to instrument with the test plug-ins).
+#
+# plugin_libs: A list of .so plug-in files that will be used to
+# compile the target program.
+#
+# On success, compileTestcase returns the path to the final test
+# executable (which will be in working_dir). On failure, the return
+# value is None.
+def compileTestcase(working_dir, target_source, plugin_libs, hooks):
+ # Create a C file with the necessary plug-in hooks and compile it.
+ hook_file_name = working_dir + '/hooks.c'
+ hook_file = open(hook_file_name, 'w');
+ hook_file.write('#include "test-driver.h"\n')
+ for name, hook in hooks.iteritems():
+ printHook(hook_file, hook)
+ hook_file.close()
+
+ test_include = '-I{0:s}/test'.format(gcc_interaspect_src)
+ hook_o_file = working_dir + '/hooks.o'
+ cmd_args = ['-Wall', '-Werror', test_include, '-c', '-o', hook_o_file,
+ hook_file_name]
+ result = runGCC(cmd_args, "Fatal -- Failed to compile plug-in hooks:")
+ if not result:
+ return None
+
+ # Compile the test driver, which has the main function.
+ main_c_file = gcc_interaspect_src + 'test/test-driver.c'
+ main_o_file = working_dir + '/test-driver.o'
+ cmd_args = ['-Wall', '-Werror', '-c', '-o', main_o_file, main_c_file]
+ result = runGCC(cmd_args, "Fatal -- Failed to compile test driver:")
+ if not result:
+ return None
+
+ # Compile the target itself.
+ target_o_file = working_dir + '/target.o'
+ cmd_args = ['-fplugin={0:s}'.format(lib) for lib in plugin_libs]
+ cmd_args += ['-Wall', '-Werror', '-c', '-o', target_o_file, target_source]
+ result = runGCC(cmd_args, "Fatal -- Failed to compile target source:")
+ if not result:
+ return None
+
+ # Link the final binary
+ executable = working_dir + '/test-executable'
+ cmd_args = ['-o', executable, hook_o_file, main_o_file, target_o_file]
+ result = runGCC(cmd_args, "Fatal -- Failed to link test case:")
+ if not result:
+ return None
+
+ return executable
+
+# Compile the run's target program with all the requested plug-ins
+# then run the resulting executable and check that its output is as
+# expected.
+# Returns True if the run passes.
+def doRun(run):
+ print " Run:", run.name
+
+ tmp_dir = tempfile.mkdtemp(prefix='test-out-')
+
+ # Compile all the plug-ins for this test.
+ plugin_libs = []
+ for i in range(len(run.plugin_list)):
+ plugin = run.plugin_list[i]
+ plugin_base_name = 'test_plugin_{0:d}'.format(i + 1)
+ plugin_lib_name = compilePlugin(tmp_dir, plugin.plugin_id,
+ plugin_base_name, plugin.source)
+ if plugin_lib_name is None:
+ return False
+ plugin_libs.append(plugin_lib_name)
+
+ test_executable = compileTestcase(tmp_dir, run.target_source, plugin_libs,
+ run.hooks)
+
+ if test_executable is not None:
+ test_proc = subprocess.Popen([test_executable])
+ test_proc.wait()
+
+ # Delete temp directory
+ shutil.rmtree(path = tmp_dir, ignore_errors = True)
+
+def usage():
+ sys.stderr.write(
+"""Usage:
+ run-testcase.py [options] testcase.xml
+
+Options
+ --with-gcc: Path to gcc to compile the tests with
+ --with-ia-lib-dir: Directory with InterAspect library file
+ --with-ia-src-dir: Directory with InterAspect source
+""")
+ # End function usage()
+
+# Return True if filename exists and is readable. If it is not, print
+# an appropriate error message and return False.
+def checkFileReadable(filename):
+ try:
+ filehandle = open(filename, 'r')
+ filehandle.close()
+ except IOError as e:
+ sys.stderr.write('{0:s}: {1:s}\n'.format(filename, e.strerror))
+ return False
+
+ return True
+
+if __name__ == '__main__':
+ # Deal with command line arguments
+ try:
+ long_switches = ["with-gcc=", "with-ia-lib-dir=", "with-ia-src-dir="]
+ opts, args = getopt.gnu_getopt(sys.argv[1:], "", long_switches)
+ except getopt.GetoptError:
+ usage()
+ sys.exit(1)
+
+ for opt, arg in opts:
+ if (opt == "--with-gcc"):
+ gcc_path = arg
+ elif (opt == "--with-ia-lib-dir"):
+ gcc_interaspect_lib = arg + '/libinteraspect.a'
+ elif (opt == "--with-ia-src-dir"):
+ gcc_interaspect_src = arg
+ else:
+ assert(1)
+
+ # Validate args
+ if gcc_interaspect_lib is None:
+ sys.stderr.write("Must specify --with-ia-lib-dir\n")
+ sys.exit(1)
+ elif not checkFileReadable(gcc_interaspect_lib):
+ sys.exit(1)
+ elif gcc_interaspect_src is None:
+ sys.stderr.write("Must specify --with-ia-src-dir\n")
+ sys.exit(1)
+ elif not checkFileReadable('{0:s}/src/aop.h'.format(gcc_interaspect_src)):
+ sys.exit(1)
+
+ # There should be exactly one non-option arg: the test case
+ if len(args) != 1:
+ usage()
+ sys.exit(1)
+ test_description = args[0]
+
+ parser = make_parser()
+ parser.setFeature(feature_namespaces, 0)
+
+ dh = TestcaseHandler()
+ parser.setContentHandler(dh)
+
+ try:
+ parser.parse(test_description)
+ except IOError as e:
+ sys.stderr.write('{0:s}: {1:s}\n'.format(test_description, e.strerror))
+ sys.exit(1)
+ except XMLDataException as e:
+ print e.getMessage()
+ sys.exit(1)
+
+ print "Testcase:", dh.name
+ for run in dh.run_list:
+ doRun(run)